diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl new file mode 100644 index 0000000000..d3d04e53df --- /dev/null +++ b/Shaders/std/sky.glsl @@ -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 diff --git a/Sources/armory/math/Helper.hx b/Sources/armory/math/Helper.hx index 03351492f2..d3579d1e30 100644 --- a/Sources/armory/math/Helper.hx +++ b/Sources/armory/math/Helper.hx @@ -72,4 +72,13 @@ class Helper { if (value <= leftMin) return rightMin; return map(value, leftMin, leftMax, rightMin, rightMax); } + + /** + Return the sign of the given value represented as `1.0` (positive value) + or `-1.0` (negative value). The sign of `0` is `0`. + **/ + public static inline function sign(value: Float): Float { + if (value == 0) return 0; + return (value < 0) ? -1.0 : 1.0; + } } diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index 91c55f3042..70751dc68f 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -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; - } - else if (link == "_ltcMag") { - if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC(); - return armory.data.ConstData.ltcMagTex; + public static function textureLink(object: Object, mat: MaterialData, link: String): Null { + 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 } - #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 { var v: Vec4 = null; - #if arm_hosek - if (link == "_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; - } - } - 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) { + 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; + } + } + 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.F.x; - v.y = armory.renderpath.HosekWilkie.data.F.y; - v.z = armory.renderpath.HosekWilkie.data.F.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 } - 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; + + return v; + } + + public static function vec2Link(object: Object, mat: MaterialData, link: String): Null { + 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]; + } } } - #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 { - #if rp_dynres - if (link == "_dynamicScale") { - return armory.renderpath.DynamicResolutionScale.dynamicScale; - } - #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; + 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 return null; } } diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx new file mode 100644 index 0000000000..7dcba4a258 --- /dev/null +++ b/Sources/armory/renderpath/Nishita.hx @@ -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); + } +} diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx index 2260ac704b..f8709ab443 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -52,7 +52,6 @@ class PhysicsWorld extends Trait { public var rbMap: Map; public var conMap: Map; public var timeScale = 1.0; - var timeStep = 1 / 60; var maxSteps = 1; public var solverIterations = 10; public var hitPointWorld = new Vec4(); @@ -67,7 +66,7 @@ class PhysicsWorld extends Trait { public static var physTime = 0.0; #end - public function new(timeScale = 1.0, timeStep = 1 / 60, solverIterations = 10) { + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10) { super(); if (nullvec) { @@ -81,8 +80,7 @@ class PhysicsWorld extends Trait { sceneRemoved = false; this.timeScale = timeScale; - this.timeStep = timeStep; - maxSteps = timeStep < 1 / 60 ? 10 : 1; + this.maxSteps = maxSteps; this.solverIterations = solverIterations; // First scene @@ -269,7 +267,13 @@ class PhysicsWorld extends Trait { if (preUpdates != null) for (f in preUpdates) f(); - world.stepSimulation(timeStep, maxSteps, t); + //Bullet physics fixed timescale + var fixedTime = 1.0 / 60; + + //This condition must be satisfied to not loose time + var currMaxSteps = t < (fixedTime * maxSteps) ? maxSteps : 1; + + world.stepSimulation(t, currMaxSteps, fixedTime); updateContacts(); for (rb in rbMap) @:privateAccess rb.physicsUpdate(); diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 90dc67acc1..d213fc89df 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2614,7 +2614,7 @@ def export_scene_traits(self) -> None: rbw = self.scene.rigidbody_world if rbw is not None and rbw.enabled: - out_trait['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] + out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] self.output['traits'].append(out_trait) @@ -2861,6 +2861,7 @@ def post_export_world(world: bpy.types.World, out_world: Dict): 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') @@ -2875,16 +2876,9 @@ def post_export_world(world: bpy.types.World, out_world: Dict): 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 diff --git a/blender/arm/lightmapper/__init__.py b/blender/arm/lightmapper/__init__.py index eaf15cec1d..8df717b451 100644 --- a/blender/arm/lightmapper/__init__.py +++ b/blender/arm/lightmapper/__init__.py @@ -1 +1 @@ -__all__ = ('Operators', 'Properties', 'Utility', 'Keymap') \ No newline at end of file +__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file diff --git a/blender/arm/lightmapper/assets/TLM_Overlay.png b/blender/arm/lightmapper/assets/TLM_Overlay.png new file mode 100644 index 0000000000..50bef87f41 Binary files /dev/null and b/blender/arm/lightmapper/assets/TLM_Overlay.png differ diff --git a/blender/arm/lightmapper/assets/tlm_data.blend b/blender/arm/lightmapper/assets/tlm_data.blend index 409c894b33..8dabd2cbe1 100644 Binary files a/blender/arm/lightmapper/assets/tlm_data.blend and b/blender/arm/lightmapper/assets/tlm_data.blend differ diff --git a/blender/arm/lightmapper/keymap/keymap.py b/blender/arm/lightmapper/keymap/keymap.py index adfa72c287..96a52f611e 100644 --- a/blender/arm/lightmapper/keymap/keymap.py +++ b/blender/arm/lightmapper/keymap/keymap.py @@ -9,6 +9,8 @@ def register(): winman = bpy.context.window_manager keyman = winman.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW") + + #TODO - In Armory3D, merge with keymap.py keyman.keymap_items.new('tlm.build_lightmaps', type='F6', value='PRESS') keyman.keymap_items.new('tlm.clean_lightmaps', type='F7', value='PRESS') tlm_keymaps.append(keyman) diff --git a/blender/arm/lightmapper/operators/__init__.py b/blender/arm/lightmapper/operators/__init__.py index 05a2ed7da5..7098994fdc 100644 --- a/blender/arm/lightmapper/operators/__init__.py +++ b/blender/arm/lightmapper/operators/__init__.py @@ -6,10 +6,11 @@ tlm.TLM_BuildLightmaps, tlm.TLM_CleanLightmaps, tlm.TLM_ExploreLightmaps, - tlm.TLM_EnableSelection, + tlm.TLM_EnableSet, tlm.TLM_DisableSelection, tlm.TLM_RemoveLightmapUV, tlm.TLM_SelectLightmapped, + tlm.TLM_ToggleTexelDensity, installopencv.TLM_Install_OpenCV, tlm.TLM_AtlasListNewItem, tlm.TLM_AtlastListDeleteItem, @@ -20,6 +21,8 @@ tlm.TLM_StartServer, tlm.TLM_BuildEnvironmentProbes, tlm.TLM_CleanBuildEnvironmentProbes, + tlm.TLM_PrepareUVMaps, + tlm.TLM_LoadLightmaps, imagetools.TLM_ImageUpscale, imagetools.TLM_ImageDownscale diff --git a/blender/arm/lightmapper/operators/imagetools.py b/blender/arm/lightmapper/operators/imagetools.py index 830629732f..9e20311498 100644 --- a/blender/arm/lightmapper/operators/imagetools.py +++ b/blender/arm/lightmapper/operators/imagetools.py @@ -1,4 +1,4 @@ -import bpy, os, time +import bpy, os, time, importlib class TLM_ImageUpscale(bpy.types.Operator): bl_idname = "tlm.image_upscale" @@ -8,6 +8,69 @@ class TLM_ImageUpscale(bpy.types.Operator): def invoke(self, context, event): + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + basename = os.path.splitext(filename)[0] + extension = os.path.splitext(filename)[1] + + size_x = active_image.size[0] + size_y = active_image.size[1] + + dir_path = os.path.dirname(os.path.realpath(img_path)) + + #newfile = os.path.join(dir_path, basename + "_" + str(size_x) + "_" + str(size_y) + extension) + newfile = os.path.join(dir_path, basename + extension) + os.rename(img_path, newfile) + + basefile = cv2.imread(newfile, cv2.IMREAD_UNCHANGED) + + scale_percent = 200 # percent of original size + width = int(basefile.shape[1] * scale_percent / 100) + height = int(basefile.shape[0] * scale_percent / 100) + dim = (width, height) + + if active_image.TLM_ImageProperties.tlm_image_scale_method == "Nearest": + interp = cv2.INTER_NEAREST + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Area": + interp = cv2.INTER_AREA + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Linear": + interp = cv2.INTER_LINEAR + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Cubic": + interp = cv2.INTER_CUBIC + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Lanczos": + interp = cv2.INTER_LANCZOS4 + + resized = cv2.resize(basefile, dim, interpolation = interp) + + #resizedFile = os.path.join(dir_path, basename + "_" + str(width) + "_" + str(height) + extension) + resizedFile = os.path.join(dir_path, basename + extension) + + cv2.imwrite(resizedFile, resized) + + active_image.filepath_raw = resizedFile + bpy.ops.image.reload() + + print(newfile) + print(img_path) + + else: + + print("Please save image") + print("Upscale") return {'RUNNING_MODAL'} @@ -20,6 +83,111 @@ class TLM_ImageDownscale(bpy.types.Operator): def invoke(self, context, event): - print("Downscale") + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + basename = os.path.splitext(filename)[0] + extension = os.path.splitext(filename)[1] + + size_x = active_image.size[0] + size_y = active_image.size[1] + + dir_path = os.path.dirname(os.path.realpath(img_path)) + + #newfile = os.path.join(dir_path, basename + "_" + str(size_x) + "_" + str(size_y) + extension) + newfile = os.path.join(dir_path, basename + extension) + os.rename(img_path, newfile) + + basefile = cv2.imread(newfile, cv2.IMREAD_UNCHANGED) + + scale_percent = 50 # percent of original size + width = int(basefile.shape[1] * scale_percent / 100) + height = int(basefile.shape[0] * scale_percent / 100) + dim = (width, height) + + if dim[0] > 1 or dim[1] > 1: + + if active_image.TLM_ImageProperties.tlm_image_scale_method == "Nearest": + interp = cv2.INTER_NEAREST + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Area": + interp = cv2.INTER_AREA + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Linear": + interp = cv2.INTER_LINEAR + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Cubic": + interp = cv2.INTER_CUBIC + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Lanczos": + interp = cv2.INTER_LANCZOS4 + + resized = cv2.resize(basefile, dim, interpolation = interp) + + #resizedFile = os.path.join(dir_path, basename + "_" + str(width) + "_" + str(height) + extension) + resizedFile = os.path.join(dir_path, basename + extension) + + cv2.imwrite(resizedFile, resized) + + active_image.filepath_raw = resizedFile + bpy.ops.image.reload() + + print(newfile) + print(img_path) + + else: + + print("Please save image") + + print("Upscale") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchUp(bpy.types.Operator): + bl_idname = "tlm.image_switchup" + bl_label = "Quickswitch Up" + bl_description = "Switches to a cached upscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch up") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchDown(bpy.types.Operator): + bl_idname = "tlm.image_switchdown" + bl_label = "Quickswitch Down" + bl_description = "Switches to a cached downscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch Down") return {'RUNNING_MODAL'} \ No newline at end of file diff --git a/blender/arm/lightmapper/operators/installopencv.py b/blender/arm/lightmapper/operators/installopencv.py index b3d173a669..21f5e37fb1 100644 --- a/blender/arm/lightmapper/operators/installopencv.py +++ b/blender/arm/lightmapper/operators/installopencv.py @@ -21,7 +21,10 @@ def execute(self, context): print("Module OpenCV") - pythonbinpath = bpy.app.binary_path_python + if (2, 91, 0) > bpy.app.version: + pythonbinpath = bpy.app.binary_path_python + else: + pythonbinpath = sys.executable if platform.system() == "Windows": pythonlibpath = os.path.join(os.path.dirname(os.path.dirname(pythonbinpath)), "lib") diff --git a/blender/arm/lightmapper/operators/tlm.py b/blender/arm/lightmapper/operators/tlm.py index ace239d773..a46f36040f 100644 --- a/blender/arm/lightmapper/operators/tlm.py +++ b/blender/arm/lightmapper/operators/tlm.py @@ -1,9 +1,36 @@ -import bpy, os, time, blf, webbrowser, platform +import bpy, os, time, blf, webbrowser, platform, numpy, bmesh import math, subprocess, multiprocessing +from .. utility import utility from .. utility import build from .. utility.cycles import cache from .. network import server +def setObjectLightmapByWeight(minimumRes, maximumRes, objWeight): + + availableResolutions = [32,64,128,256,512,1024,2048,4096,8192] + + minRes = minimumRes + minResIdx = availableResolutions.index(minRes) + maxRes = maximumRes + maxResIdx = availableResolutions.index(maxRes) + + exampleWeight = objWeight + + if minResIdx == maxResIdx: + pass + else: + + increment = 1.0/(maxResIdx-minResIdx) + + assortedRange = [] + + for a in numpy.arange(0.0, 1.0, increment): + assortedRange.append(round(a, 2)) + + assortedRange.append(1.0) + nearestWeight = min(assortedRange, key=lambda x:abs(x - exampleWeight)) + return (availableResolutions[assortedRange.index(nearestWeight) + minResIdx]) + class TLM_BuildLightmaps(bpy.types.Operator): bl_idname = "tlm.build_lightmaps" bl_label = "Build Lightmaps" @@ -52,13 +79,13 @@ def execute(self, context): for file in os.listdir(dirpath): os.remove(os.path.join(dirpath + "/" + file)) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -75,8 +102,8 @@ def execute(self, context): if image.name.endswith("_baked"): bpy.data.images.remove(image, do_unlink=True) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: @@ -92,14 +119,17 @@ def execute(self, context): bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj - #print(x) uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + for i in range(0, len(uv_layers)): - if uv_layers[i].name == 'UVMap_Lightmap': + if uv_layers[i].name == uv_channel: uv_layers.active_index = i - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift A") break bpy.ops.object.mode_set(mode='EDIT') @@ -111,9 +141,11 @@ def execute(self, context): bpy.ops.object.mode_set(mode='OBJECT') if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - #print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name) print("Resized for obj: " + obj.name) + if "Lightmap" in obj: + del obj["Lightmap"] + return {'FINISHED'} class TLM_ExploreLightmaps(bpy.types.Operator): @@ -153,63 +185,285 @@ def execute(self, context): return {'FINISHED'} -class TLM_EnableSelection(bpy.types.Operator): - """Enable for selection""" - bl_idname = "tlm.enable_selection" - bl_label = "Enable for selection" - bl_description = "Enable for selection" +class TLM_EnableSet(bpy.types.Operator): + """Enable for set""" + bl_idname = "tlm.enable_set" + bl_label = "Enable for set" + bl_description = "Enable for set" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): scene = context.scene - for obj in bpy.context.selected_objects: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + weightList = {} #ObjName : [Dimension,Weight] + max = 0 - if scene.TLM_SceneProperties.tlm_override_object_settings: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution - obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode - obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = scene.TLM_SceneProperties.tlm_mesh_unwrap_margin - obj.TLM_ObjectProperties.tlm_postpack_object = scene.TLM_SceneProperties.tlm_postpack_object + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - obj.TLM_ObjectProperties.tlm_atlas_pointer = scene.TLM_SceneProperties.tlm_atlas_pointer + print("Enabling for scene: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + print("Enabling for selection: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + print("Enabling for designated: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object - obj.TLM_ObjectProperties.tlm_postatlas_pointer = scene.TLM_SceneProperties.tlm_postatlas_pointer + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + print("") + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + return{'FINISHED'} class TLM_DisableSelection(bpy.types.Operator): - """Disable for selection""" + """Disable for set""" bl_idname = "tlm.disable_selection" - bl_label = "Disable for selection" + bl_label = "Disable for set" bl_description = "Disable for selection" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - for obj in bpy.context.selected_objects: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + scene = context.scene + + weightList = {} #ObjName : [Dimension,Weight] + max = 0 + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + return{'FINISHED'} class TLM_RemoveLightmapUV(bpy.types.Operator): - """Remove Lightmap UV for selection""" + """Remove Lightmap UV for set""" bl_idname = "tlm.remove_uv_selection" bl_label = "Remove Lightmap UV" - bl_description = "Remove Lightmap UV for selection" + bl_description = "Remove Lightmap UV for set" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - for obj in bpy.context.selected_objects: - if obj.type == "MESH": - uv_layers = obj.data.uv_layers + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - for uvlayer in uv_layers: - if uvlayer.name == "UVMap_Lightmap": - uv_layers.remove(uvlayer) + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) return{'FINISHED'} @@ -222,8 +476,8 @@ class TLM_SelectLightmapped(bpy.types.Operator): def execute(self, context): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: obj.select_set(True) @@ -278,7 +532,7 @@ def execute(self, context): list = scene.TLM_AtlasList index = scene.TLM_AtlasListItem - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: atlasName = scene.TLM_AtlasList[index].name @@ -310,7 +564,7 @@ def execute(self, context): list = scene.TLM_PostAtlasList index = scene.TLM_PostAtlasListItem - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: atlasName = scene.TLM_PostAtlasList[index].name @@ -437,7 +691,7 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator): def invoke(self, context, event): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.type == "LIGHT_PROBE": if obj.data.type == "CUBEMAP": @@ -500,7 +754,7 @@ def invoke(self, context, event): cam.rotation_euler = positions[val] filename = os.path.join(directory, val) + "_" + camobj_name + ".hdr" - bpy.data.scenes['Scene'].render.filepath = filename + bpy.context.scene.render.filepath = filename print("Writing out: " + val) bpy.ops.render.render(write_still=True) @@ -642,7 +896,7 @@ def invoke(self, context, event): subprocess.call([envpipe3], shell=True) - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: obj.select_set(False) cam_obj.select_set(True) @@ -686,7 +940,92 @@ def execute(self, context): scene = context.scene - + return {'FINISHED'} + +class TLM_PrepareUVMaps(bpy.types.Operator): + bl_idname = "tlm.prepare_uvmaps" + bl_label = "Prepare UV maps" + bl_description = "Prepare UV lightmaps for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + + + return {'FINISHED'} + +class TLM_LoadLightmaps(bpy.types.Operator): + bl_idname = "tlm.load_lightmaps" + bl_label = "Load Lightmaps" + bl_description = "Load lightmaps from selected folder" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + utility.transfer_load() + + build.finish_assemble() + + return {'FINISHED'} + +class TLM_ToggleTexelDensity(bpy.types.Operator): + bl_idname = "tlm.toggle_texel_density" + bl_label = "Toggle Texel Density" + bl_description = "Toggle visualize lightmap texel density for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + uv_layers = obj.data.uv_layers + + #if the object has a td_vis in the uv maps, toggle off + #else toggle on + + if obj.TLM_ObjectProperties.tlm_use_default_channel: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_Lightmap': + uv_layers.active_index = i + break + else: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == obj.TLM_ObjectProperties.tlm_uv_channel: + uv_layers.active_index = i + break + + #filepath = r"C:\path\to\image.png" + + #img = bpy.data.images.load(filepath) + + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + space_data = area.spaces.active + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + new_window = context.window_manager.windows[-1] + + area = new_window.screen.areas[-1] + area.type = 'VIEW_3D' + #bg = space_data.background_images.new() + print(bpy.context.object) + bpy.ops.object.bake_td_uv_to_vc() + + #bg.image = img + break + + + #set active uv_layer to + + + print("TLM_Viz_Toggle") return {'FINISHED'} @@ -698,7 +1037,4 @@ def TLM_HalfResolution(): pass def TLM_DivideLMGroups(): - pass - -def TLM_LoadFromFolder(): pass \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/__init__.py b/blender/arm/lightmapper/panels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/panels/image.py b/blender/arm/lightmapper/panels/image.py new file mode 100644 index 0000000000..929907e2fc --- /dev/null +++ b/blender/arm/lightmapper/panels/image.py @@ -0,0 +1,66 @@ +import bpy, os, math, importlib + +from bpy.types import Menu, Operator, Panel, UIList + +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, +) + +class TLM_PT_Imagetools(bpy.types.Panel): + bl_label = "TLM Imagetools" + bl_space_type = "IMAGE_EDITOR" + bl_region_type = 'UI' + bl_category = "TLM Imagetools" + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.label(text ="Image Tools") + + def draw(self, context): + layout = self.layout + + activeImg = None + + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + activeImg = area.spaces.active.image + + if activeImg is not None and activeImg.name != "Render Result" and activeImg.name != "Viewer Node": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text ="OpenCV not installed.") + else: + + row = layout.row(align=True) + row.label(text ="Method") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_engine") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_cache_switch") + row = layout.row(align=True) + row.operator("tlm.image_upscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch up.") + row = layout.row(align=True) + row.operator("tlm.image_downscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch down.") + if activeImg.TLM_ImageProperties.tlm_image_scale_engine == "OpenCV": + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_method") + + else: + row = layout.row(align=True) + row.label(text ="Select an image") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/light.py b/blender/arm/lightmapper/panels/light.py new file mode 100644 index 0000000000..fd576af1ae --- /dev/null +++ b/blender/arm/lightmapper/panels/light.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_LightMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "light" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/object.py b/blender/arm/lightmapper/panels/object.py new file mode 100644 index 0000000000..7f0e7abea1 --- /dev/null +++ b/blender/arm/lightmapper/panels/object.py @@ -0,0 +1,118 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_ObjectMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + if obj.type == "MESH": + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + row = layout.row() + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + else: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") + row = layout.row() + + + if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_filter_override: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") + row = layout.row(align=True) + if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + else: + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + + +class TLM_PT_MaterialMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + mat = bpy.context.material + if mat == None: + return + + if obj.type == "MESH": + + row = layout.row() + row.prop(mat, "TLM_ignore") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/scene.py b/blender/arm/lightmapper/panels/scene.py new file mode 100644 index 0000000000..de36f22f0f --- /dev/null +++ b/blender/arm/lightmapper/panels/scene.py @@ -0,0 +1,584 @@ +import bpy, importlib, math +from bpy.props import * +from bpy.types import Menu, Panel +from .. utility import icon +from .. properties.denoiser import oidn, optix + +class TLM_PT_Settings(bpy.types.Panel): + bl_label = "Settings" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + + #We list LuxCoreRender as available, by default we assume Cycles exists + row.prop(sceneProperties, "tlm_lightmap_engine") + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + #CYCLES SETTINGS HERE + engineProperties = scene.TLM_EngineProperties + + row = layout.row(align=True) + row.label(text="General Settings") + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_apply_on_unwrap") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_headless") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_on_finish") + + if sceneProperties.tlm_alert_on_finish: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_sound") + + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_compile_statistics") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_bg_color") + if sceneProperties.tlm_override_bg_color: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_color") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_reset_uv") + + row = layout.row(align=True) + try: + if bpy.context.scene["TLM_Buildstat"] is not None: + row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0])) + except: + pass + + row = layout.row(align=True) + row.label(text="Cycles Settings") + + row = layout.row(align=True) + row.prop(engineProperties, "tlm_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_quality") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_resolution_scale") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_bake_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_target") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lighting_mode") + # if scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectao": + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_premultiply_ao") + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row = layout.row(align=True) + row.label(text="Warning! Background mode is currently unstable", icon_value=2) + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_render") + if sceneProperties.tlm_network_render: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_paths") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_network_dir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_caching_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_directional_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_dilation_margin") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_exposure_multiplier") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_setting_supersample") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_metallic_clamp") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_interpolation") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_extrapolation") + + + + # elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + # engineProperties = scene.TLM_Engine2Properties + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_luxcore_dir") + # row = layout.row(align=True) + # row.operator("tlm.build_lightmaps") + # #LUXCORE SETTINGS HERE + # #luxcore_available = False + + # #Look for Luxcorerender in the renderengine classes + # # for engine in bpy.types.RenderEngine.__subclasses__(): + # # if engine.bl_idname == "LUXCORE": + # # luxcore_available = True + # # break + + # # row = layout.row(align=True) + # # if not luxcore_available: + # # row.label(text="Please install BlendLuxCore.") + # # else: + # # row.label(text="LuxCoreRender not yet available.") + + elif sceneProperties.tlm_lightmap_engine == "OctaneRender": + + engineProperties = scene.TLM_Engine3Properties + + #LUXCORE SETTINGS HERE + octane_available = True + + + + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + +class TLM_PT_Denoise(bpy.types.Panel): + bl_label = "Denoise" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_denoise_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_denoise_use + + row = layout.row(align=True) + + #row.prop(sceneProperties, "tlm_denoiser", expand=True) + #row = layout.row(align=True) + row.prop(sceneProperties, "tlm_denoise_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_denoise_engine == "Integrated": + row.label(text="No options for Integrated.") + elif sceneProperties.tlm_denoise_engine == "OIDN": + denoiseProperties = scene.TLM_OIDNEngineProperties + row.prop(denoiseProperties, "tlm_oidn_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_threads") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_maxmem") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_affinity") + # row = layout.row(align=True) + # row.prop(denoiseProperties, "tlm_denoise_ao") + elif sceneProperties.tlm_denoise_engine == "Optix": + denoiseProperties = scene.TLM_OptixEngineProperties + row.prop(denoiseProperties, "tlm_optix_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_maxmem") + #row = layout.row(align=True) + #row.prop(denoiseProperties, "tlm_denoise_ao") + +class TLM_PT_Filtering(bpy.types.Panel): + bl_label = "Filtering" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_filtering_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_filtering_use + #row = layout.row(align=True) + #row.label(text="TODO MAKE CHECK") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_filtering_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_filtering_engine == "OpenCV": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Please install it as an administrator.") + row = layout.row(align=True) + row.operator("tlm.install_opencv_lightmaps") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") + row = layout.row(align=True) + if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") + + +class TLM_PT_Encoding(bpy.types.Panel): + bl_label = "Encoding" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_encoding_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_encoding_use + + sceneProperties = scene.TLM_SceneProperties + row = layout.row(align=True) + + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row.label(text="Encoding options disabled in background mode") + row = layout.row(align=True) + + else: + + row.prop(sceneProperties, "tlm_encoding_device", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_encoding_device == "CPU": + row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True) + else: + row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True) + + if sceneProperties.tlm_encoding_device == "CPU": + if sceneProperties.tlm_encoding_mode_a == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_encoding_mode_a == "RGBD": + pass + if sceneProperties.tlm_encoding_mode_a == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + else: + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + + if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_encoding_mode_b == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + +class TLM_PT_Utility(bpy.types.Panel): + bl_label = "Utilities" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + row.label(text="Enable Lightmaps for set") + row = layout.row(align=True) + row.operator("tlm.enable_set") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_utility_set") + row = layout.row(align=True) + #row.label(text="ABCD") + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row = layout.row() + row.prop(sceneProperties, "tlm_postpack_object") + row = layout.row() + + if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_weight") + + if sceneProperties.tlm_resolution_weight == "Single": + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + else: + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_min") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_max") + + row = layout.row() + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.operator("tlm.select_lightmapped_objects") + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) + + + row.label(text="Environment Probes") + row = layout.row() + row.operator("tlm.build_environmentprobe") + row = layout.row() + row.operator("tlm.clean_environmentprobe") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_engine") + row = layout.row() + row.prop(sceneProperties, "tlm_cmft_path") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_create_spherical") + + if sceneProperties.tlm_create_spherical: + + row = layout.row() + row.prop(sceneProperties, "tlm_invert_direction") + row = layout.row() + row.prop(sceneProperties, "tlm_write_sh") + row = layout.row() + row.prop(sceneProperties, "tlm_write_radiance") + + row = layout.row(align=True) + row.label(text="Load lightmaps") + row = layout.row() + row.prop(sceneProperties, "tlm_load_folder") + row = layout.row() + row.operator("tlm.load_lightmaps") + +class TLM_PT_Additional(bpy.types.Panel): + bl_label = "Additional" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + atlasListItem = scene.TLM_AtlasListItem + atlasList = scene.TLM_AtlasList + postatlasListItem = scene.TLM_PostAtlasListItem + postatlasList = scene.TLM_PostAtlasList + + layout.label(text="Network Rendering") + row = layout.row() + row.operator("tlm.start_server") + layout.label(text="Atlas Groups") + row = layout.row() + row.prop(sceneProperties, "tlm_atlas_mode", expand=True) + + if sceneProperties.tlm_atlas_mode == "Prepack": + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_atlaslist.new_item", icon='ADD', text="") + col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") + + if atlasListItem >= 0 and len(atlasList) > 0: + item = atlasList[atlasListItem] + layout.prop(item, "tlm_atlas_lightmap_unwrap_mode") + layout.prop(item, "tlm_atlas_lightmap_resolution") + layout.prop(item, "tlm_atlas_unwrap_margin") + + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: + amount = amount + 1 + + layout.label(text="Objects: " + str(amount)) + + else: + + layout.label(text="Postpacking is unstable.") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + + else: + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_postatlaslist.new_item", icon='ADD', text="") + col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="") + + if postatlasListItem >= 0 and len(postatlasList) > 0: + item = postatlasList[postatlasListItem] + layout.prop(item, "tlm_atlas_lightmap_resolution") + + #Below list object counter + amount = 0 + utilized = 0 + atlasUsedArea = 0 + atlasSize = item.tlm_atlas_lightmap_resolution + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: + amount = amount + 1 + + atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 + + row = layout.row() + row.prop(item, "tlm_atlas_repack_on_cleanup") + + #TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY! + cv2 = True + + if cv2: + row = layout.row() + row.prop(item, "tlm_atlas_dilation") + layout.label(text="Objects: " + str(amount)) + + utilized = atlasUsedArea / (int(atlasSize) ** 2) + layout.label(text="Utilized: " + str(utilized * 100) + "%") + + if (utilized * 100) > 100: + layout.label(text="Warning! Overflow not yet supported") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/world.py b/blender/arm/lightmapper/panels/world.py new file mode 100644 index 0000000000..b3c5d294ec --- /dev/null +++ b/blender/arm/lightmapper/panels/world.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_WorldMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/__init__.py b/blender/arm/lightmapper/preferences/__init__.py new file mode 100644 index 0000000000..94cdfaeaeb --- /dev/null +++ b/blender/arm/lightmapper/preferences/__init__.py @@ -0,0 +1,16 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import addon_preferences +#from . import build, clean, explore, encode, installopencv + +classes = [ + addon_preferences.TLM_AddonPreferences +] + +def register(): + for cls in classes: + register_class(cls) + +def unregister(): + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/addon_preferences.py b/blender/arm/lightmapper/preferences/addon_preferences.py new file mode 100644 index 0000000000..173ac71ab6 --- /dev/null +++ b/blender/arm/lightmapper/preferences/addon_preferences.py @@ -0,0 +1,75 @@ +import bpy, platform +from os.path import basename, dirname +from bpy.types import AddonPreferences +from .. operators import installopencv +import importlib + +class TLM_AddonPreferences(AddonPreferences): + + bl_idname = "thelightmapper" + + def draw(self, context): + + layout = self.layout + + box = layout.box() + row = box.row() + row.label(text="OpenCV") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is not None: + row.label(text="OpenCV installed") + else: + if platform.system() == "Windows": + row.label(text="OpenCV not found - Install as administrator!", icon_value=2) + else: + row.label(text="OpenCV not found - Click to install!", icon_value=2) + row = box.row() + row.operator("tlm.install_opencv_lightmaps", icon="PREFERENCES") + + box = layout.box() + row = box.row() + row.label(text="Blender Xatlas") + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + row.label(text="Blender Xatlas installed and available") + else: + row.label(text="Blender Xatlas not installed", icon_value=2) + row = box.row() + row.label(text="Github: https://github.com/mattedicksoncom/blender-xatlas") + + box = layout.box() + row = box.row() + row.label(text="RizomUV Bridge") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="UVPackmaster") + row.label(text="Coming soon") + + texel_density_addon = False + for addon in bpy.context.preferences.addons.keys(): + if addon.startswith("Texel_Density"): + texel_density_addon = True + + box = layout.box() + row = box.row() + row.label(text="Texel Density Checker") + if texel_density_addon: + row.label(text="Texel Density Checker installed and available") + else: + row.label(text="Texel Density Checker", icon_value=2) + row.label(text="Coming soon") + row = box.row() + row.label(text="Github: https://github.com/mrven/Blender-Texel-Density-Checker") + + box = layout.box() + row = box.row() + row.label(text="LuxCoreRender") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="OctaneRender") + row.label(text="Coming soon") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/__init__.py b/blender/arm/lightmapper/properties/__init__.py index 052a173b15..0a011de90c 100644 --- a/blender/arm/lightmapper/properties/__init__.py +++ b/blender/arm/lightmapper/properties/__init__.py @@ -1,7 +1,7 @@ import bpy from bpy.utils import register_class, unregister_class -from . import scene, object, atlas -from . renderer import cycles, luxcorerender +from . import scene, object, atlas, image +from . renderer import cycles, luxcorerender, octanerender from . denoiser import oidn, optix classes = [ @@ -9,12 +9,14 @@ object.TLM_ObjectProperties, cycles.TLM_CyclesSceneProperties, luxcorerender.TLM_LuxCoreSceneProperties, + octanerender.TLM_OctanerenderSceneProperties, oidn.TLM_OIDNEngineProperties, optix.TLM_OptixEngineProperties, atlas.TLM_AtlasListItem, atlas.TLM_UL_AtlasList, atlas.TLM_PostAtlasListItem, - atlas.TLM_UL_PostAtlasList + atlas.TLM_UL_PostAtlasList, + image.TLM_ImageProperties ] def register(): @@ -25,12 +27,14 @@ def register(): bpy.types.Object.TLM_ObjectProperties = bpy.props.PointerProperty(type=object.TLM_ObjectProperties) bpy.types.Scene.TLM_EngineProperties = bpy.props.PointerProperty(type=cycles.TLM_CyclesSceneProperties) bpy.types.Scene.TLM_Engine2Properties = bpy.props.PointerProperty(type=luxcorerender.TLM_LuxCoreSceneProperties) + bpy.types.Scene.TLM_Engine3Properties = bpy.props.PointerProperty(type=octanerender.TLM_OctanerenderSceneProperties) bpy.types.Scene.TLM_OIDNEngineProperties = bpy.props.PointerProperty(type=oidn.TLM_OIDNEngineProperties) bpy.types.Scene.TLM_OptixEngineProperties = bpy.props.PointerProperty(type=optix.TLM_OptixEngineProperties) bpy.types.Scene.TLM_AtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) bpy.types.Scene.TLM_AtlasList = bpy.props.CollectionProperty(type=atlas.TLM_AtlasListItem) bpy.types.Scene.TLM_PostAtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) bpy.types.Scene.TLM_PostAtlasList = bpy.props.CollectionProperty(type=atlas.TLM_PostAtlasListItem) + bpy.types.Image.TLM_ImageProperties = bpy.props.PointerProperty(type=image.TLM_ImageProperties) bpy.types.Material.TLM_ignore = bpy.props.BoolProperty(name="Skip material", description="Ignore material for lightmapped object", default=False) @@ -42,9 +46,11 @@ def unregister(): del bpy.types.Object.TLM_ObjectProperties del bpy.types.Scene.TLM_EngineProperties del bpy.types.Scene.TLM_Engine2Properties + del bpy.types.Scene.TLM_Engine3Properties del bpy.types.Scene.TLM_OIDNEngineProperties del bpy.types.Scene.TLM_OptixEngineProperties del bpy.types.Scene.TLM_AtlasListItem del bpy.types.Scene.TLM_AtlasList del bpy.types.Scene.TLM_PostAtlasListItem - del bpy.types.Scene.TLM_PostAtlasList \ No newline at end of file + del bpy.types.Scene.TLM_PostAtlasList + del bpy.types.Image.TLM_ImageProperties \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/atlas.py b/blender/arm/lightmapper/properties/atlas.py index baccd9262b..8154f72c9a 100644 --- a/blender/arm/lightmapper/properties/atlas.py +++ b/blender/arm/lightmapper/properties/atlas.py @@ -34,12 +34,16 @@ class TLM_PostAtlasListItem(bpy.types.PropertyGroup): max=1.0, subtype='FACTOR') - tlm_atlas_lightmap_unwrap_mode : EnumProperty( - items = [('Lightmap', 'Lightmap', 'TODO'), - ('SmartProject', 'Smart Project', 'TODO'), - ('Xatlas', 'Xatlas', 'TODO')], + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + tlm_postatlas_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, name = "Unwrap Mode", - description="TODO", + description="Atlas unwrapping method", default='SmartProject') class TLM_UL_PostAtlasList(bpy.types.UIList): @@ -51,7 +55,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn #In list object counter amount = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: @@ -69,9 +73,6 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) - - - class TLM_AtlasListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") tlm_atlas_lightmap_resolution : EnumProperty( @@ -95,12 +96,17 @@ class TLM_AtlasListItem(bpy.types.PropertyGroup): max=1.0, subtype='FACTOR') + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + tlm_atlas_lightmap_unwrap_mode : EnumProperty( - items = [('Lightmap', 'Lightmap', 'TODO'), - ('SmartProject', 'Smart Project', 'TODO'), - ('Xatlas', 'Xatlas', 'TODO')], + items = unwrap_modes, name = "Unwrap Mode", - description="TODO", + description="Atlas unwrapping method", default='SmartProject') class TLM_UL_AtlasList(bpy.types.UIList): @@ -111,7 +117,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn amount = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: diff --git a/blender/arm/lightmapper/properties/image.py b/blender/arm/lightmapper/properties/image.py index e6e75766a3..a81169c404 100644 --- a/blender/arm/lightmapper/properties/image.py +++ b/blender/arm/lightmapper/properties/image.py @@ -1,10 +1,26 @@ import bpy from bpy.props import * -class TLM_ObjectProperties(bpy.types.PropertyGroup): - tlm_image_scale_method : EnumProperty( - items = [('Native', 'Native', 'TODO'), - ('OpenCV', 'OpenCV', 'TODO')], +class TLM_ImageProperties(bpy.types.PropertyGroup): + tlm_image_scale_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'TODO')], name = "Scaling engine", description="TODO", - default='Native') \ No newline at end of file + default='OpenCV') + + #('Native', 'Native', 'TODO'), + + tlm_image_scale_method : EnumProperty( + items = [('Nearest', 'Nearest', 'TODO'), + ('Area', 'Area', 'TODO'), + ('Linear', 'Linear', 'TODO'), + ('Cubic', 'Cubic', 'TODO'), + ('Lanczos', 'Lanczos', 'TODO')], + name = "Scaling method", + description="TODO", + default='Lanczos') + + tlm_image_cache_switch : BoolProperty( + name="Cache for quickswitch", + description="Caches scaled images for quick switching", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/object.py b/blender/arm/lightmapper/properties/object.py index 7738add9af..c5c2fe4719 100644 --- a/blender/arm/lightmapper/properties/object.py +++ b/blender/arm/lightmapper/properties/object.py @@ -7,7 +7,7 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): tlm_atlas_pointer : StringProperty( name = "Atlas Group", - description = "Atlas Lightmap Group", + description = "", default = "") tlm_postatlas_pointer : StringProperty( @@ -51,8 +51,7 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'), ('SmartProject', 'Smart Project', 'TODO'), - ('CopyExisting', 'Copy Existing', 'TODO'), - ('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO')] + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.')] tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB name="Postpack object", @@ -145,4 +144,14 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): name="Median kernel", default=3, min=1, - max=5) \ No newline at end of file + max=5) + + tlm_use_default_channel : BoolProperty( + name="Use default UV channel", + description="Will either use or create the default UV Channel 'UVMap_Lightmap' upon build.", + default=True) + + tlm_uv_channel : StringProperty( + name = "UV Channel", + description = "Use any custom UV Channel for the lightmap", + default = "UVMap") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/cycles.py b/blender/arm/lightmapper/properties/renderer/cycles.py index ad99a664fb..d3763400bc 100644 --- a/blender/arm/lightmapper/properties/renderer/cycles.py +++ b/blender/arm/lightmapper/properties/renderer/cycles.py @@ -21,6 +21,16 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): description="Select baking quality", default="0") + targets = [('texture', 'Image texture', 'Build to image texture')] + if (2, 92, 0) >= bpy.app.version: + targets.append(('vertex', 'Vertex colors', 'Build to vertex colors')) + + tlm_target : EnumProperty( + items = targets, + name = "Build Target", + description="Select target to build to", + default="texture") + tlm_resolution_scale : EnumProperty( items = [('1', '1/1', '1'), ('2', '1/2', '2'), @@ -45,10 +55,12 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): description="Select bake mode", default="Foreground") + caching_modes = [('Copy', 'Copy', 'More overhead; allows for network.')] + + #caching_modes.append(('Cache', 'Cache', 'Cache in separate blend'),('Node', 'Node restore', 'EXPERIMENTAL! Use with care')) + tlm_caching_mode : EnumProperty( - items = [('Copy', 'Copy', 'More overhead; allows for network.'), - ('Cache', 'Cache', 'Cache in separate blend'), - ('Node', 'Node restore', 'EXPERIMENTAL! Use with care')], + items = caching_modes, name = "Caching mode", description="Select cache mode", default="Copy") @@ -88,8 +100,16 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): tlm_lighting_mode : EnumProperty( items = [('combined', 'Combined', 'Bake combined lighting'), + ('combinedao', 'Combined+AO', 'Bake combined lighting with Ambient Occlusion'), ('indirect', 'Indirect', 'Bake indirect lighting'), - ('ao', 'AO', 'Bake only Ambient Occlusion')], +# ('indirectao', 'Indirect+AO', 'Bake indirect lighting with Ambient Occlusion'), + ('ao', 'AO', 'Bake only Ambient Occlusion'), + ('complete', 'Complete', 'Bake complete map')], name = "Lighting mode", description="TODO.", - default="combined") \ No newline at end of file + default="combined") + + tlm_premultiply_ao : BoolProperty( + name="Premultiply AO", + description="Ambient Occlusion will be premultiplied together with lightmaps, requiring less textures.", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/octanerender.py b/blender/arm/lightmapper/properties/renderer/octanerender.py index e69de29bb2..8c66cf13a6 100644 --- a/blender/arm/lightmapper/properties/renderer/octanerender.py +++ b/blender/arm/lightmapper/properties/renderer/octanerender.py @@ -0,0 +1,10 @@ +import bpy +from bpy.props import * + +class TLM_OctanerenderSceneProperties(bpy.types.PropertyGroup): + + tlm_lightmap_savedir : StringProperty( + name="Lightmap Directory", + description="TODO", + default="Lightmaps", + subtype="FILE_PATH") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/scene.py b/blender/arm/lightmapper/properties/scene.py index 6652b548a0..36c74fbc41 100644 --- a/blender/arm/lightmapper/properties/scene.py +++ b/blender/arm/lightmapper/properties/scene.py @@ -1,11 +1,19 @@ -import bpy +import bpy, os from bpy.props import * +from .. utility import utility + +def transfer_load(): + load_folder = bpy.context.scene.TLM_SceneProperties.tlm_load_folder + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + #transfer_assets(True, load_folder, lightmap_folder) class TLM_SceneProperties(bpy.types.PropertyGroup): engines = [('Cycles', 'Cycles', 'Use Cycles for lightmapping')] - engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) + #engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) #engines.append(('OctaneRender', 'Octane Render', 'Use Octane Render for lightmapping')) tlm_atlas_pointer : StringProperty( @@ -112,7 +120,7 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): #FILTERING SETTINGS GROUP tlm_filtering_use : BoolProperty( - name="Enable Filtering", + name="Enable denoising", description="Enable denoising for lightmaps", default=False) @@ -182,6 +190,17 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): min=1, max=5) + tlm_clamp_hdr : BoolProperty( + name="Enable HDR Clamp", + description="Clamp HDR Value", + default=False) + + tlm_clamp_hdr_value : IntProperty( + name="HDR Clamp value", + default=10, + min=0, + max=20) + #Encoding properties tlm_encoding_use : BoolProperty( name="Enable encoding", @@ -197,12 +216,13 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): encoding_modes_1 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), ('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), - ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')] + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] - encoding_modes_2 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), - ('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), + encoding_modes_2 = [('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), ('LogLuv', 'LogLuv', '8-bit HDR encoding. Different.'), - ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')] + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] tlm_encoding_mode_a : EnumProperty( items = encoding_modes_1, @@ -275,8 +295,7 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_mesh_lightmap_unwrap_mode : EnumProperty( items = [('Lightmap', 'Lightmap', 'TODO'), ('SmartProject', 'Smart Project', 'TODO'), - ('CopyExisting', 'Copy Existing', 'TODO'), - ('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO'), + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.'), ('Xatlas', 'Xatlas', 'TODO')], name = "Unwrap Mode", description="TODO", @@ -317,12 +336,30 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_metallic_clamp : EnumProperty( items = [('ignore', 'Ignore', 'Ignore clamping'), + ('skip', 'Skip', 'Skip baking metallic materials'), ('zero', 'Zero', 'Set zero'), ('limit', 'Limit', 'Clamp to 0.9')], name = "Metallic clamping", description="TODO.", default="ignore") + tlm_texture_interpolation : EnumProperty( + items = [('Smart', 'Smart', 'Bicubic when magnifying.'), + ('Cubic', 'Cubic', 'Cubic interpolation'), + ('Closest', 'Closest', 'No interpolation'), + ('Linear', 'Linear', 'Linear')], + name = "Texture interpolation", + description="Texture interpolation.", + default="Linear") + + tlm_texture_extrapolation : EnumProperty( + items = [('REPEAT', 'Repeat', 'Repeat in both direction.'), + ('EXTEND', 'Extend', 'Extend by repeating edge pixels.'), + ('CLIP', 'Clip', 'Clip to image size')], + name = "Texture extrapolation", + description="Texture extrapolation.", + default="EXTEND") + tlm_verbose : BoolProperty( name="Verbose", description="Verbose console output", @@ -409,4 +446,52 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): ('CYCLES', 'Cycles', 'TODO')], name = "Probe Render Engine", description="TODO", - default='BLENDER_EEVEE') \ No newline at end of file + default='BLENDER_EEVEE') + + tlm_load_folder : StringProperty( + name="Load Folder", + description="Load existing lightmaps from folder", + subtype="DIR_PATH") + + tlm_utility_set : EnumProperty( + items = [('Scene', 'Scene', 'Set for all objects in the scene.'), + ('Selection', 'Selection', 'Set for selected objects.'), + ('Enabled', 'Enabled', 'Set for objects that has been enabled for lightmapping.')], + name = "Set", + description="Utility selection set", + default='Scene') + + tlm_resolution_weight : EnumProperty( + items = [('Single', 'Single', 'Set a single resolution for all objects.'), + ('Dimension', 'Dimension', 'Distribute resolutions based on object dimensions.'), + ('Surface', 'Surface', 'Distribute resolutions based on mesh surface area.'), + ('Volume', 'Volume', 'Distribute resolutions based on mesh volume.')], + name = "Resolution weight", + description="Method for setting resolution value", + default='Single') + #Todo add vertex color option + + tlm_resolution_min : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Minimum resolution", + description="Minimum distributed resolution", + default='32') + + tlm_resolution_max : EnumProperty( + items = [('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Maximum resolution", + description="Maximum distributed resolution", + default='256') \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/build.py b/blender/arm/lightmapper/utility/build.py index a8ad80ddf4..891ebcf4d4 100644 --- a/blender/arm/lightmapper/utility/build.py +++ b/blender/arm/lightmapper/utility/build.py @@ -1,17 +1,20 @@ import bpy, os, subprocess, sys, platform, aud, json, datetime, socket -import threading + from . import encoding, pack from . cycles import lightmap, prepare, nodes, cache +from . luxcore import setup +from . octane import configure, lightmap2 from . denoiser import integrated, oidn, optix from . filtering import opencv +from . gui import Viewport from .. network import client + from os import listdir from os.path import isfile, join from time import time, sleep from importlib import util previous_settings = {} - postprocess_shutdown = False def prepare_build(self=0, background_mode=False, shutdown_after_build=False): @@ -21,15 +24,31 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): print("Building lightmaps") + if bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + scene = bpy.context.scene + + if not "tlm_plus_mode" in bpy.app.driver_namespace or bpy.app.driver_namespace["tlm_plus_mode"] == 0: + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + bpy.app.driver_namespace["tlm_plus_mode"] = 1 + print("Plus Mode") + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Foreground" or background_mode==True: global start_time start_time = time() + bpy.app.driver_namespace["tlm_start_time"] = time() scene = bpy.context.scene sceneProperties = scene.TLM_SceneProperties - #Timer start here bound to global + if not background_mode and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(1) if check_save(): print("Please save your file first") @@ -52,12 +71,6 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): self.report({'INFO'}, "Error:Filtering - OpenCV not installed") return{'FINISHED'} - #TODO DO some resolution change - #if checkAtlasSize(): - # print("Error: AtlasGroup overflow") - # self.report({'INFO'}, "Error: AtlasGroup overflow - Too many objects") - # return{'FINISHED'} - setMode() dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) @@ -67,19 +80,6 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): #Naming check naming_check() - # if sceneProperties.tlm_reset_uv or sceneProperties.tlm_atlas_mode == "Postpack": - # for obj in bpy.data.objects: - # if obj.type == "MESH": - # uv_layers = obj.data.uv_layers - - - - #for uvlayer in uv_layers: - # if uvlayer.name == "UVMap_Lightmap": - # uv_layers.remove(uvlayer) - - ## RENDER DEPENDENCY FROM HERE - if sceneProperties.tlm_lightmap_engine == "Cycles": prepare.init(self, previous_settings) @@ -90,18 +90,14 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): if sceneProperties.tlm_lightmap_engine == "OctaneRender": - pass - - #Renderer - Store settings - - #Renderer - Set settings - - #Renderer - Config objects, lights, world + configure.init(self, previous_settings) begin_build() else: + print("Baking in background") + filepath = bpy.data.filepath bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) @@ -111,22 +107,7 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): scene = bpy.context.scene sceneProperties = scene.TLM_SceneProperties - #We dynamically load the renderer and denoiser, instead of loading something we don't use - - if sceneProperties.tlm_lightmap_engine == "Cycles": - - pass - - if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - - pass - - if sceneProperties.tlm_lightmap_engine == "OctaneRender": - - pass - #Timer start here bound to global - if check_save(): print("Please save your file first") self.report({'INFO'}, "Please save your file first") @@ -168,23 +149,12 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): client.connect_client(HOST, PORT, bpy.data.filepath, 0) - # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - # s.connect((HOST, PORT)) - # message = { - # "call" : 1, - # "command" : 1, - # "enquiry" : 0, - # "args" : bpy.data.filepath - # } - - # s.sendall(json.dumps(message).encode()) - # data = s.recv(1024) - # print(data.decode()) - finish_assemble() else: + print("Background driver process") + bpy.app.driver_namespace["alpha"] = 0 bpy.app.driver_namespace["tlm_process"] = False @@ -196,6 +166,8 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): def distribute_building(): + print("Distributing lightmap building") + #CHECK IF THERE'S AN EXISTING SUBPROCESS if not os.path.isfile(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir, "process.tlm")): @@ -215,8 +187,16 @@ def distribute_building(): with open(os.path.join(write_directory, "process.tlm"), 'w') as file: json.dump(process_status, file, indent=2) - bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) - + if (2, 91, 0) > bpy.app.version: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Started process: " + str(bpy.app.driver_namespace["tlm_process"]) + " at " + str(datetime.datetime.now())) @@ -269,6 +249,10 @@ def finish_assemble(self=0): if sceneProperties.tlm_lightmap_engine == "OctaneRender": pass + if not 'start_time' in globals(): + global start_time + start_time = time() + manage_build(True) def begin_build(): @@ -288,7 +272,8 @@ def begin_build(): pass if sceneProperties.tlm_lightmap_engine == "OctaneRender": - pass + + lightmap2.bake() #Denoiser if sceneProperties.tlm_denoise_use: @@ -429,7 +414,7 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBMCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) - if sceneProperties.tlm_encoding_mode_b == "RGBD": + if sceneProperties.tlm_encoding_mode_a == "RGBD": if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("ENCODING RGBD") @@ -455,6 +440,36 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBDCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + if sceneProperties.tlm_encoding_mode_a == "SDR": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + else: if sceneProperties.tlm_encoding_mode_b == "HDR": @@ -562,6 +577,33 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBDGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + if sceneProperties.tlm_encoding_mode_b == "PNG": + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + manage_build() def manage_build(background_pass=False): @@ -610,6 +652,10 @@ def manage_build(background_pass=False): formatEnc = "_encoded.png" + if sceneProperties.tlm_encoding_mode_a == "SDR": + + formatEnc = ".png" + else: print("GPU Encoding") @@ -632,6 +678,10 @@ def manage_build(background_pass=False): formatEnc = "_encoded.png" + if sceneProperties.tlm_encoding_mode_b == "SDR": + + formatEnc = ".png" + if not background_pass: nodes.exchangeLightmapsToPostfix("_baked", end, formatEnc) @@ -653,13 +703,13 @@ def manage_build(background_pass=False): filepath = bpy.data.filepath dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -672,77 +722,181 @@ def manage_build(background_pass=False): if "_Original" in mat.name: bpy.data.materials.remove(mat) - for obj in bpy.data.objects: - - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - img_name = obj.name + '_baked' - Lightmapimage = bpy.data.images[img_name] - obj["Lightmap"] = Lightmapimage.filepath_raw + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + atlasName = obj.TLM_ObjectProperties.tlm_atlas_pointer + img_name = atlasName + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + elif obj.TLM_ObjectProperties.tlm_postpack_object: + atlasName = obj.TLM_ObjectProperties.tlm_postatlas_pointer + img_name = atlasName + '_baked' + ".hdr" + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + else: + img_name = obj.name + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw for image in bpy.data.images: if image.name.endswith("_baked"): bpy.data.images.remove(image, do_unlink=True) - total_time = sec_to_hours((time() - start_time)) - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print(total_time) + if "tlm_plus_mode" in bpy.app.driver_namespace: #First DIR pass - bpy.context.scene["TLM_Buildstat"] = total_time + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: #First DIR pass - reset_settings(previous_settings["settings"]) + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) - if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) - pass + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) - if sceneProperties.tlm_lightmap_engine == "OctaneRender": + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) - pass + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) - if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": - pass + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = extension = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_dir" + extension)) + + print("First DIR pass complete") + + bpy.app.driver_namespace["tlm_plus_mode"] = 2 + + prepare_build(self=0, background_mode=False, shutdown_after_build=False) + + if not background_pass and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + + filepath = bpy.data.filepath + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + if not filename.endswith("_dir"): + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_ao" + extension)) + + print("Second AO pass complete") + + total_time = sec_to_hours((time() - start_time)) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(total_time) - if scene.TLM_SceneProperties.tlm_alert_on_finish: + bpy.context.scene["TLM_Buildstat"] = total_time - alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + reset_settings(previous_settings["settings"]) + + bpy.app.driver_namespace["tlm_plus_mode"] = 0 + + if not background_pass: + + #TODO CHANGE! + + nodes.exchangeLightmapsToPostfix(end, end + "_dir", formatEnc) + + nodes.applyAOPass() - if alertSelect == "dash": - soundfile = "dash.ogg" - elif alertSelect == "pingping": - soundfile = "pingping.ogg" - elif alertSelect == "gentle": - soundfile = "gentle.ogg" else: - soundfile = "noot.ogg" - scriptDir = os.path.dirname(os.path.realpath(__file__)) - sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) + total_time = sec_to_hours((time() - start_time)) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(total_time) - device = aud.Device() - sound = aud.Sound.file(sound_path) - device.play(sound) + bpy.context.scene["TLM_Buildstat"] = total_time - print("Lightmap building finished") + reset_settings(previous_settings["settings"]) - if bpy.app.background: + print("Lightmap building finished") - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Writing background process report") - - write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - if os.path.exists(os.path.join(write_directory, "process.tlm")): + pass - process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": + pass + + if not background_pass and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + if scene.TLM_SceneProperties.tlm_alert_on_finish: + + alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + + if alertSelect == "dash": + soundfile = "dash.ogg" + elif alertSelect == "pingping": + soundfile = "pingping.ogg" + elif alertSelect == "gentle": + soundfile = "gentle.ogg" + else: + soundfile = "noot.ogg" + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) + + device = aud.Device() + sound = aud.Sound.file(sound_path) + device.play(sound) - process_status[1]["completed"] = True + if bpy.app.background: - with open(os.path.join(write_directory, "process.tlm"), 'w') as file: - json.dump(process_status, file, indent=2) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Writing background process report") + + write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + if os.path.exists(os.path.join(write_directory, "process.tlm")): + + process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + + process_status[1]["completed"] = True - if postprocess_shutdown: - sys.exit() + with open(os.path.join(write_directory, "process.tlm"), 'w') as file: + json.dump(process_status, file, indent=2) + + if postprocess_shutdown: + sys.exit() #TODO - SET BELOW TO UTILITY @@ -770,9 +924,8 @@ def reset_settings(prev_settings): def naming_check(): - for obj in bpy.data.objects: - - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: @@ -829,6 +982,9 @@ def check_save(): def check_denoiser(): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Checking denoiser path") + scene = bpy.context.scene if scene.TLM_SceneProperties.tlm_denoise_use: @@ -847,8 +1003,8 @@ def check_denoiser(): return 0 def check_materials(): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -868,15 +1024,39 @@ def check_materials(): def sec_to_hours(seconds): a=str(seconds//3600) b=str((seconds%3600)//60) - c=str((seconds%3600)%60) + c=str(round((seconds%3600)%60,1)) d=["{} hours {} mins {} seconds".format(a, b, c)] return d def setMode(): + + obj = bpy.context.scene.objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.mode_set(mode='OBJECT') #TODO Make some checks that returns to previous selection +def setGui(mode): + + if mode == 0: + + context = bpy.context + driver = bpy.app.driver_namespace + + if "TLM_UI" in driver: + driver["TLM_UI"].remove_handle() + + if mode == 1: + + #bpy.context.area.tag_redraw() + context = bpy.context + driver = bpy.app.driver_namespace + driver["TLM_UI"] = Viewport.ViewportDraw(context, "Building Lightmaps") + + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + def checkAtlasSize(): overflow = False @@ -897,7 +1077,7 @@ def checkAtlasSize(): utilized = 0 atlasUsedArea = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -912,4 +1092,5 @@ def checkAtlasSize(): if overflow == True: return True else: - return False \ No newline at end of file + return False + diff --git a/blender/arm/lightmapper/utility/cycles/cache.py b/blender/arm/lightmapper/utility/cycles/cache.py index 50542c6ed6..07f068b715 100644 --- a/blender/arm/lightmapper/utility/cycles/cache.py +++ b/blender/arm/lightmapper/utility/cycles/cache.py @@ -2,7 +2,6 @@ #Todo - Check if already exists, in case multiple objects has the same material - def backup_material_copy(slot): material = slot.material dup = material.copy() @@ -16,25 +15,49 @@ def backup_material_cache_restore(slot, path): if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Restore cache") -def backup_material_rename(obj): - if "TLM_PrevMatArray" in obj: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Renaming material for: " + obj.name) +# def backup_material_restore(obj): #?? +# if bpy.context.scene.TLM_SceneProperties.tlm_verbose: +# print("Restoring material for: " + obj.name) - for slot in obj.material_slots: +#Check if object has TLM_PrevMatArray +# if yes +# - check if array.len is bigger than 0: +# if yes: +# for slot in object: +# originalMaterial = TLM_PrevMatArray[index] +# +# +# if no: +# - In which cases are these? - if slot.material is not None: - if slot.material.name.endswith("_Original"): - newname = slot.material.name[1:-9] - if newname in bpy.data.materials: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Removing material: " + bpy.data.materials[newname].name) - bpy.data.materials.remove(bpy.data.materials[newname]) - slot.material.name = newname +# if no: +# - In which cases are there not? +# - If a lightmapped material was applied to a non-lightmap object? - del obj["TLM_PrevMatArray"] -def backup_material_restore(obj): + # if bpy.data.materials[originalMaterial].users > 0: #TODO - Check if all lightmapped + + # print("Material has multiple users") + + # if originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + # elif "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + + # else: + + # print("Material has one user") + + # if "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + # elif originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + +def backup_material_restore(obj): #?? if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Restoring material for: " + obj.name) @@ -59,9 +82,43 @@ def backup_material_restore(obj): originalMaterial = "" if slot.material is not None: - #slot.material.user_clear() Seems to be bad; See: https://developer.blender.org/T49837 - bpy.data.materials.remove(slot.material) + #if slot.material.users < 2: + #slot.material.user_clear() #Seems to be bad; See: https://developer.blender.org/T49837 + #bpy.data.materials.remove(slot.material) if "." + originalMaterial + "_Original" in bpy.data.materials: slot.material = bpy.data.materials["." + originalMaterial + "_Original"] - slot.material.use_fake_user = False \ No newline at end of file + slot.material.use_fake_user = False + + else: + + print("No previous material for " + obj.name) + + else: + + print("No previous material for " + obj.name) + +def backup_material_rename(obj): #?? + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Renaming material for: " + obj.name) + + + if "TLM_PrevMatArray" in obj: + + for slot in obj.material_slots: + + if slot.material is not None: + if slot.material.name.endswith("_Original"): + newname = slot.material.name[1:-9] + if newname in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Removing material: " + bpy.data.materials[newname].name) + #if bpy.data.materials[newname].users < 2: + #bpy.data.materials.remove(bpy.data.materials[newname]) #TODO - Maybe remove this + slot.material.name = newname + + del obj["TLM_PrevMatArray"] + + else: + + print("No Previous material array for: " + obj.name) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/lightmap.py b/blender/arm/lightmapper/utility/cycles/lightmap.py index 8588bee808..0d0c56e1ed 100644 --- a/blender/arm/lightmapper/utility/cycles/lightmap.py +++ b/blender/arm/lightmapper/utility/cycles/lightmap.py @@ -1,25 +1,26 @@ import bpy, os +from .. import build +from time import time, sleep -def bake(): +def bake(plus_pass=0): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: bpy.ops.object.select_all(action='DESELECT') obj.select_set(False) iterNum = 0 currentIterNum = 0 - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: iterNum = iterNum + 1 if iterNum > 1: iterNum = iterNum - 1 - for obj in bpy.data.objects: - if obj.type == 'MESH': - + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: scene = bpy.context.scene @@ -32,19 +33,45 @@ def bake(): obj.hide_render = False scene.render.bake.use_clear = False - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + #os.system("cls") + + #if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + #elapsed = build.sec_to_hours((time() - bpy.app.driver_namespace["tlm_start_time"])) + #print("Baked: " + str(currentIterNum) + " | Left: " + str(iterNum-currentIterNum)) + elapsedSeconds = time() - bpy.app.driver_namespace["tlm_start_time"] + bakedObjects = currentIterNum + bakedLeft = iterNum-currentIterNum + if bakedObjects == 0: + bakedObjects = 1 + averagePrBake = elapsedSeconds / bakedObjects + remaining = averagePrBake * bakedLeft + #print(time() - bpy.app.driver_namespace["tlm_start_time"]) + print("Elapsed time: " + str(round(elapsedSeconds, 2)) + "s | ETA remaining: " + str(round(remaining, 2)) + "s") #str(elapsed[0]) + #print("Averaged: " + str(averagePrBake)) + #print("Remaining: " + str(remaining)) + + if scene.TLM_EngineProperties.tlm_target == "vertex": + scene.render.bake_target = "VERTEX_COLORS" - if scene.TLM_EngineProperties.tlm_lighting_mode == "combined" or scene.TLM_EngineProperties.tlm_lighting_mode == "combinedAO": + if scene.TLM_EngineProperties.tlm_lighting_mode == "combined": bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) - elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO": + elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect": bpy.ops.object.bake(type="DIFFUSE", pass_filter={"INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) elif scene.TLM_EngineProperties.tlm_lighting_mode == "ao": bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "complete": - bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) else: bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + bpy.ops.object.select_all(action='DESELECT') currentIterNum = currentIterNum + 1 diff --git a/blender/arm/lightmapper/utility/cycles/nodes.py b/blender/arm/lightmapper/utility/cycles/nodes.py index bb42a895ab..9eb24d88b3 100644 --- a/blender/arm/lightmapper/utility/cycles/nodes.py +++ b/blender/arm/lightmapper/utility/cycles/nodes.py @@ -1,8 +1,8 @@ import bpy, os def apply_lightmaps(): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -32,8 +32,12 @@ def apply_lightmaps(): def apply_materials(): - for obj in bpy.data.objects: - if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying materials") + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: uv_layers = obj.data.uv_layers @@ -90,7 +94,7 @@ def apply_materials(): for node in nodes: if node.name == "Baked Image": lightmapNode = node - lightmapNode.location = -800, 300 + lightmapNode.location = -1200, 300 lightmapNode.name = "TLM_Lightmap" foundBakedNode = True @@ -98,9 +102,10 @@ def apply_materials(): if not foundBakedNode: lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage") - lightmapNode.location = -300, 300 + lightmapNode.location = -1200, 300 lightmapNode.name = "TLM_Lightmap" - lightmapNode.interpolation = "Smart" + lightmapNode.interpolation = bpy.context.scene.TLM_SceneProperties.tlm_texture_interpolation + lightmapNode.extension = bpy.context.scene.TLM_SceneProperties.tlm_texture_extrapolation if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""): lightmapNode.image = bpy.data.images[obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"] @@ -118,37 +123,32 @@ def apply_materials(): #Find mainnode mainNode = outputNode.inputs[0].links[0].from_node - #Clamp metallic - - # if scene.TLM_SceneProperties.tlm_metallic_clamp != "ignore": - # if mainNode.type == "BSDF_PRINCIPLED": - - # if len(mainNode.inputs[4].links) == 0: - - # if scene.TLM_SceneProperties.tlm_metallic_clamp == "zero": - # mainNode.inputs[4].default_value = 0.0 - # else: - # mainNode.inputs[4].default_value = 0.99 - - # else: - - # pass - #Add all nodes first #Add lightmap multipliction texture mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB") mixNode.name = "Lightmap_Multiplication" - mixNode.location = -300, 300 + mixNode.location = -800, 300 if scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO": mixNode.blend_type = 'ADD' else: mixNode.blend_type = 'MULTIPLY' - mixNode.inputs[0].default_value = 1.0 + + if scene.TLM_EngineProperties.tlm_lighting_mode == "complete": + mixNode.inputs[0].default_value = 0.0 + else: + mixNode.inputs[0].default_value = 1.0 UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap") - UVLightmap.uv_map = "UVMap_Lightmap" + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + UVLightmap.uv_map = uv_channel + UVLightmap.name = "Lightmap_UV" - UVLightmap.location = -1000, 300 + UVLightmap.location = -1500, 300 if(scene.TLM_SceneProperties.tlm_decoder_setup): if scene.TLM_SceneProperties.tlm_encoding_device == "CPU": @@ -196,7 +196,7 @@ def apply_materials(): baseColorValue = mainNode.inputs[0].default_value baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB") baseColorNode.outputs[0].default_value = baseColorValue - baseColorNode.location = ((mainNode.location[0] - 500, mainNode.location[1] - 300)) + baseColorNode.location = ((mainNode.location[0] - 1100, mainNode.location[1] - 300)) baseColorNode.name = "Lightmap_BasecolorNode_A" else: baseColorNode = mainNode.inputs[0].links[0].from_node @@ -235,13 +235,19 @@ def apply_materials(): mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode + #If skip metallic + if scene.TLM_SceneProperties.tlm_metallic_clamp == "skip": + if mainNode.inputs[4].default_value > 0.1: #DELIMITER + moutput = mainNode.inputs[0].links[0].from_node + mat.node_tree.links.remove(moutput.outputs[0].links[0]) + def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print(ext_postfix, new_postfix, formatHDR) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -267,6 +273,53 @@ def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): for image in bpy.data.images: image.reload() +def applyAOPass(): + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + for node in nodes: + if node.name == "Baked Image" or node.name == "TLM_Lightmap": + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + LightmapPath = node.image.filepath_raw + + filebase = os.path.basename(LightmapPath) + filename = os.path.splitext(filebase)[0] + extension = os.path.splitext(filebase)[1] + AOImagefile = filename[:-4] + "_ao" + AOImagePath = os.path.join(dirpath, AOImagefile + extension) + + AOMap = nodes.new('ShaderNodeTexImage') + AOMap.name = "TLM_AOMap" + AOImage = bpy.data.images.load(AOImagePath) + AOMap.image = AOImage + AOMap.location = -800, 0 + + AOMult = nodes.new(type="ShaderNodeMixRGB") + AOMult.name = "TLM_AOMult" + AOMult.blend_type = 'MULTIPLY' + AOMult.inputs[0].default_value = 1.0 + AOMult.location = -300, 300 + + multyNode = nodes["Lightmap_Multiplication"] + mainNode = nodes["Principled BSDF"] + UVMapNode = nodes["Lightmap_UV"] + + node_tree.links.remove(multyNode.outputs[0].links[0]) + + node_tree.links.new(multyNode.outputs[0], AOMult.inputs[1]) + node_tree.links.new(AOMap.outputs[0], AOMult.inputs[2]) + node_tree.links.new(AOMult.outputs[0], mainNode.inputs[0]) + node_tree.links.new(UVMapNode.outputs[0], AOMap.inputs[0]) + def load_library(asset_name): scriptDir = os.path.dirname(os.path.realpath(__file__)) diff --git a/blender/arm/lightmapper/utility/cycles/prepare.py b/blender/arm/lightmapper/utility/cycles/prepare.py index 6e5bd70f11..6222d18cc4 100644 --- a/blender/arm/lightmapper/utility/cycles/prepare.py +++ b/blender/arm/lightmapper/utility/cycles/prepare.py @@ -1,4 +1,4 @@ -import bpy +import bpy, math from . import cache from .. utility import * @@ -31,13 +31,16 @@ def configure_lights(): def configure_meshes(self): - for obj in bpy.data.objects: - if obj.type == "MESH": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Configuring meshes") + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -59,9 +62,18 @@ def configure_meshes(self): scene = bpy.context.scene - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying transform to: " + obj.name) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.hide_select = False #Remember to toggle this back for slot in obj.material_slots: if "." + slot.name + '_Original' in bpy.data.materials: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: @@ -77,33 +89,35 @@ def configure_meshes(self): bpy.ops.object.select_all(action='DESELECT') - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": uv_layers = obj.data.uv_layers - if not "UVMap_Lightmap" in uv_layers: + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("UVMap made A") - uvmap = uv_layers.new(name="UVMap_Lightmap") + print("UV map created for object: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) uv_layers.active_index = len(uv_layers) - 1 else: - print("Existing found...skipping") + print("Existing UV map found for object: " + obj.name) for i in range(0, len(uv_layers)): if uv_layers[i].name == 'UVMap_Lightmap': uv_layers.active_index = i - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift A") break atlas_items.append(obj) obj.select_set(True) - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - if atlasgroup.tlm_atlas_lightmap_unwrap_mode == "SmartProject": if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Smart Project A for: " + str(atlas_items)) + print("Atlasgroup Smart Project for: " + str(atlas_items)) for obj in atlas_items: print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name) @@ -112,7 +126,12 @@ def configure_meshes(self): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=atlasgroup.tlm_atlas_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Lightmap": @@ -123,8 +142,9 @@ def configure_meshes(self): bpy.ops.object.mode_set(mode='OBJECT') elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Xatlas": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Temporary skip: COPYING SMART PROJECT") + print("Using Xatlas on Atlas Group: " + atlas) for obj in atlas_items: obj.select_set(True) @@ -139,176 +159,213 @@ def configure_meshes(self): else: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Copied Existing A") + print("Copied Existing UV Map for Atlas Group: " + atlas) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: iterNum = iterNum + 1 - for obj in bpy.data.objects: - if obj.type == "MESH": - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for obj in bpy.context.scene.objects: + if obj.name in bpy.context.view_layer.objects: #Possible fix for view layer error + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - objWasHidden = False + objWasHidden = False - #For some reason, a Blender bug might prevent invisible objects from being smart projected - #We will turn the object temporarily visible - obj.hide_viewport = False - obj.hide_set(False) + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) - currentIterNum = currentIterNum + 1 + currentIterNum = currentIterNum + 1 - #Configure selection - bpy.ops.object.select_all(action='DESELECT') - bpy.context.view_layer.objects.active = obj - obj.select_set(True) - obs = bpy.context.view_layer.objects - active = obs.active + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) - #Provide material if none exists - preprocess_material(obj, scene) + obs = bpy.context.view_layer.objects + active = obs.active - #UV Layer management here - if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - uv_layers = obj.data.uv_layers - if not "UVMap_Lightmap" in uv_layers: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("UVMap made B") - uvmap = uv_layers.new(name="UVMap_Lightmap") - uv_layers.active_index = len(uv_layers) - 1 - - #If lightmap - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) - - #If smart project - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Smart Project B") - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.object.select_all(action='DESELECT') - obj.select_set(True) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) - - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": - - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - - #import blender_xatlas - #blender_xatlas.Unwrap_Lightmap_Group_Xatlas_2(bpy.context) + #Provide material if none exists + preprocess_material(obj, scene) - #bpy.ops.object.setup_unwrap() - Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + #UV Layer management here + if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + uv_layers = obj.data.uv_layers - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + if not uv_channel in uv_layers: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) - else: #if copy existing + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Copied Existing B") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - else: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Existing found...skipping") - for i in range(0, len(uv_layers)): - if uv_layers[i].name == 'UVMap_Lightmap': - uv_layers.active_index = i if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift B") - break + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing - #Sort out nodes - for slot in obj.material_slots: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) - nodetree = slot.material.node_tree + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break - outputNode = nodetree.nodes[0] #Presumed to be material output node + #print(x) - if(outputNode.type != "OUTPUT_MATERIAL"): - for node in nodetree.nodes: - if node.type == "OUTPUT_MATERIAL": - outputNode = node - break + #Sort out nodes + for slot in obj.material_slots: - mainNode = outputNode.inputs[0].links[0].from_node + nodetree = slot.material.node_tree - if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: + outputNode = nodetree.nodes[0] #Presumed to be material output node - #TODO! FIND THE PRINCIPLED PBR - self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break - if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: - mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] - else: - self.report({'INFO'}, "No principled found. Seeking diffuse") - if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: - mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] - else: - self.report({'INFO'}, "No supported nodes. Continuing anyway.") + mainNode = outputNode.inputs[0].links[0].from_node - if mainNode.type == 'GROUP': - if mainNode.node_tree != "Armory PBR": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("The material group is not supported!") + if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: - if (mainNode.type == "BSDF_PRINCIPLED"): - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("BSDF_Principled") - if scene.TLM_EngineProperties.tlm_directional_mode == "None": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Directional mode") - if not len(mainNode.inputs[19].links) == 0: + #TODO! FIND THE PRINCIPLED PBR + self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + + if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] + else: + self.report({'INFO'}, "No principled found. Seeking diffuse") + if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] + else: + self.report({'INFO'}, "No supported nodes. Continuing anyway.") + + if mainNode.type == 'GROUP': + if mainNode.node_tree != "Armory PBR": if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("NOT LEN 0") - ninput = mainNode.inputs[19].links[0] - noutput = mainNode.inputs[19].links[0].from_node - nodetree.links.remove(noutput.outputs[0].links[0]) + print("The material group is not supported!") - #Clamp metallic - if(mainNode.inputs[4].default_value == 1): - mainNode.inputs[4].default_value = 0.0 + if (mainNode.type == "BSDF_PRINCIPLED"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Principled") + if scene.TLM_EngineProperties.tlm_directional_mode == "None": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Directional mode") + if not len(mainNode.inputs[19].links) == 0: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("NOT LEN 0") + ninput = mainNode.inputs[19].links[0] + noutput = mainNode.inputs[19].links[0].from_node + nodetree.links.remove(noutput.outputs[0].links[0]) + + #Clamp metallic + if bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "limit": + MainMetNodeSocket = mainNode.inputs[4] + if not len(MainMetNodeSocket.links) == 0: + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.9 + minput = mainNode.inputs[4].links[0] #Metal input socket + moutput = mainNode.inputs[4].links[0].from_node #Metal output node + nodetree.links.remove(moutput.outputs[0].links[0]) #Works + nodetree.links.new(moutput.outputs[0], MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0],MainMetNodeSocket) #clamp node to metinput + else: + if mainNode.inputs[4].default_value > 0.9: + mainNode.inputs[4].default_value = 0.9 + elif bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "zero": + MainMetNodeSocket = mainNode.inputs[4] + if not len(MainMetNodeSocket.links) == 0: + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.0 + minput = mainNode.inputs[4].links[0] #Metal input socket + moutput = mainNode.inputs[4].links[0].from_node #Metal output node + nodetree.links.remove(moutput.outputs[0].links[0]) #Works + nodetree.links.new(moutput.outputs[0], MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0],MainMetNodeSocket) #clamp node to metinput + else: + mainNode.inputs[4].default_value = 0.0 + + if (mainNode.type == "BSDF_DIFFUSE"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Diffuse") - if (mainNode.type == "BSDF_DIFFUSE"): - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("BSDF_Diffuse") + # if (mainNode.type == "BSDF_DIFFUSE"): + # if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + # print("BSDF_Diffuse") - for slot in obj.material_slots: + #TODO FIX THIS PART! + #THIS IS USED IN CASES WHERE FOR SOME REASON THE USER FORGETS TO CONNECT SOMETHING INTO THE OUTPUT MATERIAL + for slot in obj.material_slots: - nodetree = bpy.data.materials[slot.name].node_tree - nodes = nodetree.nodes + nodetree = bpy.data.materials[slot.name].node_tree + nodes = nodetree.nodes - #First search to get the first output material type - for node in nodetree.nodes: - if node.type == "OUTPUT_MATERIAL": - mainNode = node - break + #First search to get the first output material type + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + mainNode = node + break - #Fallback to get search - if not mainNode.type == "OUTPUT_MATERIAL": - mainNode = nodetree.nodes.get("Material Output") + #Fallback to get search + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes.get("Material Output") - #Last resort to first node in list - if not mainNode.type == "OUTPUT_MATERIAL": - mainNode = nodetree.nodes[0].inputs[0].links[0].from_node + #Last resort to first node in list + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes[0].inputs[0].links[0].from_node - for node in nodes: - if "LM" in node.name: - nodetree.links.new(node.outputs[0], mainNode.inputs[0]) + # for node in nodes: + # if "LM" in node.name: + # nodetree.links.new(node.outputs[0], mainNode.inputs[0]) - for node in nodes: - if "Lightmap" in node.name: - nodes.remove(node) + # for node in nodes: + # if "Lightmap" in node.name: + # nodes.remove(node) def preprocess_material(obj, scene): if len(obj.material_slots) == 0: @@ -537,7 +594,7 @@ def store_existing(prev_container): selected = [] - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.select_get(): selected.append(obj.name) diff --git a/blender/arm/lightmapper/utility/denoiser/integrated.py b/blender/arm/lightmapper/utility/denoiser/integrated.py index e3061f30ee..38c1cabc59 100644 --- a/blender/arm/lightmapper/utility/denoiser/integrated.py +++ b/blender/arm/lightmapper/utility/denoiser/integrated.py @@ -22,7 +22,7 @@ def cull_undefined(self): bpy.ops.object.camera_add() #Just select the first camera we find, needed for the compositor - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.type == "CAMERA": bpy.context.scene.camera = obj return diff --git a/blender/arm/lightmapper/utility/encoding.py b/blender/arm/lightmapper/utility/encoding.py index 92a8e8d50e..f716f342df 100644 --- a/blender/arm/lightmapper/utility/encoding.py +++ b/blender/arm/lightmapper/utility/encoding.py @@ -332,6 +332,205 @@ def encodeImageRGBDGPU(image, maxRange, outDir, quality): #Todo - Find a way to save #bpy.ops.image.save_all_modified() +#TODO - FINISH THIS +def encodeImageRGBMGPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + //Code from here: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/helperFunctions.fx + + const float PI = 3.1415926535897932384626433832795; + const float HALF_MIN = 5.96046448e-08; // Smallest positive half. + + const float LinearEncodePowerApprox = 2.2; + const float GammaEncodePowerApprox = 1.0 / LinearEncodePowerApprox; + const vec3 LuminanceEncodeApprox = vec3(0.2126, 0.7152, 0.0722); + + const float Epsilon = 0.0000001; + #define saturate(x) clamp(x, 0.0, 1.0) + + float maxEps(float x) { + return max(x, Epsilon); + } + + float toLinearSpace(float color) + { + return pow(color, LinearEncodePowerApprox); + } + + vec3 toLinearSpace(vec3 color) + { + return pow(color, vec3(LinearEncodePowerApprox)); + } + + vec4 toLinearSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(LinearEncodePowerApprox)), color.a); + } + + vec3 toGammaSpace(vec3 color) + { + return pow(color, vec3(GammaEncodePowerApprox)); + } + + vec4 toGammaSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(GammaEncodePowerApprox)), color.a); + } + + float toGammaSpace(float color) + { + return pow(color, GammaEncodePowerApprox); + } + + float square(float value) + { + return value * value; + } + + // Check if configurable value is needed. + const float rgbdMaxRange = 255.0; + + vec4 toRGBM(vec3 color) { + + vec4 rgbm; + color *= 1.0/6.0; + rgbm.a = saturate( max( max( color.r, color.g ), max( color.b, 1e-6 ) ) ); + rgbm.a = clamp(floor(D) / 255.0, 0., 1.); + rgbm.rgb = color / rgbm.a; + + return + + float maxRGB = maxEps(max(color.r, max(color.g, color.b))); + float D = max(rgbdMaxRange / maxRGB, 1.); + D = clamp(floor(D) / 255.0, 0., 1.); + vec3 rgb = color.rgb * D; + + // Helps with png quantization. + rgb = toGammaSpace(rgb); + + return vec4(rgb, D); + } + + vec3 fromRGBD(vec4 rgbd) { + // Helps with png quantization. + rgbd.rgb = toLinearSpace(rgbd.rgb); + + // return rgbd.rgb * ((rgbdMaxRange / 255.0) / rgbd.a); + + return rgbd.rgb / rgbd.a; + } + + void main() + { + + fragColor = toRGBM(texture(image, texCoord_interp).rgb); + + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + + #Todo - Find a way to save + #bpy.ops.image.save_all_modified() + def encodeImageRGBMCPU(image, maxRange, outDir, quality): input_image = bpy.data.images[image.name] image_name = input_image.name @@ -431,21 +630,6 @@ def encodeImageRGBDCPU(image, maxRange, outDir, quality): result_pixel[i+1] = math.pow(result_pixel[i+1] * D, 1/2.2) result_pixel[i+2] = math.pow(result_pixel[i+2] * D, 1/2.2) result_pixel[i+3] = D - - - # for i in range(0,num_pixels,4): - - # m = saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6)) - # d = max(maxRange / m, 1) - # #d = saturate(math.floor(d) / 255.0) - # d = np.clip((math.floor(d) / 255.0), 0.0, 1.0) - - # #TODO TO GAMMA SPACE - - # result_pixel[i] = math.pow(result_pixel[i] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+1] = math.pow(result_pixel[i+1] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+2] = math.pow(result_pixel[i+2] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+3] = d target_image.pixels = result_pixel @@ -457,25 +641,4 @@ def encodeImageRGBDCPU(image, maxRange, outDir, quality): input_image.filepath_raw = outDir + "/" + input_image.name + ".png" input_image.file_format = "PNG" bpy.context.scene.render.image_settings.quality = quality - input_image.save() - - # const float rgbdMaxRange = 255.0; - - # vec4 toRGBD(vec3 color) { - # float maxRGB = maxEps(max(color.r, max(color.g, color.b))); - # float D = max(rgbdMaxRange / maxRGB, 1.); - # D = clamp(floor(D) / 255.0, 0., 1.); - # vec3 rgb = color.rgb * D; - - # // Helps with png quantization. - # rgb = toGammaSpace(rgb); - - # return vec4(rgb, D); - # } - - # const float Epsilon = 0.0000001; - # #define saturate(x) clamp(x, 0.0, 1.0) - - # float maxEps(float x) { - # return max(x, Epsilon); - # } \ No newline at end of file + input_image.save() \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/opencv.py b/blender/arm/lightmapper/utility/filtering/opencv.py index 501b2b3de8..c6b1b5578e 100644 --- a/blender/arm/lightmapper/utility/filtering/opencv.py +++ b/blender/arm/lightmapper/utility/filtering/opencv.py @@ -62,7 +62,7 @@ def init(lightmap_dir, denoise): #SEAM TESTING# ##################### - if obj_name in bpy.data.objects: + if obj_name in bpy.context.scene.objects: override = bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override elif obj_name in scene.TLM_AtlasList: override = False diff --git a/blender/arm/lightmapper/utility/gui/Viewport.py b/blender/arm/lightmapper/utility/gui/Viewport.py new file mode 100644 index 0000000000..be1e8d2b0b --- /dev/null +++ b/blender/arm/lightmapper/utility/gui/Viewport.py @@ -0,0 +1,67 @@ +import bpy, blf, bgl, os, gpu +from gpu_extras.batch import batch_for_shader + +class ViewportDraw: + + def __init__(self, context, text): + + bakefile = "TLM_Overlay.png" + scriptDir = os.path.dirname(os.path.realpath(__file__)) + bakefile_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'assets/' + bakefile)) + + image_name = "TLM_Overlay.png" + + bpy.ops.image.open(filepath=bakefile_path) + + print("Self path: " + bakefile_path) + + image = bpy.data.images[image_name] + + x = 15 + y = 15 + w = 400 + h = 200 + + self.shader = gpu.shader.from_builtin('2D_IMAGE') + self.batch = batch_for_shader( + self.shader, 'TRI_FAN', + { + "pos": ((x, y), (x+w, y), (x+w, y+h), (x, y+h)), + "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + self.text = text + self.image = image + #self.handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_text_callback, (context,), 'WINDOW', 'POST_PIXEL') + self.handle2 = bpy.types.SpaceView3D.draw_handler_add(self.draw_image_callback, (context,), 'WINDOW', 'POST_PIXEL') + + def draw_text_callback(self, context): + + font_id = 0 + blf.position(font_id, 15, 15, 0) + blf.size(font_id, 20, 72) + blf.draw(font_id, "%s" % (self.text)) + + def draw_image_callback(self, context): + + if self.image: + bgl.glEnable(bgl.GL_BLEND) + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode) + + self.shader.bind() + self.shader.uniform_int("image", 0) + self.batch.draw(self.shader) + bgl.glDisable(bgl.GL_BLEND) + + def update_text(self, text): + + self.text = text + + def remove_handle(self): + #bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW') + bpy.types.SpaceView3D.draw_handler_remove(self.handle2, 'WINDOW') \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/luxcore/setup.py b/blender/arm/lightmapper/utility/luxcore/setup.py new file mode 100644 index 0000000000..13bfc57bf4 --- /dev/null +++ b/blender/arm/lightmapper/utility/luxcore/setup.py @@ -0,0 +1,259 @@ +import bpy + +from .. utility import * + +def init(self, prev_container): + + #TODO - JSON classes + export.scene = """scene.camera.cliphither = 0.1 +scene.camera.clipyon = 100 +scene.camera.shutteropen = 0 +scene.camera.shutterclose = 1 +scene.camera.autovolume.enable = 1 +scene.camera.lookat.orig = 7.358891 -6.925791 4.958309 +scene.camera.lookat.target = 6.707333 -6.31162 4.513038 +scene.camera.up = -0.3240135 0.3054208 0.8953956 +scene.camera.screenwindow = -1 1 -0.5625 0.5625 +scene.camera.lensradius = 0 +scene.camera.focaldistance = 10 +scene.camera.autofocus.enable = 0 +scene.camera.type = "perspective" +scene.camera.oculusrift.barrelpostpro.enable = 0 +scene.camera.fieldofview = 39.59776 +scene.camera.bokeh.blades = 0 +scene.camera.bokeh.power = 3 +scene.camera.bokeh.distribution.type = "NONE" +scene.camera.bokeh.scale.x = 0.7071068 +scene.camera.bokeh.scale.y = 0.7071068 +scene.lights.__WORLD_BACKGROUND_LIGHT__.gain = 2e-05 2e-05 2e-05 +scene.lights.__WORLD_BACKGROUND_LIGHT__.transformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.id = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature = -1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature.normalize = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.diffuse.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.glossy.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.specular.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.type = "sky2" +scene.lights.__WORLD_BACKGROUND_LIGHT__.dir = 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.turbidity = 2.2 +scene.lights.__WORLD_BACKGROUND_LIGHT__.groundalbedo = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.enable = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.color = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.autoscale = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.width = 512 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.height = 256 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibilitymapcache.enable = 0 +scene.lights.2382361116072.gain = 1 1 1 +scene.lights.2382361116072.transformation = -0.2908646 0.9551712 -0.05518906 0 -0.7711008 -0.1998834 0.6045247 0 0.5663932 0.2183912 0.7946723 0 4.076245 1.005454 5.903862 1 +scene.lights.2382361116072.id = 0 +scene.lights.2382361116072.temperature = -1 +scene.lights.2382361116072.temperature.normalize = 0 +scene.lights.2382361116072.type = "sphere" +scene.lights.2382361116072.color = 1 1 1 +scene.lights.2382361116072.power = 0 +scene.lights.2382361116072.normalizebycolor = 0 +scene.lights.2382361116072.efficency = 0 +scene.lights.2382361116072.position = 0 0 0 +scene.lights.2382361116072.radius = 0.1 +scene.materials.Material2382357175256.type = "disney" +scene.materials.Material2382357175256.basecolor = "0.7 0.7 0.7" +scene.materials.Material2382357175256.subsurface = "0" +scene.materials.Material2382357175256.roughness = "0.2" +scene.materials.Material2382357175256.metallic = "0" +scene.materials.Material2382357175256.specular = "0.5" +scene.materials.Material2382357175256.speculartint = "0" +scene.materials.Material2382357175256.clearcoat = "0" +scene.materials.Material2382357175256.clearcoatgloss = "1" +scene.materials.Material2382357175256.anisotropic = "0" +scene.materials.Material2382357175256.sheen = "0" +scene.materials.Material2382357175256.sheentint = "0" +scene.materials.Material2382357175256.transparency.shadow = 0 0 0 +scene.materials.Material2382357175256.id = 3364224 +scene.materials.Material2382357175256.emission.gain = 1 1 1 +scene.materials.Material2382357175256.emission.power = 0 +scene.materials.Material2382357175256.emission.normalizebycolor = 1 +scene.materials.Material2382357175256.emission.efficency = 0 +scene.materials.Material2382357175256.emission.theta = 90 +scene.materials.Material2382357175256.emission.id = 0 +scene.materials.Material2382357175256.emission.importance = 1 +scene.materials.Material2382357175256.emission.temperature = -1 +scene.materials.Material2382357175256.emission.temperature.normalize = 0 +scene.materials.Material2382357175256.emission.directlightsampling.type = "AUTO" +scene.materials.Material2382357175256.visibility.indirect.diffuse.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.glossy.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.specular.enable = 1 +scene.materials.Material2382357175256.shadowcatcher.enable = 0 +scene.materials.Material2382357175256.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material2382357175256.photongi.enable = 1 +scene.materials.Material2382357175256.holdout.enable = 0 +scene.materials.Material__0012382357172440.type = "disney" +scene.materials.Material__0012382357172440.basecolor = "0.7 0.7 0.7" +scene.materials.Material__0012382357172440.subsurface = "0" +scene.materials.Material__0012382357172440.roughness = "0.2" +scene.materials.Material__0012382357172440.metallic = "0" +scene.materials.Material__0012382357172440.specular = "0.5" +scene.materials.Material__0012382357172440.speculartint = "0" +scene.materials.Material__0012382357172440.clearcoat = "0" +scene.materials.Material__0012382357172440.clearcoatgloss = "1" +scene.materials.Material__0012382357172440.anisotropic = "0" +scene.materials.Material__0012382357172440.sheen = "0" +scene.materials.Material__0012382357172440.sheentint = "0" +scene.materials.Material__0012382357172440.transparency.shadow = 0 0 0 +scene.materials.Material__0012382357172440.id = 6728256 +scene.materials.Material__0012382357172440.emission.gain = 1 1 1 +scene.materials.Material__0012382357172440.emission.power = 0 +scene.materials.Material__0012382357172440.emission.normalizebycolor = 1 +scene.materials.Material__0012382357172440.emission.efficency = 0 +scene.materials.Material__0012382357172440.emission.theta = 90 +scene.materials.Material__0012382357172440.emission.id = 0 +scene.materials.Material__0012382357172440.emission.importance = 1 +scene.materials.Material__0012382357172440.emission.temperature = -1 +scene.materials.Material__0012382357172440.emission.temperature.normalize = 0 +scene.materials.Material__0012382357172440.emission.directlightsampling.type = "AUTO" +scene.materials.Material__0012382357172440.visibility.indirect.diffuse.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.glossy.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.specular.enable = 1 +scene.materials.Material__0012382357172440.shadowcatcher.enable = 0 +scene.materials.Material__0012382357172440.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material__0012382357172440.photongi.enable = 1 +scene.materials.Material__0012382357172440.holdout.enable = 0 +scene.objects.23823611086320.material = "Material2382357175256" +scene.objects.23823611086320.ply = "mesh-00000.ply" +scene.objects.23823611086320.camerainvisible = 0 +scene.objects.23823611086320.id = 1326487202 +scene.objects.23823611086320.appliedtransformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 +scene.objects.23823611279760.material = "Material__0012382357172440" +scene.objects.23823611279760.ply = "mesh-00001.ply" +scene.objects.23823611279760.camerainvisible = 0 +scene.objects.23823611279760.id = 3772660237 +scene.objects.23823611279760.appliedtransformation = 5 0 0 0 0 5 0 0 0 0 5 0 0 0 0 1 +""" + + export.config = """context.verbose = 1 +accelerator.type = "AUTO" +accelerator.instances.enable = 1 +accelerator.motionblur.enable = 1 +accelerator.bvh.builder.type = "EMBREE_BINNED_SAH" +accelerator.bvh.treetype = 4 +accelerator.bvh.costsamples = 0 +accelerator.bvh.isectcost = 80 +accelerator.bvh.travcost = 10 +accelerator.bvh.emptybonus = 0.5 +scene.epsilon.min = "1e-05" +scene.epsilon.max = "0.1" +scene.file = "scene.scn" +images.scale = 1 +lightstrategy.type = "LOG_POWER" +native.threads.count = 8 +renderengine.type = "BAKECPU" +path.pathdepth.total = "7" +path.pathdepth.diffuse = "5" +path.pathdepth.glossy = "5" +path.pathdepth.specular = "6" +path.hybridbackforward.enable = "0" +path.hybridbackforward.partition = "0.8" +path.hybridbackforward.glossinessthreshold = "0.049" +path.russianroulette.depth = 3 +path.russianroulette.cap = 0.5 +path.clamping.variance.maxvalue = 0 +path.forceblackbackground.enable = "0" +sampler.type = "SOBOL" +sampler.imagesamples.enable = 1 +sampler.sobol.adaptive.strength = "0.9" +sampler.sobol.adaptive.userimportanceweight = 0.75 +sampler.sobol.bucketsize = "16" +sampler.sobol.tilesize = "16" +sampler.sobol.supersampling = "1" +sampler.sobol.overlapping = "1" +path.photongi.sampler.type = "METROPOLIS" +path.photongi.photon.maxcount = 100000000 +path.photongi.photon.maxdepth = 4 +path.photongi.photon.time.start = 0 +path.photongi.photon.time.end = -1 +path.photongi.visibility.lookup.radius = 0 +path.photongi.visibility.lookup.normalangle = 10 +path.photongi.visibility.targethitrate = 0.99 +path.photongi.visibility.maxsamplecount = 1048576 +path.photongi.glossinessusagethreshold = 0.05 +path.photongi.indirect.enabled = 0 +path.photongi.indirect.maxsize = 0 +path.photongi.indirect.haltthreshold = 0.05 +path.photongi.indirect.lookup.radius = 0 +path.photongi.indirect.lookup.normalangle = 10 +path.photongi.indirect.usagethresholdscale = 8 +path.photongi.indirect.filter.radiusscale = 3 +path.photongi.caustic.enabled = 0 +path.photongi.caustic.maxsize = 100000 +path.photongi.caustic.updatespp = 8 +path.photongi.caustic.updatespp.radiusreduction = 0.96 +path.photongi.caustic.updatespp.minradius = 0.003 +path.photongi.caustic.lookup.radius = 0.15 +path.photongi.caustic.lookup.normalangle = 10 +path.photongi.debug.type = "none" +path.photongi.persistent.file = "" +path.photongi.persistent.safesave = 1 +film.filter.type = "BLACKMANHARRIS" +film.filter.width = 2 +opencl.platform.index = -1 +film.width = 960 +film.height = 600 +film.safesave = 1 +film.noiseestimation.step = "32" +film.noiseestimation.warmup = "8" +film.noiseestimation.filter.scale = 4 +batch.haltnoisethreshold = 0.01 +batch.haltnoisethreshold.step = 64 +batch.haltnoisethreshold.warmup = 64 +batch.haltnoisethreshold.filter.enable = 1 +batch.haltnoisethreshold.stoprendering.enable = 1 +batch.halttime = "0" +batch.haltspp = 32 +film.outputs.safesave = 1 +film.outputs.0.type = "RGB_IMAGEPIPELINE" +film.outputs.0.filename = "RGB_IMAGEPIPELINE_0.png" +film.outputs.0.index = "0" +film.imagepipelines.000.0.type = "NOP" +film.imagepipelines.000.1.type = "TONEMAP_LINEAR" +film.imagepipelines.000.1.scale = "1" +film.imagepipelines.000.2.type = "GAMMA_CORRECTION" +film.imagepipelines.000.2.value = "2.2" +film.imagepipelines.000.radiancescales.0.enabled = "1" +film.imagepipelines.000.radiancescales.0.globalscale = "1" +film.imagepipelines.000.radiancescales.0.rgbscale = "1" "1" "1" +periodicsave.film.outputs.period = 0 +periodicsave.film.period = 0 +periodicsave.film.filename = "film.flm" +periodicsave.resumerendering.period = 0 +periodicsave.resumerendering.filename = "rendering.rsm" +resumerendering.filesafe = 1 +debug.renderconfig.parse.print = 0 +debug.scene.parse.print = 0 +screen.refresh.interval = 100 +screen.tool.type = "CAMERA_EDIT" +screen.tiles.pending.show = 1 +screen.tiles.converged.show = 0 +screen.tiles.notconverged.show = 0 +screen.tiles.passcount.show = 0 +screen.tiles.error.show = 0 +bake.minmapautosize = 64 +bake.maxmapautosize = 1024 +bake.powerof2autosize.enable = 1 +bake.skipexistingmapfiles = 1 +film.imagepipelines.1.0.type = "NOP" +bake.maps.0.type = "COMBINED" +bake.maps.0.filename = "23823611086320.exr" +bake.maps.0.imagepipelineindex = 1 +bake.maps.0.width = 512 +bake.maps.0.height = 512 +bake.maps.0.autosize.enabled = 1 +bake.maps.0.uvindex = 0 +bake.maps.0.objectnames = "23823611086320" +bake.maps.1.type = "COMBINED" +bake.maps.1.filename = "23823611279760.exr" +bake.maps.1.imagepipelineindex = 1 +bake.maps.1.width = 512 +bake.maps.1.height = 512 +bake.maps.1.autosize.enabled = 1 +bake.maps.1.uvindex = 0 +bake.maps.1.objectnames = "23823611279760" +""" \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/configure.py b/blender/arm/lightmapper/utility/octane/configure.py new file mode 100644 index 0000000000..ba6641ab78 --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/configure.py @@ -0,0 +1,243 @@ +import bpy, math + +#from . import cache +from .. utility import * + +def init(self, prev_container): + + #store_existing(prev_container) + + #set_settings() + + configure_world() + + configure_lights() + + configure_meshes(self) + +def configure_world(): + pass + +def configure_lights(): + pass + +def configure_meshes(self): + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + iterNum = 1 + currentIterNum = 0 + + scene = bpy.context.scene + + for obj in scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + obj.hide_select = False #Remember to toggle this back + + currentIterNum = currentIterNum + 1 + + obj.octane.baking_group_id = 1 + currentIterNum #0 doesn't exist, 1 is neutral and 2 is first baked object + + print("Obj: " + obj.name + " set to baking group: " + str(obj.octane.baking_group_id)) + + for slot in obj.material_slots: + if "." + slot.name + '_Original' in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original') + slot.material = bpy.data.materials["." + slot.name + '_Original'] + + + objWasHidden = False + + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) + + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + + uv_layers = obj.data.uv_layers + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + print("Setting active UV to: " + uv_layers.active_index) + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) + + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + set_camera() + +def set_camera(): + + cam_name = "TLM-BakeCam" + + if not cam_name in bpy.context.scene: + camera = bpy.data.cameras.new(cam_name) + camobj_name = "TLM-BakeCam-obj" + cam_obj = bpy.data.objects.new(camobj_name, camera) + bpy.context.collection.objects.link(cam_obj) + cam_obj.location = ((0,0,0)) + + bpy.context.scene.camera = cam_obj + +def set_settings(): + + scene = bpy.context.scene + cycles = scene.cycles + scene.render.engine = "CYCLES" + sceneProperties = scene.TLM_SceneProperties + engineProperties = scene.TLM_EngineProperties + cycles.device = scene.TLM_EngineProperties.tlm_mode + + if cycles.device == "GPU": + scene.render.tile_x = 256 + scene.render.tile_y = 256 + else: + scene.render.tile_x = 32 + scene.render.tile_y = 32 + + if engineProperties.tlm_quality == "0": + cycles.samples = 32 + cycles.max_bounces = 1 + cycles.diffuse_bounces = 1 + cycles.glossy_bounces = 1 + cycles.transparent_max_bounces = 1 + cycles.transmission_bounces = 1 + cycles.volume_bounces = 1 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "1": + cycles.samples = 64 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "2": + cycles.samples = 512 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "3": + cycles.samples = 1024 + cycles.max_bounces = 256 + cycles.diffuse_bounces = 256 + cycles.glossy_bounces = 256 + cycles.transparent_max_bounces = 256 + cycles.transmission_bounces = 256 + cycles.volume_bounces = 256 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "4": + cycles.samples = 2048 + cycles.max_bounces = 512 + cycles.diffuse_bounces = 512 + cycles.glossy_bounces = 512 + cycles.transparent_max_bounces = 512 + cycles.transmission_bounces = 512 + cycles.volume_bounces = 512 + cycles.caustics_reflective = True + cycles.caustics_refractive = True + else: #Custom + pass + +def store_existing(prev_container): + + scene = bpy.context.scene + cycles = scene.cycles + + selected = [] + + for obj in bpy.context.scene.objects: + if obj.select_get(): + selected.append(obj.name) + + prev_container["settings"] = [ + cycles.samples, + cycles.max_bounces, + cycles.diffuse_bounces, + cycles.glossy_bounces, + cycles.transparent_max_bounces, + cycles.transmission_bounces, + cycles.volume_bounces, + cycles.caustics_reflective, + cycles.caustics_refractive, + cycles.device, + scene.render.engine, + bpy.context.view_layer.objects.active, + selected, + [scene.render.resolution_x, scene.render.resolution_y] + ] \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/lightmap2.py b/blender/arm/lightmapper/utility/octane/lightmap2.py new file mode 100644 index 0000000000..ad84327632 --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/lightmap2.py @@ -0,0 +1,71 @@ +import bpy, os + +def bake(): + + cam_name = "TLM-BakeCam-obj" + + if cam_name in bpy.context.scene.objects: + + print("Camera found...") + + camera = bpy.context.scene.objects[cam_name] + + camera.data.octane.baking_camera = True + + for obj in bpy.context.scene.objects: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(False) + + iterNum = 2 + currentIterNum = 1 + + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + if iterNum > 1: + iterNum = iterNum - 1 + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + currentIterNum = currentIterNum + 1 + + scene = bpy.context.scene + + print("Baking obj: " + obj.name) + + print("Baking ID: " + str(currentIterNum) + " out of " + str(iterNum)) + + bpy.ops.object.select_all(action='DESELECT') + + camera.data.octane.baking_group_id = currentIterNum + + savedir = os.path.dirname(bpy.data.filepath) + user_dir = scene.TLM_Engine3Properties.tlm_lightmap_savedir + directory = os.path.join(savedir, user_dir) + + image_settings = bpy.context.scene.render.image_settings + image_settings.file_format = "HDR" + image_settings.color_depth = '32' + + filename = os.path.join(directory, "LM") + "_" + obj.name + ".hdr" + bpy.context.scene.render.filepath = filename + + resolution = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) + + bpy.context.scene.render.resolution_x = resolution + bpy.context.scene.render.resolution_y = resolution + + bpy.ops.render.render(write_still=True) + + else: + + print("No baking camera found") + + + + + print("Baking in Octane!") \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/pack.py b/blender/arm/lightmapper/utility/pack.py index 3c2768c796..c2e1f15c57 100644 --- a/blender/arm/lightmapper/utility/pack.py +++ b/blender/arm/lightmapper/utility/pack.py @@ -106,7 +106,7 @@ def postpack(): rect = [] #For each object that targets the atlas - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -156,7 +156,13 @@ def postpack(): obj = bpy.data.objects[aob] for idx, layer in enumerate(obj.data.uv_layers): - if layer.name == "UVMap_Lightmap": + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if layer.name == uv_channel: obj.data.uv_layers.active_index = idx print("UVLayer set to: " + str(obj.data.uv_layers.active_index)) @@ -194,7 +200,7 @@ def postpack(): print("Written: " + str(os.path.join(lightmap_directory, atlas.name + end + formatEnc))) #Change the material for each material, slot - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -219,7 +225,7 @@ def postpack(): existing_image.user_clear() #Add dilation map here... - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: diff --git a/blender/arm/lightmapper/utility/utility.py b/blender/arm/lightmapper/utility/utility.py index 7592915797..b206c773d3 100644 --- a/blender/arm/lightmapper/utility/utility.py +++ b/blender/arm/lightmapper/utility/utility.py @@ -1,5 +1,5 @@ import bpy.ops as O -import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh +import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh, shutil, glob, uuid from io import StringIO from threading import Thread from queue import Queue, Empty @@ -81,15 +81,8 @@ def save_image(image): image.filepath_raw = savepath - # if "Normal" in image.name: - # bpy.context.scene.render.image_settings.quality = 90 - # image.save_render( filepath = image.filepath_raw, scene = bpy.context.scene ) - # else: image.save() - - - def get_file_size(filepath): size = "Unpack Files" try: @@ -141,7 +134,7 @@ def check_is_org_material(self,material): def clean_empty_materials(self): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: for slot in obj.material_slots: mat = slot.material if mat is None: @@ -319,6 +312,11 @@ def lightmap_to_ao(material,lightmap_node): # https://github.com/mattedicksoncom/blender-xatlas/ ########################################################### +def gen_safe_name(): + genId = uuid.uuid4().hex + # genId = "u_" + genId.replace("-","_") + return "u_" + genId + def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): blender_xatlas = importlib.util.find_spec("blender_xatlas") @@ -330,32 +328,54 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): packOptions = bpy.context.scene.pack_tool chartOptions = bpy.context.scene.chart_tool + sharedProperties = bpy.context.scene.shared_properties + #sharedProperties.unwrapSelection context = bpy.context - - if obj.type == 'MESH': - context.view_layer.objects.active = obj - if obj.data.users > 1: - obj.data = obj.data.copy() #make single user copy - uv_layers = obj.data.uv_layers - - #setup the lightmap uvs - uvName = "UVMap_Lightmap" - if sharedProperties.lightmapUVChoiceType == "NAME": - uvName = sharedProperties.lightmapUVName - elif sharedProperties.lightmapUVChoiceType == "INDEX": - if sharedProperties.lightmapUVIndex < len(uv_layers): - uvName = uv_layers[sharedProperties.lightmapUVIndex].name - - if not uvName in uv_layers: - uvmap = uv_layers.new(name=uvName) - uv_layers.active_index = len(uv_layers) - 1 - else: - for i in range(0, len(uv_layers)): - if uv_layers[i].name == uvName: - uv_layers.active_index = i - obj.select_set(True) + + #save whatever mode the user was in + startingMode = bpy.context.object.mode + selected_objects = bpy.context.selected_objects + + #check something is actually selected + #external function/operator will select them + if len(selected_objects) == 0: + print("Nothing Selected") + self.report({"WARNING"}, "Nothing Selected, please select Something") + return {'FINISHED'} + + #store the names of objects to be lightmapped + rename_dict = dict() + safe_dict = dict() + + #make sure all the objects have ligthmap uvs + for obj in selected_objects: + if obj.type == 'MESH': + safe_name = gen_safe_name(); + rename_dict[obj.name] = (obj.name,safe_name) + safe_dict[safe_name] = obj.name + context.view_layer.objects.active = obj + if obj.data.users > 1: + obj.data = obj.data.copy() #make single user copy + uv_layers = obj.data.uv_layers + + #setup the lightmap uvs + uvName = "UVMap_Lightmap" + if sharedProperties.lightmapUVChoiceType == "NAME": + uvName = sharedProperties.lightmapUVName + elif sharedProperties.lightmapUVChoiceType == "INDEX": + if sharedProperties.lightmapUVIndex < len(uv_layers): + uvName = uv_layers[sharedProperties.lightmapUVIndex].name + + if not uvName in uv_layers: + uvmap = uv_layers.new(name=uvName) + uv_layers.active_index = len(uv_layers) - 1 + else: + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uvName: + uv_layers.active_index = i + obj.select_set(True) #save all the current edges if sharedProperties.packOnly: @@ -381,8 +401,11 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): bpy.ops.object.mode_set(mode='OBJECT') + #Create a fake obj export to a string + #Will strip this down further later fakeFile = StringIO() blender_xatlas.export_obj_simple.save( + rename_dict=rename_dict, context=bpy.context, filepath=fakeFile, mainUVChoiceType=sharedProperties.mainUVChoiceType, @@ -393,20 +416,26 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): use_mesh_modifiers=True, use_edges=True, use_smooth_groups=False, - use_smooth_groups_bitflags=False, + use_smooth_groups_bitflags=False, use_normals=True, use_uvs=True, use_materials=False, use_triangles=False, - use_nurbs=False, - use_vertex_groups=False, + use_nurbs=False, + use_vertex_groups=False, use_blen_objects=True, group_by_object=False, group_by_material=False, keep_vertex_order=False, ) - file_path = os.path.dirname(os.path.abspath(blender_xatlas.__file__)) + #print just for reference + # print(fakeFile.getvalue()) + + #get the path to xatlas + #file_path = os.path.dirname(os.path.abspath(__file__)) + scriptsDir = bpy.utils.user_resource('SCRIPTS', "addons") + file_path = os.path.join(scriptsDir, "blender_xatlas") if platform.system() == "Windows": xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe") elif platform.system() == "Linux": @@ -458,6 +487,8 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): shell=True ) + print(xatlas_path) + #shove the fake file in stdin stdin = xatlas_process.stdin value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly @@ -482,17 +513,17 @@ class uvObject: obName: string = "" uvArray: List[float] = field(default_factory=list) faceArray: List[int] = field(default_factory=list) - + convertedObjects = [] uvArrayComplete = [] - + #search through the out put for STARTOBJ #then start reading the objects obTest = None startRead = False for line in outObj.splitlines(): - + line_split = line.split() if not line_split: @@ -504,14 +535,14 @@ class uvObject: print("Start reading the objects----------------------------------------") startRead = True # obTest = uvObject() - + if startRead: #if it's a new obj if line_start == 'o': #if there is already an object append it if obTest is not None: convertedObjects.append(obTest) - + obTest = uvObject() #create new uv object obTest.obName = line_split[1] @@ -536,9 +567,9 @@ class uvObject: #append the final object convertedObjects.append(obTest) - # print(convertedObjects) - - + print(convertedObjects) + + #apply the output------------------------------------------------------------- #copy the uvs to the original objects # objIndex = 0 @@ -548,7 +579,7 @@ class uvObject: bpy.ops.object.select_all(action='DESELECT') obTest = importObject - + obTest.obName = safe_dict[obTest.obName] #probably shouldn't just replace it bpy.context.scene.objects[obTest.obName].select_set(True) context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName] bpy.ops.object.mode_set(mode = 'OBJECT') @@ -563,7 +594,7 @@ class uvObject: nFaces = len(bm.faces) #need to ensure lookup table for some reason? - if hasattr(bm.faces, "ensure_lookup_table"): + if hasattr(bm.faces, "ensure_lookup_table"): bm.faces.ensure_lookup_table() #loop through the faces @@ -601,7 +632,7 @@ class uvObject: currentObject = bpy.context.scene.objects[edgeList['object']] bm = bmesh.new() bm.from_mesh(currentObject.data) - if hasattr(bm.edges, "ensure_lookup_table"): + if hasattr(bm.edges, "ensure_lookup_table"): bm.edges.ensure_lookup_table() #assume that all the triangulated edges come after the original edges @@ -617,6 +648,27 @@ class uvObject: bm.free() bpy.ops.object.mode_set(mode='EDIT') - #End setting the quads back again------------------------------------------------------------ + #End setting the quads back again------------------------------------------------------------- + + #select the original objects that were selected + for objectName in rename_dict: + if objectName[0] in bpy.context.scene.objects: + current_object = bpy.context.scene.objects[objectName[0]] + current_object.select_set(True) + context.view_layer.objects.active = current_object + + bpy.ops.object.mode_set(mode=startingMode) + + print("Finished Xatlas----------------------------------------") + return {'FINISHED'} + +def transfer_assets(copy, source, destination): + for filename in glob.glob(os.path.join(source, '*.*')): + shutil.copy(filename, destination) - print("Finished Xatlas----------------------------------------") \ No newline at end of file +def transfer_load(): + load_folder = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_load_folder)) + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + transfer_assets(True, load_folder, lightmap_folder) \ No newline at end of file diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 64c23121b9..e99b8bd297 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -112,6 +112,7 @@ class ArmNodeAddInputButton(bpy.types.Operator): """Add a new input socket to the node set by node_index.""" bl_idname = 'arm.node_add_input' bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -135,6 +136,7 @@ class ArmNodeAddInputValueButton(bpy.types.Operator): """Add new input""" bl_idname = 'arm.node_add_input_value' bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -148,6 +150,7 @@ class ArmNodeRemoveInputButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input' bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -163,6 +166,7 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input_value' bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -178,6 +182,7 @@ class ArmNodeAddOutputButton(bpy.types.Operator): """Add a new output socket to the node set by node_index""" bl_idname = 'arm.node_add_output' bl_label = 'Add Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -201,6 +206,7 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator): """Remove last output""" bl_idname = 'arm.node_remove_output' bl_label = 'Remove Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -216,6 +222,7 @@ class ArmNodeAddInputOutputButton(bpy.types.Operator): """Add new input and output""" bl_idname = 'arm.node_add_input_output' bl_label = 'Add Input Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') in_socket_type: StringProperty(name='In Socket Type', default='NodeSocketShader') @@ -246,6 +253,7 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): """Remove last input and output""" bl_idname = 'arm.node_remove_input_output' bl_label = 'Remove Input Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -265,7 +273,7 @@ def execute(self, context): class ArmNodeSearch(bpy.types.Operator): bl_idname = "arm.node_search" bl_label = "Search..." - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "INTERNAL"} bl_property = "item" def get_search_items(self, context): diff --git a/blender/arm/logicnode/input/LN_on_swipe.py b/blender/arm/logicnode/input/LN_on_swipe.py index cfd9b17c32..ed104a4163 100644 --- a/blender/arm/logicnode/input/LN_on_swipe.py +++ b/blender/arm/logicnode/input/LN_on_swipe.py @@ -5,6 +5,7 @@ class NodeAddOutputButton(bpy.types.Operator): """Add 4 States""" bl_idname = 'arm.add_output_4_parameters' bl_label = 'Add 4 States' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') name_format: StringProperty(name='Name Format', default='Output {0}') @@ -31,6 +32,7 @@ class NodeRemoveOutputButton(bpy.types.Operator): """Remove 4 last states""" bl_idname = 'arm.remove_output_4_parameters' bl_label = 'Remove 4 States' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): diff --git a/blender/arm/make.py b/blender/arm/make.py index ffc91e3a79..001eb2d48d 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -433,7 +433,7 @@ def compilation_server_done(): def build_done(): print('Finished in {:0.3f}s'.format(time.time() - profile_time)) if log.num_warnings > 0: - log.print_warn(f'{log.num_warnings} warnings occurred during compilation') + log.print_warn(f'{log.num_warnings} warning{"s" if log.num_warnings > 1 else ""} occurred during compilation') if state.proc_build is None: return result = state.proc_build.poll() diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 8c3dcedd2b..71dac33b2d 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -17,17 +17,59 @@ 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) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index fc128d85d5..1366b66f4a 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -642,8 +642,47 @@ 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: + """Casts a value that is already parsed in a glsl string to another + value in a string. + + vec2 types are not supported (not used in the node editor) and there + is no cast towards int types. If casting from vec3 to vec4, the w + coordinate/alpha channel is filled with a 1. + + If this function is called with invalid parameters, a TypeError is + raised. + """ + if from_type == to_type: + return val + + if from_type in ('int', 'float'): + if to_type in ('int', 'float'): + return val + elif to_type in ('vec2', 'vec3', 'vec4'): + return f'{to_type}({val})' + + elif from_type == 'vec3': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec4': + return f'vec4({val}, 1.0)' + + elif from_type == 'vec4': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec3': + return f'{val}.xyz' + + raise TypeError("Invalid type cast in shader!") def rgb_to_bw(res_var: vec3str) -> floatstr: diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py index 60d86799fc..69072bcdf2 100644 --- a/blender/arm/material/cycles_nodes/nodes_input.py +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -1,6 +1,8 @@ -import bpy from typing import Union +import bpy +import mathutils + import arm.log as log import arm.material.cycles as c import arm.material.cycles_functions as c_functions @@ -10,42 +12,81 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: - # Color - if out_socket == node.outputs[0]: - # Vertex colors only for now - state.con.add_elem('col', 'short4norm') - return 'vcolor' + out_type = 'float' if out_socket.type == 'VALUE' else 'vec3' - # Vector - elif out_socket == node.outputs[1]: - # UV maps only for now - state.con.add_elem('tex', 'short2norm') + if node.attribute_name == 'time': + state.curshader.add_uniform('float time', link='_time') + + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('time', from_type='float', to_type=out_type) + + # UV maps (higher priority) and vertex colors + if node.attribute_type == 'GEOMETRY': + + # Alpha output. Armory doesn't support vertex colors with alpha + # values yet and UV maps don't have an alpha channel + if out_socket == node.outputs[3]: + return '1.0' + + # UV maps mat = c.mat_get_material() mat_users = c.mat_get_material_users() if mat_users is not None and mat in mat_users: mat_user = mat_users[mat][0] - # No UV layers for Curve + # Curves don't have uv layers, so check that first if hasattr(mat_user.data, 'uv_layers'): lays = mat_user.data.uv_layers + # First UV map referenced + if len(lays) > 0 and node.attribute_name == lays[0].name: + state.con.add_elem('tex', 'short2norm') + return c.cast_value('vec3(texCoord.x, 1.0 - texCoord.y, 0.0)', from_type='vec3', to_type=out_type) + # Second UV map referenced - if len(lays) > 1 and node.attribute_name == lays[1].name: + elif len(lays) > 1 and node.attribute_name == lays[1].name: state.con.add_elem('tex1', 'short2norm') - return 'vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)' + return c.cast_value('vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)', from_type='vec3', to_type=out_type) - return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' + # Vertex colors + # TODO: support multiple vertex color sets + state.con.add_elem('col', 'short4norm') + return c.cast_value('vcolor', from_type='vec3', to_type=out_type) - # Fac - else: - if node.attribute_name == 'time': - state.curshader.add_uniform('float time', link='_time') - return 'time' + # Check object properties + # see https://developer.blender.org/rB6fdcca8de6 for reference + mat = c.mat_get_material() + mat_users = c.mat_get_material_users() + if mat_users is not None and mat in mat_users: + # Use first material user for now... + mat_user = mat_users[mat][0] - # Return 0.0 till drivers are implemented - else: - return '0.0' + val = None + # Custom properties first + if node.attribute_name in mat_user: + val = mat_user[node.attribute_name] + # Blender properties + elif hasattr(mat_user, node.attribute_name): + val = getattr(mat_user, node.attribute_name) + + if val is not None: + if isinstance(val, float): + return c.cast_value(str(val), from_type='float', to_type=out_type) + elif isinstance(val, int): + return c.cast_value(str(val), from_type='int', to_type=out_type) + elif isinstance(val, mathutils.Vector) and len(val) <= 4: + out = val.to_4d() + + if out_socket == node.outputs[3]: + return c.to_vec1(out[3]) + return c.cast_value(c.to_vec3(out), from_type='vec3', to_type=out_type) + + # Default values, attribute name did not match + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('0.0', from_type='float', to_type=out_type) def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index d61144549a..4e593bd074 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -41,7 +41,7 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[19]) + c.write_normal(node.inputs[20]) state.out_basecol = c.parse_vector_input(node.inputs[0]) # subsurface = c.parse_vector_input(node.inputs[1]) # subsurface_radius = c.parse_vector_input(node.inputs[2]) @@ -62,11 +62,12 @@ def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: N if node.inputs[17].is_linked or node.inputs[17].default_value[0] != 0.0: state.out_emission = '({0}.x)'.format(c.parse_vector_input(node.inputs[17])) state.emission_found = True - # clearcoar_normal = c.parse_vector_input(node.inputs[20]) - # tangent = c.parse_vector_input(node.inputs[21]) + # clearcoar_normal = c.parse_vector_input(node.inputs[21]) + # tangent = c.parse_vector_input(node.inputs[22]) if state.parse_opacity: - if len(node.inputs) > 20: - state.out_opacity = c.parse_value_input(node.inputs[18]) + if len(node.inputs) > 21: + state.out_opacity = c.parse_value_input(node.inputs[19]) + def parse_bsdfdiffuse(node: bpy.types.ShaderNodeBsdfDiffuse, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index b9f57daf0c..516d555b47 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -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') diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py index 9d6eb1f069..b1567a72c5 100644 --- a/blender/arm/material/mat_utils.py +++ b/blender/arm/material/mat_utils.py @@ -73,7 +73,8 @@ def is_transluc_type(node): node.type == 'BSDF_TRANSPARENT' or \ node.type == 'BSDF_TRANSLUCENT' or \ (node.type == 'GROUP' and node.node_tree.name.startswith('Armory PBR') and (node.inputs[1].is_linked or node.inputs[1].default_value != 1.0)) or \ - (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 20 and (node.inputs[18].is_linked or node.inputs[18].default_value != 1.0)): + (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 20 and (node.inputs[18].is_linked or node.inputs[18].default_value != 1.0)) or \ + (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 21 and (node.inputs[19].is_linked or node.inputs[19].default_value != 1.0)): return True return False diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 999ea29333..be2af6823e 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -119,16 +119,29 @@ def add_constant(self, ctype, name, link=None): 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): @@ -222,10 +235,10 @@ def add_out(self, s): if s not in self.outs: self.outs.append(s) - def add_uniform(self, s, link=None, included=False, top=False): - # prevent duplicates - if s in self.uniforms or s in self.uniforms_top: - return + def add_uniform(self, s, link=None, included=False, top=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] @@ -236,9 +249,15 @@ def add_uniform(self, s, link=None, included=False, top=False): # 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]: @@ -251,9 +270,8 @@ def add_uniform(self, s, link=None, included=False, top=False): if top: if not included and s not in self.uniforms_top: self.uniforms_top.append(s) - else: - if not included and s not in self.uniforms: - self.uniforms.append(s) + elif not included and s not in self.uniforms: + self.uniforms.append(s) def add_const(self, type_str: str, name: str, value_str: str, array_size: int = 0): """ diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 3a44971fd1..12f605fb59 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -8,6 +8,7 @@ import arm.logicnode.replacement import arm.logicnode import arm.props_traits +import arm.ui_icons as ui_icons import arm.utils registered_nodes = [] @@ -58,13 +59,14 @@ class ARM_OT_AddNodeOverride(bpy.types.Operator): bl_idname = "arm.add_node_override" bl_label = "Add Node" bl_property = "type" + bl_options = {'INTERNAL'} type: StringProperty(name="NodeItem type") use_transform: BoolProperty(name="Use Transform") def invoke(self, context, event): bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.type, use_transform=self.use_transform) - return {"FINISHED"} + return {'FINISHED'} @classmethod def description(cls, context, properties): @@ -173,7 +175,7 @@ def draw(self, context): layout.operator('arm.open_node_documentation', icon='HELP') column = layout.column(align=True) column.operator('arm.open_node_python_source', icon='FILE_SCRIPT') - column.operator('arm.open_node_haxe_source', icon_value=arm.props_traits.icons_dict['haxe'].icon_id) + column.operator('arm.open_node_haxe_source', icon_value=ui_icons.get_id("haxe")) class ArmOpenNodeHaxeSource(bpy.types.Operator): @@ -261,7 +263,7 @@ def draw(self, context): setN.ntype = ID class ARMAddVarNode(bpy.types.Operator): - '''Add a linked node of that Variable''' + """Add a linked node of that Variable""" bl_idname = 'arm.add_var_node' bl_label = 'Add Get' bl_options = {'GRAB_CURSOR', 'BLOCKING'} @@ -296,7 +298,7 @@ def execute(self, context): return({'FINISHED'}) class ARMAddSetVarNode(bpy.types.Operator): - '''Add a node to set this Variable''' + """Add a node to set this Variable""" bl_idname = 'arm.add_setvar_node' bl_label = 'Add Set' bl_options = {'GRAB_CURSOR', 'BLOCKING'} diff --git a/blender/arm/props.py b/blender/arm/props.py index 5143c8c618..9c80c53843 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -178,7 +178,7 @@ def init_properties(): ('ErrorsOnly', 'Errors Only', 'Show only errors')], name="Compile Log Parameter", update=assets.invalidate_compiler_cache, default="Summary") - bpy.types.World.arm_project_win_build_cpu = IntProperty(name="Count CPU", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) + bpy.types.World.arm_project_win_build_cpu = IntProperty(name="CPU Count", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) bpy.types.World.arm_project_win_build_open = BoolProperty(name="Open Build Directory", description="Open the build directory after successfully assemble", default=False) bpy.types.World.arm_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) @@ -212,11 +212,11 @@ def init_properties(): bpy.types.World.arm_khafile = PointerProperty(name="Khafile", description="Source appended to khafile.js", update=assets.invalidate_compiler_cache, type=bpy.types.Text) bpy.types.World.arm_texture_quality = FloatProperty(name="Texture Quality", default=1.0, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_sound_quality = FloatProperty(name="Sound Quality", default=0.9, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) - bpy.types.World.arm_minimize = BoolProperty(name="Minimize Data", description="Export scene data in binary", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_minimize = BoolProperty(name="Binary Scene Data", description="Export scene data in binary", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_minify_js = BoolProperty(name="Minify JS", description="Minimize JavaScript output when publishing", default=True) bpy.types.World.arm_optimize_data = BoolProperty(name="Optimize Data", description="Export more efficient geometry and shader data, prolongs build times", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_deinterleaved_buffers = BoolProperty(name="Deinterleaved Buffers", description="Use deinterleaved vertex buffers", default=False, update=assets.invalidate_compiler_cache) - bpy.types.World.arm_export_tangents = BoolProperty(name="Export Tangents", description="Precompute tangents for normal mapping, otherwise computed in shader", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_export_tangents = BoolProperty(name="Precompute Tangents", description="Precompute tangents for normal mapping, otherwise computed in shader", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_batch_meshes = BoolProperty(name="Batch Meshes", description="Group meshes by materials to speed up rendering", default=False, update=assets.invalidate_compiler_cache) bpy.types.World.arm_batch_materials = BoolProperty(name="Batch Materials", description="Marge similar materials into single pipeline state", default=False, update=assets.invalidate_shader_cache) bpy.types.World.arm_stream_scene = BoolProperty(name="Stream Scene", description="Stream scene content", default=False, update=assets.invalidate_compiler_cache) @@ -332,6 +332,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) @@ -398,7 +399,7 @@ def init_properties(): ('destination_color', 'Destination Color', 'Destination Color'), ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], - name='Source', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + name='Source (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) bpy.types.Material.arm_blending_destination_alpha = EnumProperty( items=[('blend_one', 'One', 'One'), ('blend_zero', 'Zero', 'Zero'), @@ -410,14 +411,14 @@ def init_properties(): ('destination_color', 'Destination Color', 'Destination Color'), ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], - name='Destination', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + name='Destination (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) bpy.types.Material.arm_blending_operation_alpha = EnumProperty( items=[('add', 'Add', 'Add'), ('subtract', 'Subtract', 'Subtract'), ('reverse_subtract', 'Reverse Subtract', 'Reverse Subtract'), ('min', 'Min', 'Min'), ('max', 'Max', 'Max')], - name='Operation', default='add', description='Blending operation', update=assets.invalidate_shader_cache) + name='Operation (Alpha)', default='add', description='Blending operation', update=assets.invalidate_shader_cache) # For scene bpy.types.Scene.arm_export = BoolProperty(name="Export", description="Export scene data", default=True) bpy.types.Scene.arm_terrain_textures = StringProperty(name="Textures", description="Set root folder for terrain assets", default="//Bundled/", subtype="DIR_PATH") @@ -442,6 +443,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) diff --git a/blender/arm/props_collision_filter_mask.py b/blender/arm/props_collision_filter_mask.py index 00086ac8ef..b148314add 100644 --- a/blender/arm/props_collision_filter_mask.py +++ b/blender/arm/props_collision_filter_mask.py @@ -1,6 +1,5 @@ import bpy -from bpy.props import * -from bpy.types import Panel + class ARM_PT_RbCollisionFilterMaskPanel(bpy.types.Panel): bl_label = "Collections Filter Mask" @@ -18,13 +17,15 @@ def poll(self, context): def draw(self, context): layout = self.layout - layout.use_property_split = True + layout.use_property_split = False layout.use_property_decorate = False obj = context.object layout.prop(obj, 'arm_rb_collision_filter_mask', text="", expand=True) + def register(): bpy.utils.register_class(ARM_PT_RbCollisionFilterMaskPanel) + def unregister(): bpy.utils.unregister_class(ARM_PT_RbCollisionFilterMaskPanel) diff --git a/blender/arm/props_exporter.py b/blender/arm/props_exporter.py index 2b19a5430a..0dc2f1439d 100644 --- a/blender/arm/props_exporter.py +++ b/blender/arm/props_exporter.py @@ -358,7 +358,7 @@ def draw(self, context): layout.operator("arm.exporter_gpuprofile") class ArmoryExporterOpenFolderButton(bpy.types.Operator): - '''Open published folder''' + """Open published folder""" bl_idname = 'arm.exporter_open_folder' bl_label = 'Open Folder' diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index ef1b2e8634..44bfdca929 100755 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -34,29 +34,29 @@ class ArmLodListItem(bpy.types.PropertyGroup): class ARM_UL_LodList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # We could write some code to decide which icon to use here... - custom_icon = 'OBJECT_DATAMODE' + layout.use_property_split = False - # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.prop(item, "enabled_prop") + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") name = item.name if name == '': name = 'None' - row = layout.row() - row.label(text=name, icon=custom_icon) + row.label(text=name, icon='OBJECT_DATAMODE') col = row.column() col.alignment = 'RIGHT' col.label(text="{:.2f}".format(item.screen_size_prop)) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' - layout.label(text="", icon = custom_icon) + layout.label(text="", icon='OBJECT_DATAMODE') class ArmLodListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "arm_lodlist.new_item" bl_label = "Add a new item" + bl_options = {'UNDO'} def execute(self, context): mdata = bpy.context.object.data @@ -69,10 +69,13 @@ class ArmLodListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "arm_lodlist.delete_item" bl_label = "Deletes an item" + bl_options = {'INTERNAL', 'UNDO'} @classmethod - def poll(self, context): + def poll(cls, context): """ Enable if there's something in the list """ + if bpy.context.object is None: + return False mdata = bpy.context.object.data return len(mdata.arm_lodlist) > 0 @@ -98,6 +101,7 @@ class ArmLodListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "arm_lodlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL', 'UNDO'} direction: EnumProperty( items=( ('UP', 'Up', ""), diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py index eeea1b4f13..1f7d53b6db 100644 --- a/blender/arm/props_tilesheet.py +++ b/blender/arm/props_tilesheet.py @@ -1,30 +1,26 @@ -import shutil import bpy -import os -import json -from bpy.types import Menu, Panel, UIList from bpy.props import * class ArmTilesheetActionListItem(bpy.types.PropertyGroup): name: StringProperty( - name="Name", - description="A name for this item", - default="Untitled") + name="Name", + description="A name for this item", + default="Untitled") start_prop: IntProperty( - name="Start", - description="A name for this item", - default=0) + name="Start", + description="A name for this item", + default=0) end_prop: IntProperty( - name="End", - description="A name for this item", - default=0) + name="End", + description="A name for this item", + default=0) loop_prop: BoolProperty( - name="Loop", - description="A name for this item", - default=True) + name="Loop", + description="A name for this item", + default=True) class ARM_UL_TilesheetActionList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -52,14 +48,16 @@ def execute(self, context): return{'FINISHED'} class ArmTilesheetActionListDeleteItem(bpy.types.Operator): - # Delete the selected item from the list + """Delete the selected item from the list""" bl_idname = "arm_tilesheetactionlist.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): - """ Enable if there's something in the list """ + """Enable if there's something in the list""" wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] return len(trait.arm_tilesheetactionlist) > 0 @@ -78,18 +76,23 @@ def execute(self, context): return{'FINISHED'} class ArmTilesheetActionListMoveItem(bpy.types.Operator): - # Move an item in the list + """Move an item in the list""" bl_idname = "arm_tilesheetactionlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + direction: EnumProperty( - items=( - ('UP', 'Up', ""), - ('DOWN', 'Down', ""),)) + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) @classmethod def poll(self, context): - """ Enable if there's something in the list. """ + """Enable if there's something in the list""" wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] return len(trait.arm_tilesheetactionlist) > 0 @@ -129,27 +132,27 @@ def execute(self, context): class ArmTilesheetListItem(bpy.types.PropertyGroup): name: StringProperty( - name="Name", - description="A name for this item", - default="Untitled") + name="Name", + description="A name for this item", + default="Untitled") tilesx_prop: IntProperty( - name="Tiles X", - description="A name for this item", - default=0) + name="Tiles X", + description="A name for this item", + default=0) tilesy_prop: IntProperty( - name="Tiles Y", - description="A name for this item", - default=0) + name="Tiles Y", + description="A name for this item", + default=0) framerate_prop: FloatProperty( - name="Frame Rate", - description="A name for this item", - default=4.0) + name="Frame Rate", + description="A name for this item", + default=4.0) arm_tilesheetactionlist: CollectionProperty(type=ArmTilesheetActionListItem) - arm_tilesheetactionlist_index: IntProperty(name="Index for my_list", default=0) + arm_tilesheetactionlist_index: IntProperty(name="Index for arm_tilesheetactionlist", default=0) class ARM_UL_TilesheetList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -162,10 +165,10 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' - layout.label(text="", icon = custom_icon) + layout.label(text="", icon=custom_icon) class ArmTilesheetListNewItem(bpy.types.Operator): - # Add a new item to the list + """Add a new item to the list""" bl_idname = "arm_tilesheetlist.new_item" bl_label = "Add a new item" @@ -176,7 +179,7 @@ def execute(self, context): return{'FINISHED'} class ArmTilesheetListDeleteItem(bpy.types.Operator): - # Delete the selected item from the list + """Delete the selected item from the list""" bl_idname = "arm_tilesheetlist.delete_item" bl_label = "Deletes an item" @@ -200,13 +203,16 @@ def execute(self, context): return{'FINISHED'} class ArmTilesheetListMoveItem(bpy.types.Operator): - # Move an item in the list + """Move an item in the list""" bl_idname = "arm_tilesheetlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + direction: EnumProperty( - items=( - ('UP', 'Up', ""), - ('DOWN', 'Down', ""),)) + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) @classmethod def poll(self, context): @@ -247,7 +253,6 @@ def execute(self, context): return{'FINISHED'} def register(): - bpy.utils.register_class(ArmTilesheetActionListItem) bpy.utils.register_class(ARM_UL_TilesheetActionList) bpy.utils.register_class(ArmTilesheetActionListNewItem) @@ -261,7 +266,7 @@ def register(): bpy.utils.register_class(ArmTilesheetListMoveItem) bpy.types.World.arm_tilesheetlist = CollectionProperty(type=ArmTilesheetListItem) - bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for my_list", default=0) + bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for arm_tilesheetlist", default=0) def unregister(): bpy.utils.unregister_class(ArmTilesheetListItem) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 7f69612e16..eba08da84a 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -2,6 +2,7 @@ import os import shutil import subprocess +from typing import Union import webbrowser from bpy.types import NodeTree @@ -10,10 +11,26 @@ import arm.make as make from arm.props_traits_props import * import arm.proxy as proxy +import arm.ui_icons as ui_icons import arm.utils import arm.write_data as write_data -icons_dict: bpy.utils.previews.ImagePreviewCollection +ICON_HAXE = ui_icons.get_id('haxe') +ICON_NODES = 'NODETREE' +ICON_CANVAS = 'NODE_COMPOSITING' +ICON_BUNDLED = ui_icons.get_id('bundle') +ICON_WASM = ui_icons.get_id('wasm') + +# Pay attention to the ID number parameter for backward compatibility! +# This is important if the enum is reordered or the string identifier +# is changed as the number is what's stored in the blend file +PROP_TYPES_ENUM = [ + ('Haxe Script', 'Haxe', 'Haxe script', ICON_HAXE, 0), + ('Logic Nodes', 'Nodes', 'Logic nodes (visual scripting)', ICON_NODES, 4), + ('UI Canvas', 'UI', 'User interface', ICON_CANVAS, 2), + ('Bundled Script', 'Bundled', 'Premade script with common functionality', ICON_BUNDLED, 3), + ('WebAssembly', 'Wasm', 'WebAssembly', ICON_WASM, 1) +] def trigger_recompile(self, context): @@ -61,78 +78,81 @@ def update_trait_group(self, context): pass class ArmTraitListItem(bpy.types.PropertyGroup): + def poll_node_trees(self, tree: NodeTree): + """Ensure that only logic node trees show up as node traits""" + return tree.bl_idname == 'ArmLogicTreeType' + name: StringProperty(name="Name", description="A name for this item", default="") enabled_prop: BoolProperty(name="", description="A name for this item", default=True, update=trigger_recompile) is_object: BoolProperty(name="", default=True) fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False) - type_prop: EnumProperty( - items = [('Haxe Script', 'Haxe', 'Haxe Script'), - ('WebAssembly', 'Wasm', 'WebAssembly'), - ('UI Canvas', 'UI', 'UI Canvas'), - ('Bundled Script', 'Bundled', 'Bundled Script'), - ('Logic Nodes', 'Nodes', 'Logic Nodes') - ], - name = "Type") + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group) canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group) webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group) - node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group) + node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, poll=poll_node_trees) arm_traitpropslist: CollectionProperty(type=ArmTraitPropListItem) arm_traitpropslist_index: IntProperty(name="Index for my_list", default=0) arm_traitpropswarnings: CollectionProperty(type=ArmTraitPropWarning) class ARM_UL_TraitList(bpy.types.UIList): + """List of traits.""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.use_property_split = False + custom_icon = "NONE" custom_icon_value = 0 if item.type_prop == "Haxe Script": - custom_icon_value = icons_dict["haxe"].icon_id + custom_icon_value = ui_icons.get_id("haxe") elif item.type_prop == "WebAssembly": - custom_icon_value = icons_dict["wasm"].icon_id + custom_icon_value = ui_icons.get_id("wasm") elif item.type_prop == "UI Canvas": - custom_icon = "OBJECT_DATAMODE" + custom_icon = "NODE_COMPOSITING" elif item.type_prop == "Bundled Script": - custom_icon_value = icons_dict["bundle"].icon_id + custom_icon_value = ui_icons.get_id("bundle") elif item.type_prop == "Logic Nodes": custom_icon = 'NODETREE' - # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.prop(item, "enabled_prop") + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") # Display " " for props without a name to right-align the # fake_user button - layout.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) + row.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) - layout.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") + row = layout.row(align=True) + row.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") class ArmTraitListNewItem(bpy.types.Operator): bl_idname = "arm_traitlist.new_item" - bl_label = "New Trait Item" + bl_label = "Add Trait" bl_description = "Add a new trait item to the list" - is_object: BoolProperty(name="Object Trait", description="Whether this is an object or scene trait", default=False) - type_prop: EnumProperty( - items = [('Haxe Script', 'Haxe', 'Haxe Script'), - ('WebAssembly', 'Wasm', 'WebAssembly'), - ('UI Canvas', 'UI', 'UI Canvas'), - ('Bundled Script', 'Bundled', 'Bundled Script'), - ('Logic Nodes', 'Nodes', 'Logic Nodes') - ], - name = "Type") + is_object: BoolProperty(name="Is Object Trait", description="Whether this trait belongs to an object or a scene", default=False) + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) + + # Show more options when invoked from the operator search menu + invoked_by_search: BoolProperty(name="", default=True) def invoke(self, context, event): wm = context.window_manager - return wm.invoke_props_dialog(self) + return wm.invoke_props_dialog(self, width=400) def draw(self, context): layout = self.layout - # Todo: show is_object property when called from operator search menu - # layout.prop(self, "is_object") - layout.prop(self, "type_prop", expand=True) + + if self.invoked_by_search: + row = layout.row() + row.prop(self, "is_object") + + row = layout.row() + row.scale_y = 1.3 + row.prop(self, "type_prop", expand=True) def execute(self, context): if self.is_object: @@ -149,7 +169,7 @@ def execute(self, context): class ArmTraitListDeleteItem(bpy.types.Operator): """Delete the selected item from the list""" bl_idname = "arm_traitlist.delete_item" - bl_label = "Deletes an item" + bl_label = "Remove Trait" bl_options = {'INTERNAL'} is_object: BoolProperty(name="", description="A name for this item", default=False) @@ -582,8 +602,6 @@ def execute(self, context): self.canvas_name = self.canvas_name.replace(' ', '') write_data.write_canvasjson(self.canvas_name) arm.utils.fetch_script_names() - # Todo: create new trait item when called from operator search - # menu, then remove 'INTERNAL' from bl_options item = obj.arm_traitlist[obj.arm_traitlist_index] item.canvas_name_prop = self.canvas_name return {'FINISHED'} @@ -635,11 +653,8 @@ class ARM_PT_TraitPanel(bpy.types.Panel): bl_context = "object" def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False obj = bpy.context.object - draw_traits(layout, obj, is_object=True) + draw_traits_panel(self.layout, obj, is_object=True) class ARM_PT_SceneTraitPanel(bpy.types.Panel): bl_label = "Armory Scene Traits" @@ -648,11 +663,8 @@ class ARM_PT_SceneTraitPanel(bpy.types.Panel): bl_context = "scene" def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False obj = bpy.context.scene - draw_traits(layout, obj, is_object=False) + draw_traits_panel(self.layout, obj, is_object=False) class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): bl_label = 'Copy Traits from Active Object' @@ -728,21 +740,28 @@ def invoke(self, context, event): return {'INTERFACE'} -def draw_traits(layout, obj, is_object): - rows = 2 + +def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, bpy.types.Scene], + is_object: bool) -> None: + layout.use_property_split = True + layout.use_property_decorate = False + + # Make the list bigger when there are a few traits + num_rows = 2 if len(obj.arm_traitlist) > 1: - rows = 4 + num_rows = 4 row = layout.row() - row.template_list("ARM_UL_TraitList", "The_List", obj, "arm_traitlist", obj, "arm_traitlist_index", rows=rows) + row.template_list("ARM_UL_TraitList", "The_List", obj, "arm_traitlist", obj, "arm_traitlist_index", rows=num_rows) col = row.column(align=True) op = col.operator("arm_traitlist.new_item", icon='ADD', text="") + op.invoked_by_search = False op.is_object = is_object if is_object: - op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="")#.all = False + op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="") else: - op = col.operator("arm_traitlist.delete_item_scene", icon='REMOVE', text="")#.all = False + op = col.operator("arm_traitlist.delete_item_scene", icon='REMOVE', text="") op.is_object = is_object if len(obj.arm_traitlist) > 1: @@ -754,35 +773,29 @@ def draw_traits(layout, obj, is_object): op.direction = 'DOWN' op.is_object = is_object + # Draw trait specific content if obj.arm_traitlist_index >= 0 and len(obj.arm_traitlist) > 0: item = obj.arm_traitlist[obj.arm_traitlist_index] + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.scale_y = 1.2 + if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': if item.type_prop == 'Haxe Script': - row = layout.row(align=True) - row.alignment = 'EXPAND' + row.operator("arm.new_script", icon="FILE_NEW").is_object = is_object column = row.column(align=True) - column.alignment = 'EXPAND' - if item.class_name_prop == '': - column.enabled = False - op = column.operator("arm.edit_script", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.new_script") - op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") - else: # Bundled + column.enabled = item.class_name_prop != '' + column.operator("arm.edit_script", icon="FILE_SCRIPT").is_object = is_object + + # Bundled scripts + else: if item.class_name_prop == 'NavMesh': - row = layout.row(align=True) - row.alignment = 'EXPAND' - op = layout.operator("arm.generate_navmesh") - row = layout.row(align=True) - row.alignment = 'EXPAND' - column = row.column(align=True) - column.alignment = 'EXPAND' - if not item.class_name_prop == 'NavMesh': - op = column.operator("arm.edit_bundled_script", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") + row.operator("arm.generate_navmesh", icon="UV_VERTEXSEL") + else: + row.operator("arm.edit_bundled_script", icon="FILE_SCRIPT").is_object = is_object + + row.operator("arm.refresh_scripts", text="Refresh", icon="FILE_REFRESH") # Default props item.name = item.class_name_prop @@ -797,86 +810,56 @@ def draw_traits(layout, obj, is_object): elif item.type_prop == 'WebAssembly': item.name = item.webassembly_prop + + row.operator("arm.new_wasm", icon="FILE_NEW") + row.operator("arm.refresh_scripts", text="Refresh", icon="FILE_REFRESH") + row = layout.row() row.prop_search(item, "webassembly_prop", bpy.data.worlds['Arm'], "arm_wasm_list", text="Module") - row = layout.row(align=True) - row.alignment = 'EXPAND' - column = row.column(align=True) - column.alignment = 'EXPAND' - if item.class_name_prop == '': - column.enabled = False - # op = column.operator("arm.edit_script", icon="FILE_SCRIPT") - # op.is_object = is_object - op = row.operator("arm.new_wasm") - # op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") elif item.type_prop == 'UI Canvas': item.name = item.canvas_name_prop - row = layout.row(align=True) - row.alignment = 'EXPAND' + row.operator("arm.new_canvas", icon="FILE_NEW").is_object = is_object column = row.column(align=True) - column.alignment = 'EXPAND' - if item.canvas_name_prop == '': - column.enabled = False - op = column.operator("arm.edit_canvas", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.new_canvas") - op.is_object = is_object - op = row.operator("arm.refresh_canvas_list", text="Refresh") + column.enabled = item.canvas_name_prop != '' + column.operator("arm.edit_canvas", icon="NODE_COMPOSITING").is_object = is_object + row.operator("arm.refresh_canvas_list", text="Refresh", icon="FILE_REFRESH") row = layout.row() row.prop_search(item, "canvas_name_prop", bpy.data.worlds['Arm'], "arm_canvas_list", text="Canvas") elif item.type_prop == 'Logic Nodes': - # Row for buttons - row = layout.row(align=True) - row.alignment = 'EXPAND' - # New - column = row.column(align=True) - column.alignment = 'EXPAND' - op = column.operator("arm.new_treenode", text="New Node Tree", icon="ADD") - op.is_object = is_object - # At least one check is active Logic Node Editor - is_check_logic_node_editor = False - context_screen = bpy.context.screen - # Loop for all spaces - if context_screen is not None: - areas = context_screen.areas + # Check if there is at least one active Logic Node Editor + is_editor_active = False + if bpy.context.screen is not None: + areas = bpy.context.screen.areas for area in areas: for space in area.spaces: if space.type == 'NODE_EDITOR': if space.tree_type == 'ArmLogicTreeType' and space.node_tree is not None: - is_check_logic_node_editor = True + is_editor_active = True break - if is_check_logic_node_editor: + if is_editor_active: break - # Edit + + row.operator("arm.new_treenode", text="New Tree", icon="ADD").is_object = is_object + column = row.column(align=True) - column.alignment = 'EXPAND' - if item.node_tree_prop is None: - column.enabled = False - else: - column.enabled = is_check_logic_node_editor - op = column.operator("arm.edit_treenode", text="Edit Node Tree", icon="NODETREE") - op.is_object = is_object - # Get from Node Tree Editor + column.enabled = is_editor_active and item.node_tree_prop is not None + column.operator("arm.edit_treenode", text="Edit Tree", icon="NODETREE").is_object = is_object + column = row.column(align=True) - column.alignment = 'EXPAND' - if item is None: - column.enabled = False - else: - column.enabled = is_check_logic_node_editor - op = column.operator("arm.get_treenode", text="From Node Editor", icon="IMPORT") - op.is_object = is_object + column.enabled = is_editor_active and item is not None + column.operator("arm.get_treenode", text="From Editor", icon="IMPORT").is_object = is_object - # Row for search row = layout.row() row.prop_search(item, "node_tree_prop", bpy.data, "node_groups", text="Tree") + # ===================== + # Draw trait properties if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': - # Props + if item.arm_traitpropslist: layout.label(text="Trait Properties:") if item.arm_traitpropswarnings: @@ -887,13 +870,12 @@ def draw_traits(layout, obj, is_object): for warning in item.arm_traitpropswarnings: col.label(text=f'"{warning.propName}": {warning.warning}') - propsrow = layout.row() propsrows = max(len(item.arm_traitpropslist), 6) row = layout.row() row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + def register(): - global icons_dict bpy.utils.register_class(ArmTraitListItem) bpy.utils.register_class(ARM_UL_TraitList) bpy.utils.register_class(ArmTraitListNewItem) @@ -921,14 +903,8 @@ def register(): bpy.types.Scene.arm_traitlist = CollectionProperty(type=ArmTraitListItem) bpy.types.Scene.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0) - icons_dict = bpy.utils.previews.new() - icons_dir = os.path.join(os.path.dirname(__file__), "custom_icons") - icons_dict.load("haxe", os.path.join(icons_dir, "haxe.png"), 'IMAGE') - icons_dict.load("wasm", os.path.join(icons_dir, "wasm.png"), 'IMAGE') - icons_dict.load("bundle", os.path.join(icons_dir, "bundle.png"), 'IMAGE') def unregister(): - global icons_dict bpy.utils.unregister_class(ARM_OT_CopyTraitsFromActive) bpy.utils.unregister_class(ArmTraitListItem) bpy.utils.unregister_class(ARM_UL_TraitList) @@ -950,4 +926,3 @@ def unregister(): bpy.utils.unregister_class(ArmRefreshCanvasListButton) bpy.utils.unregister_class(ARM_PT_TraitPanel) bpy.utils.unregister_class(ARM_PT_SceneTraitPanel) - bpy.utils.previews.remove(icons_dict) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 9ed79befe3..00b139404a 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -7,6 +7,7 @@ import arm.api import arm.assets as assets +from arm.exporter import ArmoryExporter import arm.log as log import arm.logicnode.replacement import arm.make as make @@ -16,16 +17,17 @@ import arm.props_traits import arm.nodes_logic import arm.proxy +import arm.ui_icons as ui_icons import arm.utils from arm.lightmapper.utility import icon from arm.lightmapper.properties.denoiser import oidn, optix +from arm.lightmapper.panels import scene import importlib -from arm.exporter import ArmoryExporter -# Menu in object region class ARM_PT_ObjectPropsPanel(bpy.types.Panel): + """Menu in object region.""" bl_label = "Armory Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -40,12 +42,13 @@ def draw(self, context): if obj == None: return - layout.prop(obj, 'arm_export') + col = layout.column() + col.prop(obj, 'arm_export') if not obj.arm_export: return - layout.prop(obj, 'arm_spawn') - layout.prop(obj, 'arm_mobile') - layout.prop(obj, 'arm_animation_enabled') + col.prop(obj, 'arm_spawn') + col.prop(obj, 'arm_mobile') + col.prop(obj, 'arm_animation_enabled') if obj.type == 'MESH': layout.prop(obj, 'arm_instanced') @@ -73,9 +76,18 @@ def draw(self, context): if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") row = layout.row() if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": @@ -93,7 +105,6 @@ def draw(self, context): row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") row = layout.row() - if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: row = layout.row() @@ -257,6 +268,7 @@ def draw(self, context): 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') @@ -283,7 +295,7 @@ def draw(self, context): row.prop(scene, 'arm_export') class InvalidateCacheButton(bpy.types.Operator): - '''Delete cached mesh data''' + """Delete cached mesh data""" bl_idname = "arm.invalidate_cache" bl_label = "Invalidate Cache" @@ -292,7 +304,7 @@ def execute(self, context): return{'FINISHED'} class InvalidateMaterialCacheButton(bpy.types.Operator): - '''Delete cached material data''' + """Delete cached material data""" bl_idname = "arm.invalidate_material_cache" bl_label = "Invalidate Cache" @@ -525,7 +537,7 @@ class ARM_PT_MaterialBlendingPropsPanel(bpy.types.Panel): bl_parent_id = "ARM_PT_MaterialPropsPanel" def draw_header(self, context): - if context.material == None: + if context.material is None: return self.layout.prop(context.material, 'arm_blending', text="") @@ -534,16 +546,18 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False mat = bpy.context.material - if mat == None: + if mat is None: return flow = layout.grid_flow() flow.enabled = mat.arm_blending - col = flow.column() + col = flow.column(align=True) col.prop(mat, 'arm_blending_source') col.prop(mat, 'arm_blending_destination') col.prop(mat, 'arm_blending_operation') - col = flow.column() + flow.separator() + + col = flow.column(align=True) col.prop(mat, 'arm_blending_source_alpha') col.prop(mat, 'arm_blending_destination_alpha') col.prop(mat, 'arm_blending_operation_alpha') @@ -559,20 +573,25 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] + row = layout.row(align=True) row.alignment = 'EXPAND' + row.scale_y = 1.3 if state.proc_play is None and state.proc_build is None: row.operator("arm.play", icon="PLAY") else: row.operator("arm.stop", icon="MESH_PLANE") - row.operator("arm.clean_menu") - layout.prop(wrd, 'arm_runtime') - layout.prop(wrd, 'arm_play_camera') - layout.prop(wrd, 'arm_play_scene') + row.operator("arm.clean_menu", icon="BRUSH_DATA") + + box = layout.box() + box.prop(wrd, 'arm_runtime') + box.prop(wrd, 'arm_play_camera') + box.prop(wrd, 'arm_play_scene') if log.num_warnings > 0: box = layout.box() - # Less spacing between lines + box.alert = True + col = box.column(align=True) warnings = 'warnings' if log.num_warnings > 1 else 'warning' col.label(text=f'{log.num_warnings} {warnings} occurred during compilation!', icon='ERROR') @@ -583,6 +602,7 @@ def draw(self, context): if log.num_errors > 0: box = layout.box() + box.alert = True # Less spacing between lines col = box.column(align=True) errors = 'errors' if log.num_errors > 1 else 'error' @@ -602,12 +622,14 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] + row = layout.row(align=True) row.alignment = 'EXPAND' - row.operator("arm.build_project") + row.scale_y = 1.3 + row.enabled = wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 + row.operator("arm.build_project", icon="MOD_BUILD") # row.operator("arm.patch_project") row.operator("arm.publish_project", icon="EXPORT") - row.enabled = wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 rows = 2 if len(wrd.arm_exporterlist) > 1: @@ -640,59 +662,78 @@ def draw(self, context): box.prop_search(item, 'arm_project_scene', bpy.data, 'scenes', text='Scene') layout.separator() - col = layout.column() + col = layout.column(align=True) col.prop(wrd, 'arm_project_name') col.prop(wrd, 'arm_project_package') col.prop(wrd, 'arm_project_bundle') + + col = layout.column(align=True) col.prop(wrd, 'arm_project_version') col.prop(wrd, 'arm_project_version_autoinc') + + col = layout.column() col.prop(wrd, 'arm_project_icon') + + col = layout.column(heading='Code Output') col.prop(wrd, 'arm_dce') col.prop(wrd, 'arm_compiler_inline') col.prop(wrd, 'arm_minify_js') + + col = layout.column(heading='Data') + col.prop(wrd, 'arm_minimize') col.prop(wrd, 'arm_optimize_data') col.prop(wrd, 'arm_asset_compression') col.prop(wrd, 'arm_single_data_file') -class ARM_PT_ArmoryExporterAndroidSettingsPanel(bpy.types.Panel): - bl_label = "Android Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" + +class ExporterTargetSettingsMixin: + """Mixin for common exporter setting subpanel functionality. + + Panels that inherit from this mixin need to have a arm_target + variable for polling.""" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'render' + bl_parent_id = 'ARM_PT_ArmoryExporterPanel' + + # Override this in sub classes + arm_panel = '' @classmethod def poll(cls, context): wrd = bpy.data.worlds['Arm'] if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'android-hl' - else: - return False + return item.arm_project_target == cls.arm_target + return False + + def draw_header(self, context): + self.layout.label(text='', icon='SETTINGS') + + +class ARM_PT_ArmoryExporterAndroidSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "Android Settings" + arm_target = 'android-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='Android Settings', icon='SETTINGS') - row = layout.row() - row.prop(wrd, 'arm_winorient') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_compile') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_min') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_target') + + col = layout.column() + col.prop(wrd, 'arm_winorient') + col.prop(wrd, 'arm_project_android_sdk_compile') + col.prop(wrd, 'arm_project_android_sdk_min') + col.prop(wrd, 'arm_project_android_sdk_target') + class ARM_PT_ArmoryExporterAndroidPermissionsPanel(bpy.types.Panel): bl_label = "Permissions" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = { 'DEFAULT_CLOSED' } + bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" def draw(self, context): @@ -750,7 +791,7 @@ class ARM_PT_ArmoryExporterAndroidBuildAPKPanel(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = { 'DEFAULT_CLOSED'} + bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" def draw(self, context): @@ -758,109 +799,84 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - row = layout.row() - row.prop(wrd, 'arm_project_android_build_apk') path = arm.utils.get_android_sdk_root_path() + + col = layout.column() + + row = col.row() row.enabled = len(path) > 0 - row = layout.row() - row.prop(wrd, 'arm_project_android_rename_apk') + row.prop(wrd, 'arm_project_android_build_apk') + + row = col.row() row.enabled = wrd.arm_project_android_build_apk - row = layout.row() + row.prop(wrd, 'arm_project_android_rename_apk') + row = col.row() + row.enabled = wrd.arm_project_android_build_apk and len(arm.utils.get_android_apk_copy_path()) > 0 row.prop(wrd, 'arm_project_android_copy_apk') - row.enabled = (wrd.arm_project_android_build_apk) and (len(arm.utils.get_android_apk_copy_path()) > 0) - row = layout.row() + + row = col.row(align=True) row.prop(wrd, 'arm_project_android_list_avd') - col = row.column(align=True) - col.operator('arm.update_list_android_emulator', text='', icon='FILE_REFRESH') - col.enabled = len(path) > 0 - col = row.column(align=True) - col.operator('arm.run_android_emulator', text='', icon='PLAY') - col.enabled = len(path) > 0 and len(arm.utils.get_android_emulator_name()) > 0 - row = layout.row() - row.prop(wrd, 'arm_project_android_run_avd') + sub = row.column(align=True) + sub.enabled = len(path) > 0 + sub.operator('arm.update_list_android_emulator', text='', icon='FILE_REFRESH') + sub = row.column(align=True) + sub.enabled = len(path) > 0 and len(arm.utils.get_android_emulator_name()) > 0 + sub.operator('arm.run_android_emulator', text='', icon='PLAY') + + row = col.row() row.enabled = arm.utils.get_project_android_build_apk() and len(arm.utils.get_android_emulator_name()) > 0 + row.prop(wrd, 'arm_project_android_run_avd') -class ARM_PT_ArmoryExporterHTML5SettingsPanel(bpy.types.Panel): - bl_label = "HTML5 Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" - @classmethod - def poll(cls, context): - wrd = bpy.data.worlds['Arm'] - if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): - item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'html5' - else: - return False +class ARM_PT_ArmoryExporterHTML5SettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "HTML5 Settings" + arm_target = 'html5' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='HTML5 Settings', icon='SETTINGS') - row = layout.row() - row.prop(wrd, 'arm_project_html5_popupmenu_in_browser') - row = layout.row() - row.prop(wrd, 'arm_project_html5_copy') + + col = layout.column() + col.prop(wrd, 'arm_project_html5_popupmenu_in_browser') + row = col.row() row.enabled = len(arm.utils.get_html5_copy_path()) > 0 - row = layout.row() + row.prop(wrd, 'arm_project_html5_copy') + row = col.row() + row.enabled = len(arm.utils.get_html5_copy_path()) > 0 and wrd.arm_project_html5_copy and len(arm.utils.get_link_web_server()) > 0 row.prop(wrd, 'arm_project_html5_start_browser') - row.enabled = (len(arm.utils.get_html5_copy_path()) > 0) and (wrd.arm_project_html5_copy) and (len(arm.utils.get_link_web_server()) > 0) -class ARM_PT_ArmoryExporterWindowsSettingsPanel(bpy.types.Panel): - bl_label = "Windows Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" - @classmethod - def poll(cls, context): - wrd = bpy.data.worlds['Arm'] - if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): - item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'windows-hl' - else: - return False +class ARM_PT_ArmoryExporterWindowsSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "Windows Settings" + arm_target = 'windows-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='Windows Settings', icon='SETTINGS') - row = layout.row() + + col = layout.column() + row = col.row(align=True) row.prop(wrd, 'arm_project_win_list_vs') - col = row.column(align=True) - col.operator('arm.update_list_installed_vs', text='', icon='FILE_REFRESH') - col.enabled = arm.utils.get_os_is_windows() - row = layout.row() - row.prop(wrd, 'arm_project_win_build') + sub = row.column(align=True) + sub.enabled = arm.utils.get_os_is_windows() + sub.operator('arm.update_list_installed_vs', text='', icon='FILE_REFRESH') + + row = col.row() row.enabled = arm.utils.get_os_is_windows() - is_enable = arm.utils.get_os_is_windows() and wrd.arm_project_win_build != '0' and wrd.arm_project_win_build != '1' - row = layout.row() - row.prop(wrd, 'arm_project_win_build_mode') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_arch') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_log') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_cpu') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_open') - row.enabled = is_enable + row.prop(wrd, 'arm_project_win_build', text='After Publish') + layout.separator() + + col = layout.column() + col.enabled = arm.utils.get_os_is_windows() and wrd.arm_project_win_build != '0' and wrd.arm_project_win_build != '1' + col.prop(wrd, 'arm_project_win_build_mode') + col.prop(wrd, 'arm_project_win_build_arch') + col.prop(wrd, 'arm_project_win_build_log') + col.prop(wrd, 'arm_project_win_build_cpu') + col.prop(wrd, 'arm_project_win_build_open') class ARM_PT_ArmoryProjectPanel(bpy.types.Panel): bl_label = "Armory Project" @@ -889,19 +905,26 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - layout.prop(wrd, 'arm_verbose_output') - layout.prop(wrd, 'arm_cache_build') - layout.prop(wrd, 'arm_live_patch') - layout.prop(wrd, 'arm_stream_scene') - layout.prop(wrd, 'arm_batch_meshes') - layout.prop(wrd, 'arm_batch_materials') - layout.prop(wrd, 'arm_write_config') - layout.prop(wrd, 'arm_minimize') - layout.prop(wrd, 'arm_deinterleaved_buffers') - layout.prop(wrd, 'arm_export_tangents') - layout.prop(wrd, 'arm_loadscreen') - layout.prop(wrd, 'arm_texture_quality') - layout.prop(wrd, 'arm_sound_quality') + + col = layout.column(heading='Debug') + col.prop(wrd, 'arm_verbose_output') + col.prop(wrd, 'arm_cache_build') + + col = layout.column(heading='Runtime') + col.prop(wrd, 'arm_live_patch') + col.prop(wrd, 'arm_stream_scene') + col.prop(wrd, 'arm_loadscreen') + col.prop(wrd, 'arm_write_config') + + col = layout.column(heading='Renderer') + col.prop(wrd, 'arm_batch_meshes') + col.prop(wrd, 'arm_batch_materials') + col.prop(wrd, 'arm_deinterleaved_buffers') + col.prop(wrd, 'arm_export_tangents') + + col = layout.column(heading='Quality') + col.prop(wrd, 'arm_texture_quality') + col.prop(wrd, 'arm_sound_quality') class ARM_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_label = "Debug Console" @@ -911,23 +934,20 @@ class ARM_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ProjectFlagsPanel" + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + self.layout.prop(wrd, 'arm_debug_console', text='') + def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - row = layout.row() - row.enabled = wrd.arm_ui != 'Disabled' - row.prop(wrd, 'arm_debug_console') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_position') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_scale') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_visible') + col = layout.column() + col.enabled = wrd.arm_debug_console + col.prop(wrd, 'arm_debug_console_position') + col.prop(wrd, 'arm_debug_console_scale') + col.prop(wrd, 'arm_debug_console_visible') class ARM_PT_ProjectWindowPanel(bpy.types.Panel): bl_label = "Window" @@ -943,11 +963,15 @@ def draw(self, context): layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] layout.prop(wrd, 'arm_winmode') - layout.prop(wrd, 'arm_winresize') - col = layout.column() - col.enabled = wrd.arm_winresize - col.prop(wrd, 'arm_winmaximize') - layout.prop(wrd, 'arm_winminimize') + + col = layout.column(align=True) + col.prop(wrd, 'arm_winresize') + sub = col.column() + sub.enabled = wrd.arm_winresize + sub.prop(wrd, 'arm_winmaximize') + col.enabled = True + col.prop(wrd, 'arm_winminimize') + layout.prop(wrd, 'arm_vsync') class ARM_PT_ProjectModulesPanel(bpy.types.Panel): @@ -997,7 +1021,7 @@ def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} @@ -1036,7 +1060,7 @@ class ArmoryBuildProjectButton(bpy.types.Operator): def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} @@ -1076,7 +1100,7 @@ class ArmoryPublishProjectButton(bpy.types.Operator): def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} @@ -1268,17 +1292,23 @@ def draw(self, context): layout.prop(rpdat, 'arm_tess_shadows_outer') layout.prop(rpdat, 'arm_particles') - layout.prop(rpdat, 'arm_skin') - row = layout.row() - row.enabled = rpdat.arm_skin == 'On' - row.prop(rpdat, 'arm_skin_max_bones_auto') - row = layout.row() + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, 'arm_skin') + col = col.column() + col.enabled = rpdat.arm_skin == 'On' + col.prop(rpdat, 'arm_skin_max_bones_auto') + row = col.row() row.enabled = not rpdat.arm_skin_max_bones_auto row.prop(rpdat, 'arm_skin_max_bones') - layout.prop(rpdat, "rp_hdr") - layout.prop(rpdat, "rp_stereo") - layout.prop(rpdat, 'arm_culling') - layout.prop(rpdat, 'rp_pp') + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, "rp_hdr") + col.prop(rpdat, "rp_stereo") + col.prop(rpdat, 'arm_culling') + col.prop(rpdat, 'rp_pp') class ARM_PT_RenderPathShadowsPanel(bpy.types.Panel): bl_label = "Shadows" @@ -1430,16 +1460,22 @@ def draw(self, context): rpdat = wrd.arm_rplist[wrd.arm_rplist_index] layout.prop(rpdat, "rp_background") - layout.prop(rpdat, 'arm_irradiance') + col = layout.column() - col.enabled = rpdat.arm_irradiance - col.prop(rpdat, 'arm_radiance') + col.prop(rpdat, 'arm_irradiance') colb = col.column() - colb.enabled = rpdat.arm_radiance - colb.prop(rpdat, 'arm_radiance_size') + colb.enabled = rpdat.arm_irradiance + colb.prop(rpdat, 'arm_radiance') + sub = colb.row() + sub.enabled = rpdat.arm_radiance + sub.prop(rpdat, 'arm_radiance_size') + layout.separator() + layout.prop(rpdat, 'arm_clouds') - layout.prop(rpdat, "rp_water") + col = layout.column(align=True) + col.prop(rpdat, "rp_water") + col = col.column(align=True) col.enabled = rpdat.rp_water col.prop(rpdat, 'arm_water_level') col.prop(rpdat, 'arm_water_density') @@ -1475,27 +1511,35 @@ def draw(self, context): rpdat = wrd.arm_rplist[wrd.arm_rplist_index] layout.enabled = rpdat.rp_render_to_texture - row = layout.row() - row.prop(rpdat, "rp_antialiasing") - layout.prop(rpdat, "rp_supersampling") - layout.prop(rpdat, 'arm_rp_resolution') + col = layout.column() + col.prop(rpdat, "rp_antialiasing") + col.prop(rpdat, "rp_supersampling") + + col = layout.column() + col.prop(rpdat, 'arm_rp_resolution') if rpdat.arm_rp_resolution == 'Custom': - layout.prop(rpdat, 'arm_rp_resolution_size') - layout.prop(rpdat, 'arm_rp_resolution_filter') - layout.prop(rpdat, 'rp_dynres') + col.prop(rpdat, 'arm_rp_resolution_size') + col.prop(rpdat, 'arm_rp_resolution_filter') + col.prop(rpdat, 'rp_dynres') layout.separator() - row = layout.row() - row.prop(rpdat, "rp_ssgi") + col = layout.column() - col.enabled = rpdat.rp_ssgi != 'Off' - col.prop(rpdat, 'arm_ssgi_half_res') - col.prop(rpdat, 'arm_ssgi_rays') - col.prop(rpdat, 'arm_ssgi_radius') - col.prop(rpdat, 'arm_ssgi_strength') - col.prop(rpdat, 'arm_ssgi_max_steps') + col.prop(rpdat, "rp_ssgi") + sub = col.column() + sub.enabled = rpdat.rp_ssgi != 'Off' + sub.prop(rpdat, 'arm_ssgi_half_res') + sub.prop(rpdat, 'arm_ssgi_rays') + sub.prop(rpdat, 'arm_ssgi_radius') + sub.prop(rpdat, 'arm_ssgi_strength') + sub.prop(rpdat, 'arm_ssgi_max_steps') + layout.separator(factor=0.5) + + layout.prop(rpdat, 'arm_micro_shadowing') layout.separator() - layout.prop(rpdat, "rp_ssr") + col = layout.column() + col.prop(rpdat, "rp_ssr") + col = col.column() col.enabled = rpdat.rp_ssr col.prop(rpdat, 'arm_ssr_half_res') col.prop(rpdat, 'arm_ssr_ray_step') @@ -1504,33 +1548,42 @@ def draw(self, context): col.prop(rpdat, 'arm_ssr_falloff_exp') col.prop(rpdat, 'arm_ssr_jitter') layout.separator() - layout.prop(rpdat, 'arm_ssrs') + col = layout.column() + col.prop(rpdat, 'arm_ssrs') + col = col.column() col.enabled = rpdat.arm_ssrs col.prop(rpdat, 'arm_ssrs_ray_step') - layout.prop(rpdat, 'arm_micro_shadowing') layout.separator() - layout.prop(rpdat, "rp_bloom") + col = layout.column() + col.prop(rpdat, "rp_bloom") + col = col.column() col.enabled = rpdat.rp_bloom col.prop(rpdat, 'arm_bloom_threshold') col.prop(rpdat, 'arm_bloom_strength') col.prop(rpdat, 'arm_bloom_radius') layout.separator() - layout.prop(rpdat, "rp_motionblur") + col = layout.column() + col.prop(rpdat, "rp_motionblur") + col = col.column() col.enabled = rpdat.rp_motionblur != 'Off' col.prop(rpdat, 'arm_motion_blur_intensity') layout.separator() - layout.prop(rpdat, "rp_volumetriclight") + col = layout.column() + col.prop(rpdat, "rp_volumetriclight") + col = col.column() col.enabled = rpdat.rp_volumetriclight col.prop(rpdat, 'arm_volumetric_light_air_color') col.prop(rpdat, 'arm_volumetric_light_air_turbidity') col.prop(rpdat, 'arm_volumetric_light_steps') layout.separator() - layout.prop(rpdat, "rp_chromatic_aberration") + col = layout.column() + col.prop(rpdat, "rp_chromatic_aberration") + col = col.column() col.enabled = rpdat.rp_chromatic_aberration col.prop(rpdat, 'arm_chromatic_aberration_type') col.prop(rpdat, 'arm_chromatic_aberration_strength') @@ -1563,45 +1616,51 @@ def draw(self, context): layout.enabled = rpdat.rp_compositornodes layout.prop(rpdat, 'arm_tonemap') - layout.prop(rpdat, 'arm_letterbox') - col = layout.column() - col.enabled = rpdat.arm_letterbox - col.prop(rpdat, 'arm_letterbox_size') - layout.prop(rpdat, 'arm_sharpen') - col = layout.column() - col.enabled = rpdat.arm_sharpen - col.prop(rpdat, 'arm_sharpen_strength') - layout.prop(rpdat, 'arm_fisheye') - layout.prop(rpdat, 'arm_vignette') + layout.separator() + col = layout.column() - col.enabled = rpdat.arm_vignette - col.prop(rpdat, 'arm_vignette_strength') - layout.prop(rpdat, 'arm_lensflare') - layout.prop(rpdat, 'arm_grain') + draw_conditional_prop(col, 'Letterbox', rpdat, 'arm_letterbox', 'arm_letterbox_size') + draw_conditional_prop(col, 'Sharpen', rpdat, 'arm_sharpen', 'arm_sharpen_strength') + draw_conditional_prop(col, 'Vignette', rpdat, 'arm_vignette', 'arm_vignette_strength') + draw_conditional_prop(col, 'Film Grain', rpdat, 'arm_grain', 'arm_grain_strength') + layout.separator() + col = layout.column() - col.enabled = rpdat.arm_grain - col.prop(rpdat, 'arm_grain_strength') - layout.prop(rpdat, 'arm_fog') - col = layout.column(align=True) + col.prop(rpdat, 'arm_fog') + col = col.column(align=True) col.enabled = rpdat.arm_fog col.prop(rpdat, 'arm_fog_color') col.prop(rpdat, 'arm_fog_amounta') col.prop(rpdat, 'arm_fog_amountb') layout.separator() - layout.prop(rpdat, "rp_autoexposure") + + col = layout.column() + col.prop(rpdat, "rp_autoexposure") + sub = col.column(align=True) + sub.enabled = rpdat.rp_autoexposure + sub.prop(rpdat, 'arm_autoexposure_strength', text='Strength') + sub.prop(rpdat, 'arm_autoexposure_speed', text='Speed') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_lensflare') + col.prop(rpdat, 'arm_fisheye') + layout.separator() + col = layout.column() - col.enabled = rpdat.rp_autoexposure - col.prop(rpdat, 'arm_autoexposure_strength', text='Strength') - col.prop(rpdat, 'arm_autoexposure_speed', text='Speed') - layout.prop(rpdat, 'arm_lens_texture') + col.prop(rpdat, 'arm_lens_texture') if rpdat.arm_lens_texture != "": - layout.prop(rpdat, 'arm_lens_texture_masking') + col.prop(rpdat, 'arm_lens_texture_masking') if rpdat.arm_lens_texture_masking: - layout.prop(rpdat, 'arm_lens_texture_masking_centerMinClip') - layout.prop(rpdat, 'arm_lens_texture_masking_centerMaxClip') - layout.prop(rpdat, 'arm_lens_texture_masking_luminanceMin') - layout.prop(rpdat, 'arm_lens_texture_masking_luminanceMax') - layout.prop(rpdat, 'arm_lens_texture_masking_brightnessExp') + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_centerMinClip') + sub.prop(rpdat, 'arm_lens_texture_masking_centerMaxClip') + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMin') + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMax') + col.prop(rpdat, 'arm_lens_texture_masking_brightnessExp') + layout.separator() + layout.prop(rpdat, 'arm_lut_texture') class ARM_PT_BakePanel(bpy.types.Panel): @@ -1656,425 +1715,8 @@ def draw(self, context): layout.prop(item, "res_x") layout.prop(item, "res_y") - else: - - scene = context.scene - sceneProperties = scene.TLM_SceneProperties - row = layout.row(align=True) - - row = layout.row(align=True) - - #We list LuxCoreRender as available, by default we assume Cycles exists - row.prop(sceneProperties, "tlm_lightmap_engine") - - if sceneProperties.tlm_lightmap_engine == "Cycles": - - #CYCLES SETTINGS HERE - engineProperties = scene.TLM_EngineProperties - - row = layout.row(align=True) - row.label(text="General Settings") - row = layout.row(align=True) - row.operator("tlm.build_lightmaps") - row = layout.row(align=True) - row.operator("tlm.clean_lightmaps") - row = layout.row(align=True) - row.operator("tlm.explore_lightmaps") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_apply_on_unwrap") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_headless") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_alert_on_finish") - - if sceneProperties.tlm_alert_on_finish: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_alert_sound") - - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_verbose") - #row = layout.row(align=True) - #row.prop(sceneProperties, "tlm_compile_statistics") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_bg_color") - if sceneProperties.tlm_override_bg_color: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_color") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_reset_uv") - - row = layout.row(align=True) - try: - if bpy.context.scene["TLM_Buildstat"] is not None: - row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0])) - except: - pass - - row = layout.row(align=True) - row.label(text="Cycles Settings") - - row = layout.row(align=True) - row.prop(engineProperties, "tlm_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_quality") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_resolution_scale") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_bake_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_lighting_mode") - - if scene.TLM_EngineProperties.tlm_bake_mode == "Background": - row = layout.row(align=True) - row.label(text="Warning! Background mode is currently unstable", icon_value=2) - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_network_render") - if sceneProperties.tlm_network_render: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_network_paths") - #row = layout.row(align=True) - #row.prop(sceneProperties, "tlm_network_dir") - row = layout.row(align=True) - row = layout.row(align=True) - row.prop(engineProperties, "tlm_caching_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_directional_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_lightmap_savedir") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_dilation_margin") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_exposure_multiplier") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_setting_supersample") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_metallic_clamp") - - elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - - #LUXCORE SETTINGS HERE - luxcore_available = False - - #Look for Luxcorerender in the renderengine classes - for engine in bpy.types.RenderEngine.__subclasses__(): - if engine.bl_idname == "LUXCORE": - luxcore_available = True - break - - row = layout.row(align=True) - if not luxcore_available: - row.label(text="Please install BlendLuxCore.") - else: - row.label(text="LuxCoreRender not yet available.") - - elif sceneProperties.tlm_lightmap_engine == "OctaneRender": - - #LUXCORE SETTINGS HERE - octane_available = False - - row = layout.row(align=True) - row.label(text="Octane Render not yet available.") - - - ################## - #DENOISE SETTINGS! - row = layout.row(align=True) - row.label(text="Denoise Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_denoise_use") - row = layout.row(align=True) - - if sceneProperties.tlm_denoise_use: - row.prop(sceneProperties, "tlm_denoise_engine", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_denoise_engine == "Integrated": - row.label(text="No options for Integrated.") - elif sceneProperties.tlm_denoise_engine == "OIDN": - denoiseProperties = scene.TLM_OIDNEngineProperties - row.prop(denoiseProperties, "tlm_oidn_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_threads") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_maxmem") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_affinity") - # row = layout.row(align=True) - # row.prop(denoiseProperties, "tlm_denoise_ao") - elif sceneProperties.tlm_denoise_engine == "Optix": - denoiseProperties = scene.TLM_OptixEngineProperties - row.prop(denoiseProperties, "tlm_optix_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_maxmem") - - - ################## - #FILTERING SETTINGS! - row = layout.row(align=True) - row.label(text="Filtering Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_filtering_use") - row = layout.row(align=True) - - if sceneProperties.tlm_filtering_use: - - if sceneProperties.tlm_filtering_engine == "OpenCV": - - cv2 = importlib.util.find_spec("cv2") - - if cv2 is None: - row = layout.row(align=True) - row.label(text="OpenCV is not installed. Install it below.") - row = layout.row(align=True) - row.label(text="It is recommended to install as administrator.") - row = layout.row(align=True) - row.operator("tlm.install_opencv_lightmaps") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") - row = layout.row(align=True) - if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") - - - ################## - #ENCODING SETTINGS! - row = layout.row(align=True) - row.label(text="Encoding Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_use") - row = layout.row(align=True) - - if sceneProperties.tlm_encoding_use: - - - if scene.TLM_EngineProperties.tlm_bake_mode == "Background": - row.label(text="Encoding options disabled in background mode") - row = layout.row(align=True) - - row.prop(sceneProperties, "tlm_encoding_device", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_encoding_device == "CPU": - row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True) - else: - row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True) - - if sceneProperties.tlm_encoding_device == "CPU": - if sceneProperties.tlm_encoding_mode_a == "RGBM": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_range") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - if sceneProperties.tlm_encoding_mode_a == "RGBD": - pass - if sceneProperties.tlm_encoding_mode_a == "HDR": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_format") - else: - - if sceneProperties.tlm_encoding_mode_b == "RGBM": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_range") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - - if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - if sceneProperties.tlm_encoding_mode_b == "HDR": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_format") - - ################## - #SELECTION OPERATORS! - - row = layout.row(align=True) - row.label(text="Selection Operators") - row = layout.row(align=True) - - row = layout.row(align=True) - row.operator("tlm.enable_selection") - row = layout.row(align=True) - row.operator("tlm.disable_selection") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_object_settings") - - if sceneProperties.tlm_override_object_settings: - - row = layout.row(align=True) - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") - row = layout.row() - - if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - - if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: - row = layout.row() - item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] - row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') - else: - row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") - - else: - row = layout.row() - row.prop(sceneProperties, "tlm_postpack_object") - row = layout.row() - - if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": - if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: - row = layout.row() - item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] - row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') - row = layout.row() - - else: - row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") - row = layout.row() - - if sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": - row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_unwrap_margin") - - row = layout.row(align=True) - row.operator("tlm.remove_uv_selection") - row = layout.row(align=True) - row.operator("tlm.select_lightmapped_objects") - row = layout.row(align=True) - - ################## - #Additional settings - row = layout.row(align=True) - row.label(text="Additional options") - sceneProperties = scene.TLM_SceneProperties - atlasListItem = scene.TLM_AtlasListItem - atlasList = scene.TLM_AtlasList - postatlasListItem = scene.TLM_PostAtlasListItem - postatlasList = scene.TLM_PostAtlasList - - layout.label(text="Atlas Groups") - row = layout.row() - row.prop(sceneProperties, "tlm_atlas_mode", expand=True) - - if sceneProperties.tlm_atlas_mode == "Prepack": - - rows = 2 - if len(atlasList) > 1: - rows = 4 - row = layout.row() - row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows) - col = row.column(align=True) - col.operator("tlm_atlaslist.new_item", icon='ADD', text="") - col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") - #col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") - - # if len(scene.TLM_AtlasList) > 1: - # col.separator() - # op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="") - # op.direction = 'UP' - # op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="") - # op.direction = 'DOWN' - - if atlasListItem >= 0 and len(atlasList) > 0: - item = atlasList[atlasListItem] - #layout.prop_search(item, "obj", bpy.data, "objects", text="Object") - #layout.prop(item, "res_x") - layout.prop(item, "tlm_atlas_lightmap_unwrap_mode") - layout.prop(item, "tlm_atlas_lightmap_resolution") - layout.prop(item, "tlm_atlas_unwrap_margin") - - amount = 0 - - for obj in bpy.data.objects: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: - amount = amount + 1 - - layout.label(text="Objects: " + str(amount)) - - # layout.use_property_split = True - # layout.use_property_decorate = False - # layout.label(text="Enable for selection") - # layout.label(text="Disable for selection") - # layout.label(text="Something...") - - else: - - layout.label(text="Postpacking is unstable.") - rows = 2 - if len(atlasList) > 1: - rows = 4 - row = layout.row() - row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows) - col = row.column(align=True) - col.operator("tlm_postatlaslist.new_item", icon='ADD', text="") - col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="") - - if postatlasListItem >= 0 and len(postatlasList) > 0: - item = postatlasList[postatlasListItem] - layout.prop(item, "tlm_atlas_lightmap_resolution") - - #Below list object counter - amount = 0 - utilized = 0 - atlasUsedArea = 0 - atlasSize = item.tlm_atlas_lightmap_resolution - - for obj in bpy.data.objects: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - if obj.TLM_ObjectProperties.tlm_postpack_object: - if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: - amount = amount + 1 - - atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 - - row = layout.row() - row.prop(item, "tlm_atlas_repack_on_cleanup") - - #TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY! - cv2 = True - - if cv2: - row = layout.row() - row.prop(item, "tlm_atlas_dilation") - layout.label(text="Objects: " + str(amount)) - - utilized = atlasUsedArea / (int(atlasSize) ** 2) - layout.label(text="Utilized: " + str(utilized * 100) + "%") - - if (utilized * 100) > 100: - layout.label(text="Warning! Overflow not yet supported") - class ArmGenLodButton(bpy.types.Operator): - '''Automatically generate LoD levels''' + """Automatically generate LoD levels.""" bl_idname = 'arm.generate_lod' bl_label = 'Auto Generate' @@ -2338,20 +1980,27 @@ def draw(self, context): layout.use_property_split = True layout.use_property_decorate = False layout.operator("arm.make_proxy") + obj = bpy.context.object - if obj != None and obj.proxy != None: - layout.label(text="Sync") - layout.prop(obj, "arm_proxy_sync_loc") - layout.prop(obj, "arm_proxy_sync_rot") - layout.prop(obj, "arm_proxy_sync_scale") - layout.prop(obj, "arm_proxy_sync_materials") - layout.prop(obj, "arm_proxy_sync_modifiers") - layout.prop(obj, "arm_proxy_sync_traits") - row = layout.row() + if obj is not None and obj.proxy is not None: + col = layout.column(heading="Sync") + col.prop(obj, "arm_proxy_sync_loc") + col.prop(obj, "arm_proxy_sync_rot") + col.prop(obj, "arm_proxy_sync_scale") + col.separator() + + col.prop(obj, "arm_proxy_sync_materials") + col.prop(obj, "arm_proxy_sync_modifiers") + col.separator() + + col.prop(obj, "arm_proxy_sync_traits") + row = col.row() row.enabled = obj.arm_proxy_sync_traits row.prop(obj, "arm_proxy_sync_trait_props") - layout.operator("arm.proxy_toggle_all") - layout.operator("arm.proxy_apply_all") + + row = layout.row(align=True) + row.operator("arm.proxy_toggle_all") + row.operator("arm.proxy_apply_all") class ArmMakeProxyButton(bpy.types.Operator): '''Create proxy from linked object''' @@ -2463,7 +2112,7 @@ class ARM_OT_ShowFileVersionInfo(bpy.types.Operator): bl_idname = 'arm.show_old_file_version_info' bl_description = ('Displays an info panel that warns about opening a file' 'which was created in a previous version of Armory') - # bl_options = {'INTERNAL'} + bl_options = {'INTERNAL'} wrd = None @@ -2752,7 +2401,7 @@ def draw_custom_node_menu(self, context): layout = self.layout layout.separator() layout.operator("arm.open_node_documentation", text="Show documentation for this node", icon='HELP') - layout.operator("arm.open_node_haxe_source", text="Open .hx source in the browser", icon_value=arm.props_traits.icons_dict['haxe'].icon_id) + layout.operator("arm.open_node_haxe_source", text="Open .hx source in the browser", icon_value=ui_icons.get_id("haxe")) layout.operator("arm.open_node_python_source", text="Open .py source in the browser", icon='FILE_SCRIPT') elif context.space_data.tree_type == 'ShaderNodeTree': @@ -2762,6 +2411,18 @@ def draw_custom_node_menu(self, context): layout.prop(context.active_node, 'arm_material_param', text='Armory: Material Parameter') +def draw_conditional_prop(layout: bpy.types.UILayout, heading: str, data: bpy.types.AnyType, prop_condition: str, prop_value: str) -> None: + """Draws a property row with a checkbox that enables a value field. + The function fails when prop_condition is not a boolean property. + """ + col = layout.column(heading=heading) + row = col.row() + row.prop(data, prop_condition, text='') + sub = row.row() + sub.enabled = getattr(data, prop_condition) + sub.prop(data, prop_value, expand=True) + + def register(): bpy.utils.register_class(ARM_PT_ObjectPropsPanel) bpy.utils.register_class(ARM_PT_ModifiersPropsPanel) @@ -2827,6 +2488,13 @@ def register(): bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) + bpy.utils.register_class(scene.TLM_PT_Settings) + bpy.utils.register_class(scene.TLM_PT_Denoise) + bpy.utils.register_class(scene.TLM_PT_Filtering) + bpy.utils.register_class(scene.TLM_PT_Encoding) + bpy.utils.register_class(scene.TLM_PT_Utility) + bpy.utils.register_class(scene.TLM_PT_Additional) + bpy.types.VIEW3D_HT_header.append(draw_view3d_header) bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) bpy.types.NODE_MT_context_menu.append(draw_custom_node_menu) @@ -2900,3 +2568,10 @@ def unregister(): bpy.utils.unregister_class(ArmSyncProxyButton) bpy.utils.unregister_class(ArmPrintTraitsButton) bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) + + bpy.utils.unregister_class(scene.TLM_PT_Settings) + bpy.utils.unregister_class(scene.TLM_PT_Denoise) + bpy.utils.unregister_class(scene.TLM_PT_Filtering) + bpy.utils.unregister_class(scene.TLM_PT_Encoding) + bpy.utils.unregister_class(scene.TLM_PT_Utility) + bpy.utils.unregister_class(scene.TLM_PT_Additional) diff --git a/blender/arm/ui_icons.py b/blender/arm/ui_icons.py new file mode 100644 index 0000000000..ed2649f5da --- /dev/null +++ b/blender/arm/ui_icons.py @@ -0,0 +1,33 @@ +""" +Blender user interface icon handling. +""" +import os.path +from typing import Optional + +import bpy.utils.previews + +_icons_dict: Optional[bpy.utils.previews.ImagePreviewCollection] = None +"""Dictionary of all loaded icons, or `None` if not loaded""" + +_icons_dir = os.path.join(os.path.dirname(__file__), "custom_icons") +"""Directory of the icon files""" + + +def _load_icons() -> None: + """(Re)loads all icons""" + global _icons_dict + + if _icons_dict is not None: + bpy.utils.previews.remove(_icons_dict) + + _icons_dict = bpy.utils.previews.new() + _icons_dict.load("bundle", os.path.join(_icons_dir, "bundle.png"), 'IMAGE') + _icons_dict.load("haxe", os.path.join(_icons_dir, "haxe.png"), 'IMAGE') + _icons_dict.load("wasm", os.path.join(_icons_dir, "wasm.png"), 'IMAGE') + + +def get_id(identifier: str) -> int: + """Returns the icon ID from the given identifier""" + if _icons_dict is None: + _load_icons() + return _icons_dict[identifier].icon_id diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 88db6888f7..7b3af7389a 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -946,7 +946,7 @@ def get_link_web_server(): return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server def compare_version_blender_arm(): - return not (bpy.app.version[0] != 2 or bpy.app.version[1] != 83) + return not (bpy.app.version[0] != 2 or bpy.app.version[1] != 93) def type_name_to_type(name: str) -> bpy.types.bpy_struct: """Return the Blender type given by its name, if registered.""" diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 115293945f..f6ca036e5a 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -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