Merge months of changes into 'newrotation' branch

(and homogeneised the contents of said branch in the process)
(plus a couple bugfixes, because what else)
This commit is contained in:
niacdoial 2021-08-18 17:04:00 +02:00
commit f892fdfd8a
No known key found for this signature in database
GPG key ID: D2B9B9FE6D7B1C8E
528 changed files with 12765 additions and 4749 deletions

View file

@ -21,7 +21,9 @@
uniform sampler2D gbufferD;
uniform sampler2D gbuffer0;
uniform sampler2D gbuffer1;
#ifdef _gbuffer2
uniform sampler2D gbuffer2;
#endif
#ifdef _VoxelAOvar
uniform sampler3D voxels;
@ -94,7 +96,7 @@ uniform vec3 eye;
uniform vec3 eyeLook;
#ifdef _Clusters
uniform vec4 lightsArray[maxLights * 2];
uniform vec4 lightsArray[maxLights * 3];
#ifdef _Spot
uniform vec4 lightsArraySpot[maxLights];
#endif
@ -206,7 +208,9 @@ void main() {
vec3 v = normalize(eye - p);
float dotNV = max(dot(n, v), 0.0);
#ifdef _gbuffer2
vec4 g2 = textureLod(gbuffer2, texCoord, 0.0);
#endif
#ifdef _MicroShadowing
occspec.x = mix(1.0, occspec.x, dotNV); // AO Fresnel
@ -221,14 +225,16 @@ void main() {
vec3 envl = shIrradiance(n, shirr);
if (g2.b < 0.5) {
envl = envl;
} else {
envl = vec3(1.0);
}
#ifdef _gbuffer2
if (g2.b < 0.5) {
envl = envl;
} else {
envl = vec3(1.0);
}
#endif
#ifdef _EnvTex
envl /= PI;
envl /= PI;
#endif
#else
vec3 envl = vec3(1.0);
@ -456,18 +462,19 @@ void main() {
n,
v,
dotNV,
lightsArray[li * 2].xyz, // lp
lightsArray[li * 2 + 1].xyz, // lightCol
lightsArray[li * 3].xyz, // lp
lightsArray[li * 3 + 1].xyz, // lightCol
albedo,
roughness,
occspec.y,
f0
#ifdef _ShadowMap
, li, lightsArray[li * 2].w, true // bias
// light index, shadow bias, cast_shadows
, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0
#endif
#ifdef _Spot
, lightsArray[li * 2 + 1].w != 0.0
, lightsArray[li * 2 + 1].w // cutoff
, lightsArray[li * 3 + 2].y != 0.0
, lightsArray[li * 3 + 2].y // cutoff
, lightsArraySpot[li].w // cutoff - exponent
, lightsArraySpot[li].xyz // spotDir
#endif

View file

@ -99,7 +99,7 @@
},
{
"name": "LWVP",
"link": "_biasLightWorldViewProjectionMatrix",
"link": "_biasLightWorldViewProjectionMatrixSun",
"ifndef": ["_CSM"],
"ifdef": ["_Sun", "_ShadowMap"]
},

View file

@ -29,12 +29,15 @@ uniform int envmapNumMipmaps;
uniform vec3 backgroundCol;
#endif
#ifdef _SMSizeUniform
//!uniform vec2 smSizeUniform;
#endif
uniform vec2 cameraProj;
uniform vec3 eye;
uniform vec3 eyeLook;
#ifdef _Clusters
uniform vec4 lightsArray[maxLights * 2];
uniform vec4 lightsArray[maxLights * 3];
#ifdef _Spot
uniform vec4 lightsArraySpot[maxLights];
#endif
@ -255,18 +258,19 @@ void main() {
n,
v,
dotNV,
lightsArray[li * 2].xyz, // lp
lightsArray[li * 2 + 1].xyz, // lightCol
lightsArray[li * 3].xyz, // lp
lightsArray[li * 3 + 1].xyz, // lightCol
albedo,
roughness,
occspec.y,
f0
#ifdef _ShadowMap
, li, lightsArray[li * 2].w, true // bias
// light index, shadow bias, cast_shadows
, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0
#endif
#ifdef _Spot
, lightsArray[li * 2 + 1].w != 0.0
, lightsArray[li * 2 + 1].w // cutoff
, lightsArray[li * 3 + 2].y != 0.0
, lightsArray[li * 3 + 2].y // cutoff
, lightsArraySpot[li].w // cutoff - exponent
, lightsArraySpot[li].xyz // spotDir
#endif

View file

@ -88,7 +88,7 @@
},
{
"name": "LWVP",
"link": "_biasLightWorldViewProjectionMatrix",
"link": "_biasLightWorldViewProjectionMatrixSun",
"ifndef": ["_CSM"],
"ifdef": ["_Sun", "_ShadowMap"]
},
@ -107,6 +107,11 @@
"link": "_viewProjectionMatrix",
"ifdef": ["_SSRS"]
},
{
"name": "smSizeUniform",
"link": "_shadowMapSize",
"ifdef": ["_SMSizeUniform"]
},
{
"name": "lightProj",
"link": "_lightPlaneProj",

View file

@ -8,6 +8,10 @@ int getClusterI(vec2 tc, float viewz, vec2 cameraPlane) {
float z = log(viewz - cnear + 1.0) / log(cameraPlane.y - cnear + 1.0);
sliceZ = int(z * (clusterSlices.z - 1)) + 1;
}
// address gap between near plane and cluster near offset
else if (viewz >= cameraPlane.x) {
sliceZ = 1;
}
return int(tc.x * clusterSlices.x) +
int(int(tc.y * clusterSlices.y) * clusterSlices.x) +
int(sliceZ * clusterSlices.x * clusterSlices.y);

View file

@ -185,17 +185,13 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas
vec3 uv = lPos.xyz / lPos.w;
#ifdef _InvY
uv.y = 1.0 - uv.y; // invert Y coordinates for direct3d coordinate system
#endif
direct *= shadowTest(
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
shadowMapAtlas
#endif
, uv, bias
, lPos.xyz / lPos.w, bias
);
#else
if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias);

View file

@ -8,39 +8,39 @@
#endif
#ifdef _ShadowMap
#ifdef _SinglePoint
#ifdef _Spot
uniform sampler2DShadow shadowMapSpot[1];
uniform mat4 LWVPSpot[1];
#else
uniform samplerCubeShadow shadowMapPoint[1];
uniform vec2 lightProj;
#ifdef _SinglePoint
#ifdef _Spot
uniform sampler2DShadow shadowMapSpot[1];
uniform mat4 LWVPSpot[1];
#else
uniform samplerCubeShadow shadowMapPoint[1];
uniform vec2 lightProj;
#endif
#endif
#endif
#ifdef _Clusters
#ifdef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlas;
#endif
uniform vec2 lightProj;
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasPoint;
#endif
#else
uniform samplerCubeShadow shadowMapPoint[4];
#endif
#ifdef _Spot
#ifdef _Clusters
#ifdef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlas;
#endif
uniform vec2 lightProj;
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSpot;
uniform sampler2DShadow shadowMapAtlasPoint;
#endif
#else
uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
uniform samplerCubeShadow shadowMapPoint[4];
#endif
#ifdef _Spot
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSpot;
#endif
#else
uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
#endif
uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif
uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif
#endif
#endif
vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, const vec3 lp, const vec3 lightCol,
const vec3 albedo, const float rough, const float spec, const vec3 f0
@ -80,17 +80,13 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas
vec3 uv = lPos.xyz / lPos.w;
#ifdef _InvY
uv.y = 1.0 - uv.y; // invert Y coordinates for direct3d coordinate system
#endif
direct *= shadowTest(
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
shadowMapAtlas
#endif
, uv, bias
, lPos.xyz / lPos.w, bias
);
#else
if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias);
@ -106,11 +102,13 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#endif
#ifdef _ShadowMap
#ifndef _Spot
if (receiveShadow) {
#ifdef _SinglePoint
#ifndef _Spot
direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n);
#endif
#endif
#ifdef _Clusters
#ifdef _ShadowMapAtlas
direct *= PCFFakeCube(
@ -130,7 +128,6 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#endif
}
#endif
#endif
return direct;
}

View file

@ -23,7 +23,7 @@ vec2 rand2(const vec2 coord) {
const float width = 1100.0;
const float height = 500.0;
float noiseX = ((fract(1.0 - coord.s * (width / 2.0)) * 0.25) + (fract(coord.t * (height / 2.0)) * 0.75)) * 2.0 - 1.0;
float noiseY = ((fract(1.0 - coord.s * (width / 2.0)) * 0.75) + (fract(coord.t * (height / 2.0)) * 0.25)) * 2.0 - 1.0;
float noiseY = ((fract(1.0 - coord.s * (width / 2.0)) * 0.75) + (fract(coord.t * (height / 2.0)) * 0.25)) * 2.0 - 1.0;
return vec2(noiseX, noiseY);
}
@ -40,4 +40,9 @@ float attenuate(const float dist) {
// 1.0 / (quadratic * dist * dist);
}
float safe_acos(const float x) {
// acos is undefined if |x| > 1
return acos(clamp(x, -1.0, 1.0));
}
#endif

View file

@ -42,7 +42,16 @@ vec2 sampleCube(vec3 dir, out int faceIndex) {
uv = vec2(dir.x < 0.0 ? dir.z : -dir.z, -dir.y);
}
// downscale uv a little to hide seams
return uv * 0.9976 * ma + 0.5;
// transform coordinates from clip space to texture space
#ifndef _FlipY
return uv * 0.9976 * ma + 0.5;
#else
#ifdef HLSL
return uv * 0.9976 * ma + 0.5;
#else
return vec2(uv.x * ma, uv.y * -ma) * 0.9976 + 0.5;
#endif
#endif
}
#endif
@ -87,6 +96,7 @@ float PCFCube(samplerCubeShadow shadowMapCube, const vec3 lp, vec3 ml, const flo
#ifdef _ShadowMapAtlas
// transform "out-of-bounds" coordinates to the correct face/coordinate system
// https://www.khronos.org/opengl/wiki/File:CubeMapAxes.png
vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) {
if (uv.x < 0.0) {
if (faceIndex == 0) { // X+
@ -189,7 +199,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
vec4 pointLightTile = pointLightDataArray[lightIndex + faceIndex]; // x: tile X offset, y: tile Y offset, z: tile size relative to atlas
vec2 uvtiled = pointLightTile.z * uv + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
@ -199,15 +209,15 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 0.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(pointLightTile.z * uvtiled + pointLightTile.xy, compare));
result += texture(shadowMap, vec3(uvtiled, compare));
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -215,7 +225,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, -1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -223,7 +233,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, -1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -231,7 +241,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, 1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -239,7 +249,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, -1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -247,7 +257,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 0.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));
@ -255,7 +265,7 @@ float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float
uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 1.0) / smSize)));
pointLightTile = pointLightDataArray[lightIndex + newFaceIndex];
uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy;
#ifdef _InvY
#ifdef _FlipY
uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system
#endif
result += texture(shadowMap, vec3(uvtiled, compare));

155
Shaders/std/sky.glsl Normal file
View file

@ -0,0 +1,155 @@
/* 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_
#include "std/math.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
// Values from [Hill: 60]
#define sun_limb_darkening_col vec3(0.397, 0.503, 0.652)
vec3 nishita_lookupLUT(const float height, const float sunTheta) {
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 = safe_acos(dot(normalize(iPos), normalize(pSun)));
vec3 jAttn = nishita_lookupLUT(iHeight, sunTheta);
// Calculate attenuation
vec3 iAttn = exp(-(
nishita_mie_coeff * iOdMie
+ nishita_rayleigh_coeff * iOdRlh
// + 0 for ozone
));
vec3 attn = iAttn * jAttn;
// Apply dithering to reduce visible banding
attn *= 0.98 + rand(r.xy) * 0.04;
// Accumulate scattering
totalRlh += odStepRlh * attn;
totalMie += odStepMie * attn;
iTime += iStepSize;
}
return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie);
}
vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const float intensity) {
// Normalized SDF
float dist = distance(n, light_dir) / disk_size;
// Darken the edges of the sun
// [Hill: 28, 60] (according to [Nec96])
float invDist = 1.0 - dist;
float mu = sqrt(invDist * invDist);
vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col));
return 1 + (1.0 - step(1.0, dist)) * nishita_sun_intensity * intensity * limb_darkening;
}
#endif

View file

@ -12,7 +12,7 @@ uniform sampler2D gbufferD;
uniform sampler2D snoise;
#ifdef _Clusters
uniform vec4 lightsArray[maxLights * 2];
uniform vec4 lightsArray[maxLights * 3];
#ifdef _Spot
uniform vec4 lightsArraySpot[maxLights];
#endif
@ -44,7 +44,15 @@ uniform vec2 cameraPlane;
uniform vec3 sunDir;
uniform vec3 sunCol;
#ifdef _ShadowMap
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSun;
#else
uniform sampler2DShadow shadowMapAtlas;
#endif
#else
uniform sampler2DShadow shadowMap;
#endif
uniform float shadowsBias;
#ifdef _CSM
//!uniform vec4 casData[shadowmapCascades * 4 + 4];
@ -95,7 +103,17 @@ void rayStep(inout vec3 curPos, inout float curOpticalDepth, inout float scatter
#endif
vec4 lPos = LWVP * vec4(curPos, 1.0);
lPos.xyz /= lPos.w;
visibility = texture(shadowMap, vec3(lPos.xy, lPos.z - shadowsBias));
visibility = texture(
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
shadowMapAtlasSun
#else
shadowMapAtlas
#endif
#else
shadowMap
#endif
, vec3(lPos.xy, lPos.z - shadowsBias));
#endif
#ifdef _SinglePoint

View file

@ -63,7 +63,7 @@
},
{
"name": "LWVP",
"link": "_biasLightWorldViewProjectionMatrix",
"link": "_biasLightWorldViewProjectionMatrixSun",
"ifndef": ["_CSM"],
"ifdef": ["_Sun", "_ShadowMap"]
},

View file

@ -1,13 +1,11 @@
package armory.logicnode;
import armory.trait.physics.PhysicsConstraint;
#if arm_physics
import armory.trait.physics.bullet.PhysicsConstraint.ConstraintType;
import armory.trait.physics.bullet.PhysicsConstraint.ConstraintAxis;
#end
import iron.object.Object;
import armory.trait.physics.RigidBody;
import armory.logicnode.PhysicsConstraintNode;
#if arm_physics
import armory.trait.physics.PhysicsConstraint;
import armory.trait.physics.bullet.PhysicsConstraint.ConstraintType;
#end
class AddPhysicsConstraintNode extends LogicNode {
@ -21,115 +19,96 @@ class AddPhysicsConstraintNode extends LogicNode {
}
override function run(from: Int) {
var pivotObject:Object = inputs[1].get();
var pivotObject: Object = inputs[1].get();
rb1 = inputs[2].get();
rb2 = inputs[3].get();
var disableCollisions: Bool = inputs[4].get();
var breakable: Bool = inputs[5].get();
var breakingThreshold: Float = inputs[6].get();
var type: ConstraintType = 0;
if (pivotObject == null || rb1 == null || rb2 == null) return;
#if arm_physics
var disableCollisions: Bool = inputs[4].get();
var breakable: Bool = inputs[5].get();
var breakingThreshold: Float = inputs[6].get();
var type: ConstraintType = 0;
var con: PhysicsConstraint = pivotObject.getTrait(PhysicsConstraint);
if(con == null)
{
switch(property0)
{
case 'Fixed':
type = Fixed;
case 'Point':
type = Point;
case 'Hinge':
type = Hinge;
case 'Slider':
type = Slider;
case 'Piston':
type = Piston;
case 'Generic Spring':
type = Generic;
if (con == null) {
switch (property0) {
case "Fixed": type = Fixed;
case "Point": type = Point;
case "Hinge": type = Hinge;
case "Slider": type = Slider;
case "Piston": type = Piston;
case "Generic Spring": type = Generic;
}
if(! breakable) breakingThreshold = 0.0;
if (!breakable) breakingThreshold = 0.0;
if (type != Generic) {
if(type != Generic) {
con = new PhysicsConstraint(rb1, rb2, type, disableCollisions, breakingThreshold);
switch (type)
{
switch (type) {
case Hinge:
var setLimit:Bool = inputs[7].get();
var low:Float = inputs[8].get();
var up:Float = inputs[9].get();
var setLimit: Bool = inputs[7].get();
var low: Float = inputs[8].get();
var up: Float = inputs[9].get();
con.setHingeConstraintLimits(setLimit, low, up);
case Slider:
var setLimit:Bool = inputs[7].get();
var low:Float = inputs[8].get();
var up:Float = inputs[9].get();
var setLimit: Bool = inputs[7].get();
var low: Float = inputs[8].get();
var up: Float = inputs[9].get();
con.setSliderConstraintLimits(setLimit, low, up);
case Piston:
var setLinLimit:Bool = inputs[7].get();
var linLow:Float = inputs[8].get();
var linUp:Float = inputs[9].get();
var setAngLimit:Bool = inputs[10].get();
var angLow:Float = inputs[11].get();
var angUp:Float = inputs[12].get();
var setLinLimit: Bool = inputs[7].get();
var linLow: Float = inputs[8].get();
var linUp: Float = inputs[9].get();
var setAngLimit: Bool = inputs[10].get();
var angLow: Float = inputs[11].get();
var angUp: Float = inputs[12].get();
con.setPistonConstraintLimits(setLinLimit, linLow, linUp, setAngLimit, angLow, angUp);
default:
default:
}
}
else
{
else {
var spring: Bool = false;
var prop: PhysicsConstraintNode;
for(inp in 7...inputs.length)
{
for (inp in 7...inputs.length) {
prop = inputs[inp].get();
if(prop == null) continue;
if(prop.isSpring)
{
if (prop == null) continue;
if (prop.isSpring) {
spring = true;
break;
}
}
if(spring) {
if (spring) {
con = new PhysicsConstraint(rb1, rb2, GenericSpring, disableCollisions, breakingThreshold);
}
}
else {
con = new PhysicsConstraint(rb1, rb2, Generic, disableCollisions, breakingThreshold);
}
for(inp in 7...inputs.length)
{
for (inp in 7...inputs.length) {
prop = inputs[inp].get();
if(prop == null) continue;
(inp + ': ');
if (prop == null) continue;
if(prop.isSpring)
{
if (prop.isSpring) {
con.setSpringParams(prop.isSpring, prop.value1, prop.value2, prop.axis, prop.isAngular);
}
else
{
else {
con.setGenericConstraintLimits(true, prop.value1, prop.value2, prop.axis, prop.isAngular);
}
}
}
pivotObject.addTrait(con);
}
#end
runOutput(0);
}
}

View file

@ -1,15 +1,17 @@
package armory.logicnode;
import iron.object.Object;
#if arm_physics
import armory.trait.physics.RigidBody;
import armory.trait.physics.bullet.RigidBody.Shape;
#end
import iron.object.Object;
import armory.trait.physics.RigidBody;
class AddRigidBodyNode extends LogicNode {
public var property0: String;//Shape
public var property1: String;//Advanced
public var property0: String; //Shape
public var property1: Bool; //Advanced
public var object: Object;
public function new(tree: LogicTree) {
@ -18,6 +20,10 @@ class AddRigidBodyNode extends LogicNode {
override function run(from: Int) {
object = inputs[1].get();
if (object == null) return;
#if arm_physics
var mass: Float = inputs[2].get();
var active: Bool = inputs[3].get();
var animated: Bool = inputs[4].get();
@ -38,8 +44,7 @@ class AddRigidBodyNode extends LogicNode {
var shape: Shape = 1;
if(property1 == 'true')
{
if (property1) {
margin = inputs[9].get();
marginLen = inputs[10].get();
linDamp = inputs[11].get();
@ -49,50 +54,34 @@ class AddRigidBodyNode extends LogicNode {
angVelThreshold = inputs[15].get();
group = inputs[16].get();
mask = inputs[17].get();
}
if (object == null) return;
#if arm_physics
var rb: RigidBody = object.getTrait(RigidBody);
if((group < 0) || (group > 32)) group = 1; //Limiting max groups to 32
if((mask < 0) || (mask > 32)) mask = 1; //Limiting max masks to 32
if(rb == null)
{
switch (property0){
case 'Box':
shape = Box;
case 'Sphere':
shape = Sphere;
case 'Capsule':
shape = Capsule;
case 'Cone':
shape = Cone;
case 'Cylinder':
shape = Cylinder;
case 'Convex Hull':
shape = ConvexHull;
case 'Mesh':
shape = Mesh;
if ((group < 0) || (group > 32)) group = 1; //Limiting max groups to 32
if ((mask < 0) || (mask > 32)) mask = 1; //Limiting max masks to 32
if (rb == null) {
switch (property0) {
case "Box": shape = Box;
case "Sphere": shape = Sphere;
case "Capsule": shape = Capsule;
case "Cone": shape = Cone;
case "Cylinder": shape = Cylinder;
case "Convex Hull": shape = ConvexHull;
case "Mesh": shape = Mesh;
}
rb = new RigidBody(shape, mass, friction, bounciness, group, mask);
rb.animated = animated;
rb.staticObj = ! active;
rb.staticObj = !active;
rb.isTriggerObject(trigger);
if(property1 == 'true')
{
if (property1) {
rb.linearDamping = linDamp;
rb.angularDamping = angDamp;
if(margin) rb.collisionMargin = marginLen;
if(useDeactiv) {
if (margin) rb.collisionMargin = marginLen;
if (useDeactiv) {
rb.setUpDeactivation(true, linearVelThreshold, angVelThreshold, 0.0);
}
}
object.addTrait(rb);

View file

@ -1,5 +1,7 @@
package armory.logicnode;
import iron.math.Quat;
import iron.math.Vec4;
import iron.object.Object;
import iron.object.BoneAnimation;
import iron.math.Mat4;
@ -26,19 +28,28 @@ class BoneFKNode extends LogicNode {
var anim = object.animation != null ? cast(object.animation, BoneAnimation) : null;
if (anim == null) anim = object.getParentArmature(object.name);
// Manipulating bone in world space
// Get bone in armature
var bone = anim.getBone(boneName);
m = anim.getBoneMat(bone);
w = anim.getAbsMat(bone);
function moveBone() {
m.setFrom(w);
m.multmat(transform);
iw.getInverse(w);
m.multmat(iw);
var t2 = Mat4.identity();
var loc= new Vec4();
var rot = new Quat();
var scl = new Vec4();
// anim.removeUpdate(moveBone);
// notified = false;
//Set scale to Armature scale. Bone scaling not yet implemented
t2.setFrom(transform);
t2.decompose(loc, rot, scl);
scl = object.transform.world.getScale();
t2.compose(loc, rot, scl);
//Set the bone local transform from world transform
anim.setBoneMatFromWorldMat(t2, bone);
//Remove this method from animation loop after FK
anim.removeUpdate(moveBone);
notified = false;
}
if (!notified) {

View file

@ -7,6 +7,13 @@ import iron.math.Vec4;
class BoneIKNode extends LogicNode {
var goal: Vec4;
var pole: Vec4;
var poleEnabled: Bool;
var chainLength: Int;
var maxIterartions: Int;
var precision: Float;
var rollAngle: Float;
var notified = false;
public function new(tree: LogicTree) {
@ -19,6 +26,12 @@ class BoneIKNode extends LogicNode {
var object: Object = inputs[1].get();
var boneName: String = inputs[2].get();
goal = inputs[3].get();
poleEnabled = inputs[4].get();
pole = inputs[5].get();
chainLength = inputs[6].get();
maxIterartions = inputs[7].get();
precision = inputs[8].get();
rollAngle = inputs[9].get();
if (object == null || goal == null) return;
var anim = object.animation != null ? cast(object.animation, BoneAnimation) : null;
@ -26,11 +39,15 @@ class BoneIKNode extends LogicNode {
var bone = anim.getBone(boneName);
function solveBone() {
anim.solveIK(bone, goal);
if(! poleEnabled) pole = null;
// anim.removeUpdate(solveBone);
// notified = false;
function solveBone() {
//Solve IK
anim.solveIK(bone, goal, precision, maxIterartions, chainLength, pole, rollAngle);
//Remove this method from animation loop after IK
anim.removeUpdate(solveBone);
notified = false;
}
if (!notified) {

View file

@ -1,18 +1,18 @@
package armory.logicnode;
import kha.FastFloat;
class ClampNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var value: kha.FastFloat = inputs[0].get();
var min: kha.FastFloat = inputs[1].get();
var max: kha.FastFloat = inputs[2].get();
override function get(from: Int): FastFloat {
var value = inputs[0].get();
var min = inputs[1].get();
var max = inputs[2].get();
if (value == null || min == null || max == null) return null;
value <= min ? return min : value >= max ? return max : return value;
return value < min ? min : value > max ? max : value;
}
}

View file

@ -19,9 +19,9 @@ class CompareNode extends LogicNode {
switch (property0) {
case "Equal":
cond = Std.is(v1, Vec4) ? v1.equals(v2) : v1 == v2;
cond = Std.isOfType(v1, Vec4) ? v1.equals(v2) : v1 == v2;
case "Almost Equal":
cond = Std.is(v1, Vec4) ? v1.almostEquals(v2, property1) : Math.abs(v1 - v2) < property1;
cond = Std.isOfType(v1, Vec4) ? v1.almostEquals(v2, property1) : Math.abs(v1 - v2) < property1;
case "Greater":
cond = v1 > v2;
case "Greater Equal":

View file

@ -18,9 +18,9 @@ class GateNode extends LogicNode {
switch (property0) {
case "Equal":
cond = Std.is(v1, Vec4) ? v1.equals(v2) : v1 == v2;
cond = Std.isOfType(v1, Vec4) ? v1.equals(v2) : v1 == v2;
case "Almost Equal":
cond = Std.is(v1, Vec4) ? v1.almostEquals(v2, property1) : Math.abs(v1 - v2) < property1;
cond = Std.isOfType(v1, Vec4) ? v1.almostEquals(v2, property1) : Math.abs(v1 - v2) < property1;
case "Greater":
cond = v1 > v2;
case "Greater Equal":

View file

@ -0,0 +1,31 @@
package armory.logicnode;
import iron.object.Object;
import iron.object.BoneAnimation;
class GetBoneFkIkOnlyNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Bool {
#if arm_skin
var object: Object = inputs[0].get();
var boneName: String = inputs[1].get();
if (object == null) return null;
var anim = object.animation != null ? cast(object.animation, BoneAnimation) : null;
if (anim == null) anim = object.getParentArmature(object.name);
// Get bone in armature
var bone = anim.getBone(boneName);
//Get bone transform in world coordinates
return bone.is_ik_fk_only;
#end
}
}

View file

@ -0,0 +1,30 @@
package armory.logicnode;
import iron.object.Object;
import iron.object.BoneAnimation;
import iron.math.Mat4;
class GetBoneTransformNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Mat4 {
#if arm_skin
var object: Object = inputs[0].get();
var boneName: String = inputs[1].get();
if (object == null) return null;
var anim = object.animation != null ? cast(object.animation, BoneAnimation) : null;
if (anim == null) anim = object.getParentArmature(object.name);
// Get bone in armature
var bone = anim.getBone(boneName);
return anim.getAbsWorldMat(bone);
#end
}
}

View file

@ -0,0 +1,33 @@
package armory.logicnode;
import iron.system.Input;
class GetGamepadStartedNode extends LogicNode {
var buttonStarted: Null<String>;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var g = Input.getGamepad(inputs[0].get());
buttonStarted = null;
for (b in Gamepad.buttons) {
if (g.started(b)) {
buttonStarted = b;
break;
}
}
if (buttonStarted != null) {
runOutput(0);
}
}
override function get(from: Int) {
return buttonStarted;
}
}

View file

@ -0,0 +1,24 @@
package armory.logicnode;
import armory.system.InputMap;
class GetInputMapKeyNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var inputMap = inputs[0].get();
var key = inputs[1].get();
var k = InputMap.getInputMapKey(inputMap, key);
if (k != null) {
if (from == 0) return k.scale;
else if (from == 1) return k.deadzone;
}
return null;
}
}

View file

@ -0,0 +1,32 @@
package armory.logicnode;
import iron.system.Input;
class GetKeyboardStartedNode extends LogicNode {
var kb = Input.getKeyboard();
var keyStarted: Null<String>;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
keyStarted = null;
for (k in Keyboard.keys) {
if (kb.started(k)) {
keyStarted = k;
break;
}
}
if (keyStarted != null) {
runOutput(0);
}
}
override function get(from: Int) {
return keyStarted;
}
}

View file

@ -10,9 +10,22 @@ class GetLocationNode extends LogicNode {
override function get(from: Int): Dynamic {
var object: Object = inputs[0].get();
var relative: Bool = inputs[1].get();
if (object == null) return null;
return object.transform.world.getLoc();
var loc = object.transform.world.getLoc();
if (relative && object.parent != null) {
loc.sub(object.parent.transform.world.getLoc()); // Add parent location influence
// Convert loc to parent local space
var dotX = loc.dot(object.parent.transform.right());
var dotY = loc.dot(object.parent.transform.look());
var dotZ = loc.dot(object.parent.transform.up());
loc.set(dotX, dotY, dotZ);
}
return loc;
}
}

View file

@ -0,0 +1,32 @@
package armory.logicnode;
import iron.system.Input;
class GetMouseStartedNode extends LogicNode {
var m = Input.getMouse();
var buttonStarted: Null<String>;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
buttonStarted = null;
for (b in Mouse.buttons) {
if (m.started(b)) {
buttonStarted = b;
break;
}
}
if (buttonStarted != null) {
runOutput(0);
}
}
override function get(from: Int) {
return buttonStarted;
}
}

View file

@ -14,36 +14,7 @@ class GetObjectNode extends LogicNode {
override function get(from: Int): Dynamic {
var objectName: String = inputs[0].get();
if (property0 == null || property0 == iron.Scene.active.raw.name) {
return iron.Scene.active.getChild(objectName);
}
#if arm_json
property0 += ".json";
#elseif arm_compress
property0 += ".lz4";
#end
var outObj: Null<Object> = null;
// Create the object in the active scene if it is from an inactive scene
iron.data.Data.getSceneRaw(property0, (rawScene: TSceneFormat) -> {
var objData: Null<TObj> = null;
for (o in rawScene.objects) {
if (o.name == objectName) {
objData = o;
break;
}
}
if (objData == null) return;
iron.Scene.active.createObject(objData, rawScene, null, null, (newObj: Object) -> {
outObj = newObj;
});
});
return outObj;
return iron.Scene.active.getChild(objectName);
}
}

View file

@ -3,22 +3,24 @@ package armory.logicnode;
class GetSystemName extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
super(tree);
}
public static function equalsCI(a : String, b : String) return a.toLowerCase() == b.toLowerCase();
override function get(from: Int): Dynamic {
var systemName: String = kha.System.systemId;
return switch (from) {
case 0: systemName;
case 1: equalsCI(kha.System.systemId, 'Windows');
case 2: equalsCI(kha.System.systemId, 'Linux');
case 3: equalsCI(kha.System.systemId, 'Mac');
case 4: equalsCI(kha.System.systemId, 'HTML5');
case 5: equalsCI(kha.System.systemId, 'Android');
case 1: equalsCI(systemName, 'Windows');
case 2: equalsCI(systemName, 'Linux');
case 3: equalsCI(systemName, 'Mac');
case 4: equalsCI(systemName, 'HTML5');
case 5: equalsCI(systemName, 'Android');
default: null;
}
}
static inline function equalsCI(a: String, b: String): Bool {
return a.toLowerCase() == b.toLowerCase();
}
}

View file

@ -16,12 +16,12 @@ class GetTraitNameNode extends LogicNode {
case 0: {
// Check CanvasScript
var cname = cast Type.resolveClass("armory.trait.internal.CanvasScript");
if (Std.is(trait, cname)) {
if (Std.isOfType(trait, cname)) {
return trait.cnvName;
}
// Check WasmScript
var cname = cast Type.resolveClass("armory.trait.internal.WasmScript");
if (Std.is(trait, cname)) {
if (Std.isOfType(trait, cname)) {
return trait.wasmName;
}
// Other

View file

@ -1,31 +1,136 @@
package armory.logicnode;
#if arm_patch @:keep @:keepSub #end
class LogicNode {
var tree: LogicTree;
var inputs: Array<LogicNodeInput> = [];
var outputs: Array<Array<LogicNode>> = [];
var inputs: Array<LogicNodeLink> = [];
var outputs: Array<Array<LogicNodeLink>> = [];
#if arm_debug
#if (arm_debug || arm_patch)
public var name = "";
public function watch(b: Bool) { // Watch in debug console
var nodes = armory.trait.internal.DebugConsole.watchNodes;
b ? nodes.push(this) : nodes.remove(this);
}
#if (arm_debug)
public function watch(b: Bool) { // Watch in debug console
var nodes = armory.trait.internal.DebugConsole.watchNodes;
b ? nodes.push(this) : nodes.remove(this);
}
#end
#end
public function new(tree: LogicTree) {
this.tree = tree;
}
public function addInput(node: LogicNode, from: Int) {
inputs.push(new LogicNodeInput(node, from));
/**
Resize the inputs array to a given size to minimize dynamic
reallocation and over-allocation later.
**/
inline function preallocInputs(amount: Int) {
this.inputs.resize(amount);
}
public function addOutputs(nodes: Array<LogicNode>) {
outputs.push(nodes);
/**
Resize the outputs array to a given size to minimize dynamic
reallocation and over-allocation later.
**/
inline function preallocOutputs(amount: Int) {
this.outputs.resize(amount);
for (i in 0...outputs.length) {
outputs[i] = [];
}
}
/**
Add a link between to nodes to the tree.
**/
public static function addLink(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int): LogicNodeLink {
var link = new LogicNodeLink(fromNode, toNode, fromIndex, toIndex);
if (toNode.inputs.length <= toIndex) {
toNode.inputs.resize(toIndex + 1);
}
toNode.inputs[toIndex] = link;
var fromNodeOuts = fromNode.outputs;
var outLen = fromNodeOuts.length;
if (outLen <= fromIndex) {
fromNodeOuts.resize(fromIndex + 1);
// Initialize with empty arrays
for (i in outLen...fromIndex + 1) {
fromNodeOuts[i] = [];
}
}
fromNodeOuts[fromIndex].push(link);
return link;
}
#if arm_patch
/**
Removes a link from the tree.
**/
static function removeLink(link: LogicNodeLink) {
link.fromNode.outputs[link.fromIndex].remove(link);
// Reuse the same link and connect a default input node to it.
// That's why this function is only available in arm_patch mode, we need
// access to the link's type and value.
link.fromNode = LogicNode.createSocketDefaultNode(link.toNode.tree, link.toType, link.toValue);
link.fromIndex = 0;
}
/**
Removes all inputs and their links from this node.
Warning: this function changes the amount of node inputs to 0!
**/
function clearInputs() {
for (link in inputs) {
link.fromNode.outputs[link.fromIndex].remove(link);
}
inputs.resize(0);
}
/**
Removes all outputs and their links from this node.
Warning: this function changes the amount of node inputs to 0!
**/
function clearOutputs() {
for (links in outputs) {
for (link in links) {
var defaultNode = LogicNode.createSocketDefaultNode(tree, link.toType, link.toValue);
link.fromNode = defaultNode;
link.fromIndex = 0;
defaultNode.outputs[0] = [link];
}
}
outputs.resize(0);
}
/**
Creates a default node for a socket so that get() and set() can be
used without null checks.
Loosely equivalent to `make_logic.build_default_node()` in Python.
**/
static inline function createSocketDefaultNode(tree: LogicTree, socketType: String, value: Dynamic): LogicNode {
// Make sure to not add these nodes to the LogicTree.nodes array as they
// won't be garbage collected then if unlinked later.
return switch (socketType) {
case "VECTOR": new armory.logicnode.VectorNode(tree, value[0], value[1], value[2]);
case "RGBA": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2], value[3]);
case "RGB": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2]);
case "VALUE": new armory.logicnode.FloatNode(tree, value);
case "INT": new armory.logicnode.IntegerNode(tree, value);
case "BOOLEAN": new armory.logicnode.BooleanNode(tree, value);
case "STRING": new armory.logicnode.StringNode(tree, value);
case "NONE": new armory.logicnode.NullNode(tree);
case "OBJECT": new armory.logicnode.ObjectNode(tree, value);
default: new armory.logicnode.DynamicNode(tree, value);
}
}
#end
/**
Called when this node is activated.
@param from impulse index
@ -38,42 +143,45 @@ class LogicNode {
**/
function runOutput(i: Int) {
if (i >= outputs.length) return;
for (o in outputs[i]) {
// Check which input activated the node
for (j in 0...o.inputs.length) {
if (o.inputs[j].node == this) {
o.run(j);
break;
}
}
for (outLink in outputs[i]) {
outLink.toNode.run(outLink.toIndex);
}
}
@:allow(armory.logicnode.LogicNodeInput)
@:allow(armory.logicnode.LogicNodeLink)
function get(from: Int): Dynamic { return this; }
@:allow(armory.logicnode.LogicNodeInput)
@:allow(armory.logicnode.LogicNodeLink)
function set(value: Dynamic) {}
}
class LogicNodeInput {
@:allow(armory.logicnode.LogicNode)
@:allow(armory.logicnode.LogicTree)
class LogicNodeLink {
@:allow(armory.logicnode.LogicNode)
var node: LogicNode;
var from: Int; // Socket index
var fromNode: LogicNode;
var toNode: LogicNode;
var fromIndex: Int;
var toIndex: Int;
public function new(node: LogicNode, from: Int) {
this.node = node;
this.from = from;
#if arm_patch
var fromType: String;
var toType: String;
var toValue: Dynamic;
#end
inline function new(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int) {
this.fromNode = fromNode;
this.toNode = toNode;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@:allow(armory.logicnode.LogicNode)
function get(): Dynamic {
return node.get(from);
inline function get(): Dynamic {
return fromNode.get(fromIndex);
}
@:allow(armory.logicnode.LogicNode)
function set(value: Dynamic) {
node.set(value);
inline function set(value: Dynamic) {
fromNode.set(value);
}
}

View file

@ -2,10 +2,26 @@ package armory.logicnode;
class LogicTree extends iron.Trait {
#if arm_patch
/**
Stores all trait instances of the tree via its name.
**/
public static var nodeTrees = new Map<String, Array<LogicTree>>();
/**
[node name => logic node] for later node replacement for live patching.
**/
public var nodes: Map<String, LogicNode>;
#end
public var loopBreak = false; // Trigger break from loop nodes
public function new() {
super();
#if arm_patch
nodes = new Map<String, LogicNode>();
#end
}
public function add() {}

View file

@ -3,7 +3,7 @@ package armory.logicnode;
class MathNode extends LogicNode {
public var property0: String; // Operation
public var property1: String; // Clamp
public var property1: Bool; // Clamp
public function new(tree: LogicTree) {
super(tree);
@ -80,8 +80,8 @@ class MathNode extends LogicNode {
}
}
// Clamp
if (property1 == "true") r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r);
if (property1) r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r);
return r;
}
}
}

View file

@ -2,11 +2,31 @@ package armory.logicnode;
class MergeNode extends LogicNode {
/** Execution mode. **/
public var property0: String;
var lastInputIndex = -1;
public function new(tree: LogicTree) {
super(tree);
tree.notifyOnLateUpdate(lateUpdate);
}
override function run(from: Int) {
// Check if there already were executions on the same frame
if (lastInputIndex != -1 && property0 == "once_per_frame") {
return;
}
lastInputIndex = from;
runOutput(0);
}
override function get(from: Int): Dynamic {
return lastInputIndex;
}
function lateUpdate() {
lastInputIndex = -1;
}
}

View file

@ -6,7 +6,7 @@ class MixNode extends LogicNode {
public var property0: String; // Type
public var property1: String; // Ease
public var property2: String; // Clamp
public var property2: Bool; // Clamp
var ease: Float->Float = null;
@ -50,7 +50,9 @@ class MixNode extends LogicNode {
var v2: Float = inputs[2].get();
var f = v1 + (v2 - v1) * ease(k);
if (property2 == "true") f = f < 0 ? 0 : f > 1 ? 1 : f;
// Clamp
if (property2) f = f < 0 ? 0 : f > 1 ? 1 : f;
return f;
}
}

View file

@ -21,7 +21,7 @@ class ObjectNode extends LogicNode {
override function set(value: Dynamic) {
if (inputs.length > 0) inputs[0].set(value);
else {
objectName = value.name;
objectName = value != null ? value.name : "";
this.value = value;
}
}

View file

@ -4,7 +4,7 @@ import armory.trait.internal.CanvasScript;
import iron.Scene;
#if arm_ui
import zui.Canvas.Anchor;
import armory.ui.Canvas.Anchor;
#end
class OnCanvasElementNode extends LogicNode {

View file

@ -0,0 +1,35 @@
package armory.logicnode;
import armory.system.InputMap;
class OnInputMapNode extends LogicNode {
var inputMap: Null<InputMap>;
public function new(tree: LogicTree) {
super(tree);
tree.notifyOnUpdate(update);
}
function update() {
var i = inputs[0].get();
inputMap = InputMap.getInputMap(i);
if (inputMap != null) {
if (inputMap.started()) {
runOutput(0);
}
if (inputMap.released()) {
runOutput(1);
}
}
}
override function get(from: Int): Dynamic {
if (from == 2) return inputMap.value();
else return inputMap.lastKeyPressed;
}
}

View file

@ -0,0 +1,22 @@
package armory.logicnode;
class OncePerFrameNode extends LogicNode {
var c = false;
public function new(tree: LogicTree) {
super(tree);
tree.notifyOnUpdate(update);
}
override function run(from: Int) {
if(c) {
c = false;
runOutput(0);
}
}
function update() {
c = true;
}
}

View file

@ -8,7 +8,7 @@ class PauseTraitNode extends LogicNode {
override function run(from: Int) {
var trait: Dynamic = inputs[1].get();
if (trait == null || !Std.is(trait, LogicTree)) return;
if (trait == null || !Std.isOfType(trait, LogicTree)) return;
cast(trait, LogicTree).pause();

View file

@ -3,50 +3,39 @@ package armory.logicnode;
#if arm_physics
import armory.trait.physics.bullet.PhysicsConstraint.ConstraintAxis;
#end
import iron.object.Object;
class PhysicsConstraintNode extends LogicNode {
public var property0: String;//Linear or Angular
public var property1: String;//Axis
public var property2: String;//Is a spring
public var value1: Float;//Lower limit or Spring Stiffness
public var value2: Float;//Upper limit or Spring Damping
public var property0: String; //Linear or Angular
public var property1: String; //Axis
public var property2: Bool; //Is a spring
#if arm_physics
public var value1: Float; //Lower limit or Spring Stiffness
public var value2: Float; //Upper limit or Spring Damping
public var isAngular: Bool;
public var axis: ConstraintAxis;
public var isSpring: Bool;
#end
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): PhysicsConstraintNode {
#if arm_physics
value1 = inputs[0].get();
value2 = inputs[1].get();
if(property0 == 'Linear') {
isAngular = false;
}
else{
isAngular = true;
}
isAngular = property0 != "Linear";
isSpring = property2;
if(property2 == 'true'){
isSpring = true;
switch (property1) {
case "X": axis = X;
case "Y": axis = Y;
case "Z": axis = Z;
}
else {
isSpring = false;
}
switch (property1){
case 'X':
axis = X;
case 'Y':
axis = Y;
case 'Z':
axis = Z;
}
#end
return this;
}
}

View file

@ -11,10 +11,10 @@ class QuaternionNode extends LogicNode {
super(tree);
if (x != null) {
addInput(new FloatNode(tree, x), 0);
addInput(new FloatNode(tree, y), 0);
addInput(new FloatNode(tree, z), 0);
addInput(new FloatNode(tree, w), 0);
LogicNode.addLink(new FloatNode(tree, x), this, 0, 0);
LogicNode.addLink(new FloatNode(tree, y), this, 0, 1);
LogicNode.addLink(new FloatNode(tree, z), this, 0, 2);
LogicNode.addLink(new FloatNode(tree, w), this, 0, 3);
}
}
@ -27,15 +27,15 @@ class QuaternionNode extends LogicNode {
switch (from){
case 0:
return value;
case 1:
var value1 = new Vec4();
case 1:
var value1 = new Vec4();
value1.x = value.x;
value1.y = value.y;
value1.z = value.z;
value1.w = 0; // use 0 to avoid this vector being translated.
return value1;
case 2:
return value.w;
return value.w;
default:
return null;
}

View file

@ -0,0 +1,19 @@
package armory.logicnode;
import armory.system.InputMap;
class RemoveInputMapKeyNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var inputMap = inputs[1].get();
var key = inputs[2].get();
if (InputMap.removeInputMapKey(inputMap, key)) {
runOutput(0);
}
}
}

View file

@ -8,7 +8,7 @@ class ResumeTraitNode extends LogicNode {
override function run(from: Int) {
var trait: Dynamic = inputs[1].get();
if (trait == null || !Std.is(trait, LogicTree)) return;
if (trait == null || !Std.isOfType(trait, LogicTree)) return;
cast(trait, LogicTree).resume();

View file

@ -0,0 +1,37 @@
package armory.logicnode;
class SelectNode extends LogicNode {
/** Execution mode. **/
public var property0: String;
var value: Dynamic = null;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
// Get value according to the activated input (run() can only be called
// if the execution mode is from_input).
value = inputs[from + Std.int(inputs.length / 2)].get();
runOutput(0);
}
override function get(from: Int): Dynamic {
if (property0 == "from_index") {
var index = inputs[0].get() + 2;
// Return default value for invalid index
if (index < 2 || index >= inputs.length) {
return inputs[1].get();
}
return inputs[index].get();
}
// from_input
return value;
}
}

View file

@ -0,0 +1,35 @@
package armory.logicnode;
import iron.math.Quat;
import iron.math.Vec4;
import iron.object.Object;
import iron.object.BoneAnimation;
class SetBoneFkIkOnlyNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if arm_skin
var object: Object = inputs[1].get();
var boneName: String = inputs[2].get();
var fk_ik_only: Bool = inputs[3].get();
if (object == null) return;
var anim = object.animation != null ? cast(object.animation, BoneAnimation) : null;
if (anim == null) anim = object.getParentArmature(object.name);
// Get bone in armature
var bone = anim.getBone(boneName);
//Set bone animated by FK or IK only
bone.is_ik_fk_only = fk_ik_only;
runOutput(0);
#end
}
}

View file

@ -0,0 +1,46 @@
package armory.logicnode;
import armory.system.InputMap;
class SetInputMapKeyNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var inputMap = inputs[1].get();
var key = inputs[2].get();
var scale = inputs[3].get();
var deadzone = inputs[4].get();
var index = inputs[5].get();
var i = InputMap.getInputMap(inputMap);
if (i == null) {
i = InputMap.addInputMap(inputMap);
}
var k = InputMap.getInputMapKey(inputMap, key);
if (k == null) {
switch(property0) {
case "keyboard": k = i.addKeyboard(key, scale);
case "mouse": k = i.addMouse(key, scale, deadzone);
case "gamepad": {
k = i.addGamepad(key, scale, deadzone);
k.setIndex(index);
}
}
} else {
k.scale = scale;
k.deadzone = deadzone;
k.setIndex(index);
}
runOutput(0);
}
}

View file

@ -13,9 +13,21 @@ class SetLocationNode extends LogicNode {
override function run(from: Int) {
var object: Object = inputs[1].get();
var vec: Vec4 = inputs[2].get();
var relative: Bool = inputs[3].get();
if (object == null || vec == null) return;
if (!relative && object.parent != null) {
var loc = vec.clone();
loc.sub(object.parent.transform.world.getLoc()); // Remove parent location influence
// Convert vec to parent local space
var dotX = loc.dot(object.parent.transform.right());
var dotY = loc.dot(object.parent.transform.look());
var dotZ = loc.dot(object.parent.transform.up());
vec.set(dotX, dotY, dotZ);
}
object.transform.loc.setFrom(vec);
object.transform.buildMatrix();
@ -26,4 +38,4 @@ class SetLocationNode extends LogicNode {
runOutput(0);
}
}
}

View file

@ -1,40 +1,40 @@
package armory.logicnode;
import iron.Scene;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialImageParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, kha.Image>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalTextureLinks.push(textureLink);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var perObject: Null<Bool>;
var object = inputs[1].get();
if(object == null) return;
perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Texture);
object = Scene.active.root;
}
iron.data.Data.getImage(inputs[3].get(), function(image: kha.Image) {
entry.set(inputs[2].get(), image); // Node name, value
var img = inputs[5].get();
if(img == null) return;
iron.data.Data.getImage(img, function(image: kha.Image) {
UniformsManager.setTextureValue(mat, object, inputs[4].get(), image);
});
runOutput(0);
}
static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -1,38 +1,35 @@
package armory.logicnode;
import iron.Scene;
import iron.math.Vec4;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialRgbParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, Vec4>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalVec3Links.push(vec3Link);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var perObject: Null<Bool>;
var object = inputs[1].get();
if(object == null) return;
perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Vector);
object = Scene.active.root;
}
entry.set(inputs[2].get(), inputs[3].get()); // Node name, value
UniformsManager.setVec3Value(mat, object, inputs[4].get(), inputs[5].get());
runOutput(0);
}
static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -1,37 +1,35 @@
package armory.logicnode;
import iron.Scene;
import iron.data.MaterialData;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class SetMaterialValueParamNode extends LogicNode {
static var registered = false;
static var map = new Map<MaterialData, Map<String, Null<kha.FastFloat>>>();
public function new(tree: LogicTree) {
super(tree);
if (!registered) {
registered = true;
iron.object.Uniforms.externalFloatLinks.push(floatLink);
}
}
override function run(from: Int) {
var mat = inputs[1].get();
if (mat == null) return;
var entry = map.get(mat);
if (entry == null) {
entry = new Map();
map.set(mat, entry);
var perObject: Null<Bool>;
var object = inputs[1].get();
if(object == null) return;
perObject = inputs[2].get();
if(perObject == null) perObject = false;
var mat = inputs[3].get();
if(mat == null) return;
if(! perObject){
UniformsManager.removeObjectFromMap(object, Float);
object = Scene.active.root;
}
entry.set(inputs[2].get(), inputs[3].get()); // Node name, value
UniformsManager.setFloatValue(mat, object, inputs[4].get(), inputs[5].get());
runOutput(0);
}
static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
if (mat == null) return null;
var entry = map.get(mat);
if (entry == null) return null;
return entry.get(link);
}
}

View file

@ -14,8 +14,9 @@ class SetParentNode extends LogicNode {
var parent: Object;
var isUnparent = false;
if (Std.is(inputs[2].node, ObjectNode)) {
var parentNode = cast(inputs[2].node, ObjectNode);
if (Std.isOfType(inputs[2].fromNode, ObjectNode)) {
var parentNode = cast(inputs[2].fromNode, ObjectNode);
isUnparent = parentNode.objectName == "";
}
if (isUnparent) parent = iron.Scene.active.root;
@ -24,7 +25,7 @@ class SetParentNode extends LogicNode {
if (object == null || parent == null || object.parent == parent) return;
object.parent.removeChild(object, isUnparent); // keepTransform
#if arm_physics
var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) rigidBody.setActivationState(0);

View file

@ -10,7 +10,7 @@ class SetTraitPausedNode extends LogicNode {
var trait: Dynamic = inputs[1].get();
var paused: Bool = inputs[2].get();
if (trait == null || !Std.is(trait, LogicTree)) return;
if (trait == null || !Std.isOfType(trait, LogicTree)) return;
paused ? cast(trait, LogicTree).pause() : cast(trait, LogicTree).resume();

View file

@ -0,0 +1,72 @@
package armory.logicnode;
import iron.data.SceneFormat.TSceneFormat;
import iron.data.Data;
import iron.object.Object;
import iron.math.Mat4;
import armory.trait.physics.RigidBody;
class SpawnObjectByNameNode extends LogicNode {
var object: Object;
var matrices: Array<Mat4> = [];
/** Scene from which to take the object **/
public var property0: Null<String>;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var objectName = inputs[1].get();
if (objectName == null) return;
#if arm_json
property0 += ".json";
#elseif arm_compress
property0 += ".lz4";
#end
var m: Mat4 = inputs[2].get();
matrices.push(m != null ? m.clone() : null);
var spawnChildren: Bool = inputs.length > 3 ? inputs[3].get() : true; // TODO
Data.getSceneRaw(property0, (rawScene: TSceneFormat) -> {
//Check if object with given name present in the specified scene
var objPresent: Bool = false;
for (o in rawScene.objects) {
if (o.name == objectName) {
objPresent = true;
break;
}
}
if (! objPresent) return;
//Spawn object if present
iron.Scene.active.spawnObject(objectName, null, function(o: Object) {
object = o;
var matrix = matrices.pop(); // Async spawn in a loop, order is non-stable
if (matrix != null) {
object.transform.setMatrix(matrix);
#if arm_physics
var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) {
object.transform.buildMatrix();
rigidBody.syncTransform();
}
#end
}
object.visible = true;
runOutput(0);
}, spawnChildren, rawScene);
});
}
override function get(from: Int): Dynamic {
return object;
}
}

View file

@ -7,7 +7,7 @@ class VectorMixNode extends LogicNode {
public var property0: String; // Type
public var property1: String; // Ease
public var property2: String; // Clamp
public var property2: Bool; // Clamp
var v = new Vec4();
@ -57,7 +57,7 @@ class VectorMixNode extends LogicNode {
v.y = v1.y + (v2.y - v1.y) * f;
v.z = v1.z + (v2.z - v1.z) * f;
if (property2 == "true") v.clamp(0, 1);
if (property2) v.clamp(0, 1);
return v;
}
}

View file

@ -10,9 +10,9 @@ class VectorNode extends LogicNode {
super(tree);
if (x != null) {
addInput(new FloatNode(tree, x), 0);
addInput(new FloatNode(tree, y), 0);
addInput(new FloatNode(tree, z), 0);
LogicNode.addLink(new FloatNode(tree, x), this, 0, 0);
LogicNode.addLink(new FloatNode(tree, y), this, 0, 1);
LogicNode.addLink(new FloatNode(tree, z), this, 0, 2);
}
}

View file

@ -3,8 +3,6 @@ package armory.logicnode;
import iron.object.Object;
import iron.math.Vec4;
using armory.object.TransformExtension;
class VectorToObjectOrientationNode extends LogicNode {
public function new(tree: LogicTree) {
@ -18,7 +16,7 @@ class VectorToObjectOrientationNode extends LogicNode {
if (object == null || vec == null) return null;
return object.transform.worldVecToOrientation(vec);
return vec.applyQuat(object.transform.rot);
}
}

View file

@ -3,8 +3,6 @@ package armory.logicnode;
import iron.math.Vec4;
import iron.object.Object;
using armory.object.TransformExtension;
class WorldVectorToLocalSpaceNode extends LogicNode {
public function new(tree: LogicTree) {
@ -17,7 +15,8 @@ class WorldVectorToLocalSpaceNode extends LogicNode {
if (object == null || worldVec == null) return null;
var localVec: Vec4 = new Vec4();
var localVec = new Vec4();
localVec.sub(object.transform.world.getLoc());
localVec.x = worldVec.dot(object.transform.right());
localVec.y = worldVec.dot(object.transform.look());

View file

@ -72,4 +72,13 @@ class Helper {
if (value <= leftMin) return rightMin;
return map(value, leftMin, leftMax, rightMin, rightMax);
}
/**
Return the sign of the given value represented as `1.0` (positive value)
or `-1.0` (negative value). The sign of `0` is `0`.
**/
public static inline function sign(value: Float): Float {
if (value == 0) return 0;
return (value < 0) ? -1.0 : 1.0;
}
}

View file

@ -11,176 +11,204 @@ class Uniforms {
public static function register() {
iron.object.Uniforms.externalTextureLinks = [textureLink];
iron.object.Uniforms.externalVec2Links = [];
iron.object.Uniforms.externalVec2Links = [vec2Link];
iron.object.Uniforms.externalVec3Links = [vec3Link];
iron.object.Uniforms.externalVec4Links = [];
iron.object.Uniforms.externalFloatLinks = [floatLink];
iron.object.Uniforms.externalIntLinks = [];
}
public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
#if arm_ltc
if (link == "_ltcMat") {
if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC();
return armory.data.ConstData.ltcMatTex;
public static function textureLink(object: Object, mat: MaterialData, link: String): Null<kha.Image> {
switch (link) {
case "_nishitaLUT": {
if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world);
return armory.renderpath.Nishita.data.lut;
}
#if arm_ltc
case "_ltcMat": {
if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC();
return armory.data.ConstData.ltcMatTex;
}
case "_ltcMag": {
if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC();
return armory.data.ConstData.ltcMagTex;
}
#end
}
else if (link == "_ltcMag") {
if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC();
return armory.data.ConstData.ltcMagTex;
}
#end
var target = iron.RenderPath.active.renderTargets.get(link.endsWith("_depth") ? link.substr(0, link.length - 6) : link);
return target != null ? target.image : null;
}
public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
public static function vec3Link(object: Object, mat: MaterialData, link: String): Null<iron.math.Vec4> {
var v: Vec4 = null;
#if arm_hosek
if (link == "_hosekA") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
switch (link) {
#if arm_hosek
case "_hosekA": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.A.x;
v.y = armory.renderpath.HosekWilkie.data.A.y;
v.z = armory.renderpath.HosekWilkie.data.A.z;
}
}
if (armory.renderpath.HosekWilkie.data != null) {
case "_hosekB": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.B.x;
v.y = armory.renderpath.HosekWilkie.data.B.y;
v.z = armory.renderpath.HosekWilkie.data.B.z;
}
}
case "_hosekC": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.C.x;
v.y = armory.renderpath.HosekWilkie.data.C.y;
v.z = armory.renderpath.HosekWilkie.data.C.z;
}
}
case "_hosekD": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.D.x;
v.y = armory.renderpath.HosekWilkie.data.D.y;
v.z = armory.renderpath.HosekWilkie.data.D.z;
}
}
case "_hosekE": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.E.x;
v.y = armory.renderpath.HosekWilkie.data.E.y;
v.z = armory.renderpath.HosekWilkie.data.E.z;
}
}
case "_hosekF": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.F.x;
v.y = armory.renderpath.HosekWilkie.data.F.y;
v.z = armory.renderpath.HosekWilkie.data.F.z;
}
}
case "_hosekG": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.G.x;
v.y = armory.renderpath.HosekWilkie.data.G.y;
v.z = armory.renderpath.HosekWilkie.data.G.z;
}
}
case "_hosekH": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.H.x;
v.y = armory.renderpath.HosekWilkie.data.H.y;
v.z = armory.renderpath.HosekWilkie.data.H.z;
}
}
case "_hosekI": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.I.x;
v.y = armory.renderpath.HosekWilkie.data.I.y;
v.z = armory.renderpath.HosekWilkie.data.I.z;
}
}
case "_hosekZ": {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.Z.x;
v.y = armory.renderpath.HosekWilkie.data.Z.y;
v.z = armory.renderpath.HosekWilkie.data.Z.z;
}
}
#end
#if rp_voxelao
case "_cameraPositionSnap": {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.A.x;
v.y = armory.renderpath.HosekWilkie.data.A.y;
v.z = armory.renderpath.HosekWilkie.data.A.z;
var camera = iron.Scene.active.camera;
v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz());
var l = camera.lookWorld();
var e = Main.voxelgiHalfExtents;
v.x += l.x * e * 0.9;
v.y += l.y * e * 0.9;
var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range
v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f);
}
#end
}
return v;
}
public static function vec2Link(object: Object, mat: MaterialData, link: String): Null<iron.math.Vec4> {
var v: Vec4 = null;
switch (link) {
case "_nishitaDensity": {
var w = Scene.active.world;
if (w != null) {
v = iron.object.Uniforms.helpVec;
// We only need Rayleigh and Mie density in the sky shader -> Vec2
v.x = w.raw.nishita_density[0];
v.y = w.raw.nishita_density[1];
}
}
}
else if (link == "_hosekB") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.B.x;
v.y = armory.renderpath.HosekWilkie.data.B.y;
v.z = armory.renderpath.HosekWilkie.data.B.z;
}
}
else if (link == "_hosekC") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.C.x;
v.y = armory.renderpath.HosekWilkie.data.C.y;
v.z = armory.renderpath.HosekWilkie.data.C.z;
}
}
else if (link == "_hosekD") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.D.x;
v.y = armory.renderpath.HosekWilkie.data.D.y;
v.z = armory.renderpath.HosekWilkie.data.D.z;
}
}
else if (link == "_hosekE") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.E.x;
v.y = armory.renderpath.HosekWilkie.data.E.y;
v.z = armory.renderpath.HosekWilkie.data.E.z;
}
}
else if (link == "_hosekF") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.F.x;
v.y = armory.renderpath.HosekWilkie.data.F.y;
v.z = armory.renderpath.HosekWilkie.data.F.z;
}
}
else if (link == "_hosekG") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.G.x;
v.y = armory.renderpath.HosekWilkie.data.G.y;
v.z = armory.renderpath.HosekWilkie.data.G.z;
}
}
else if (link == "_hosekH") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.H.x;
v.y = armory.renderpath.HosekWilkie.data.H.y;
v.z = armory.renderpath.HosekWilkie.data.H.z;
}
}
else if (link == "_hosekI") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.I.x;
v.y = armory.renderpath.HosekWilkie.data.I.y;
v.z = armory.renderpath.HosekWilkie.data.I.z;
}
}
else if (link == "_hosekZ") {
if (armory.renderpath.HosekWilkie.data == null) {
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
}
if (armory.renderpath.HosekWilkie.data != null) {
v = iron.object.Uniforms.helpVec;
v.x = armory.renderpath.HosekWilkie.data.Z.x;
v.y = armory.renderpath.HosekWilkie.data.Z.y;
v.z = armory.renderpath.HosekWilkie.data.Z.z;
}
}
#end
#if rp_voxelao
if (link == "_cameraPositionSnap") {
v = iron.object.Uniforms.helpVec;
var camera = iron.Scene.active.camera;
v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz());
var l = camera.lookWorld();
var e = Main.voxelgiHalfExtents;
v.x += l.x * e * 0.9;
v.y += l.y * e * 0.9;
var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range
v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f);
}
#end
return v;
}
public static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
#if rp_dynres
if (link == "_dynamicScale") {
return armory.renderpath.DynamicResolutionScale.dynamicScale;
switch (link) {
#if rp_dynres
case "_dynamicScale": {
return armory.renderpath.DynamicResolutionScale.dynamicScale;
}
#end
#if arm_debug
case "_debugFloat": {
return armory.trait.internal.DebugConsole.debugFloat;
}
#end
#if rp_voxelao
case "_voxelBlend": { // Blend current and last voxels
var freq = armory.renderpath.RenderPathCreator.voxelFreq;
return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq;
}
#end
}
#end
#if arm_debug
if (link == "_debugFloat") {
return armory.trait.internal.DebugConsole.debugFloat;
}
#end
#if rp_voxelao
if (link == "_voxelBlend") { // Blend current and last voxels
var freq = armory.renderpath.RenderPathCreator.voxelFreq;
return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq;
}
#end
return null;
}
}

View file

@ -57,7 +57,11 @@ class Inc {
break;
if (LightObject.discardLightCulled(light)) continue;
if (light.data.raw.type == "point") {
for(k in 0...light.tileOffsetX.length) {
if (!light.data.raw.cast_shadow) {
j += 4 * 6;
continue;
}
for(k in 0...6) {
LightObject.pointLightsData[j ] = light.tileOffsetX[k]; // posx
LightObject.pointLightsData[j + 1] = light.tileOffsetY[k]; // posy
LightObject.pointLightsData[j + 2] = light.tileScale[k]; // tile scale factor relative to atlas
@ -108,10 +112,17 @@ class Inc {
lastFrame = RenderPath.active.frame;
#end
// add new lights to the atlases
#if arm_debug
beginShadowsLogicProfile();
// reset data on rejected lights
for (atlas in ShadowMapAtlas.shadowMapAtlases) {
atlas.rejectedLights = [];
}
#end
for (light in iron.Scene.active.lights) {
if (!light.lightInAtlas && !light.culledLight && light.visible && light.shadowMapScale > 0.0
&& light.data.raw.strength > 0.0 && light.data.raw.cast_shadow) {
light.lightInAtlas = ShadowMapAtlas.addLight(light);
ShadowMapAtlas.addLight(light);
}
}
// update point light data before rendering
@ -151,8 +162,8 @@ class Inc {
// set the tile offset for this tile and every linked tile to this one
var j = 0;
tile.forEachTileLinked(function (lTile) {
tile.light.tileOffsetX[j] = (lTile.coordsX == 0) ? 0.0 : lTile.coordsX / atlas.sizew;
tile.light.tileOffsetY[j] = (lTile.coordsY == 0) ? 0.0 : lTile.coordsY / atlas.sizew;
tile.light.tileOffsetX[j] = lTile.coordsX / atlas.sizew;
tile.light.tileOffsetY[j] = lTile.coordsY / atlas.sizew;
tile.light.tileScale[j] = lTile.size / atlas.sizew;
j++;
});
@ -164,6 +175,9 @@ class Inc {
var face = 0;
var faces = ShadowMapTile.tilesLightType(tile.light.data.raw.type);
#if arm_debug
beginShadowsRenderProfile();
#end
tile.forEachTileLinked(function (lTile) {
if (faces > 1) {
#if arm_csm
@ -180,6 +194,9 @@ class Inc {
path.drawMeshesStream("shadowmap");
});
#if arm_debug
endShadowsRenderProfile();
#end
path.currentFace = -1;
}
@ -202,6 +219,9 @@ class Inc {
tile.freeTile();
}
}
#if arm_debug
endShadowsLogicProfile();
#end
#end // rp_shadowmap
}
#else
@ -517,6 +537,25 @@ class Inc {
return null;
#end
}
#if arm_debug
public static var shadowsLogicTime = 0.0;
public static var shadowsRenderTime = 0.0;
static var startShadowsLogicTime = 0.0;
static var startShadowsRenderTime = 0.0;
static var callBackSetup = false;
static function setupEndFrameCallback() {
if (!callBackSetup) {
callBackSetup = true;
iron.App.endFrameCallbacks.push(endFrame);
}
}
static function beginShadowsLogicProfile() { setupEndFrameCallback(); startShadowsLogicTime = kha.Scheduler.realTime(); }
static function beginShadowsRenderProfile() { startShadowsRenderTime = kha.Scheduler.realTime(); }
static function endShadowsLogicProfile() { shadowsLogicTime += kha.Scheduler.realTime() - startShadowsLogicTime - shadowsRenderTime; }
static function endShadowsRenderProfile() { shadowsRenderTime += kha.Scheduler.realTime() - startShadowsRenderTime; }
public static function endFrame() { shadowsLogicTime = 0; shadowsRenderTime = 0; }
#end
}
#if arm_shadowmap_atlas
@ -534,12 +573,17 @@ class ShadowMapAtlas {
public var activeTiles: Array<ShadowMapTile> = [];
public var depth = 1;
#if arm_shadowmap_atlas_lod
static var tileSizes: Array<Int> = [];
static var tileSizeFactor: Array<Float> = [];
var tileSizes: Array<Int> = [];
var tileSizeFactor: Array<Float> = [];
#end
public var updateRenderTarget = false;
public static var shadowMapAtlases:Map<String, ShadowMapAtlas> = new Map(); // map a shadowmap atlas to their light type
#if arm_debug
public var lightType: String;
public var rejectedLights: Array<LightObject> = [];
#end
function new(light: LightObject) {
var maxTileSize = shadowMapAtlasSize(light);
@ -549,8 +593,15 @@ class ShadowMapAtlas {
this.maxAtlasSizeConst = getMaxAtlasSize(light.data.raw.type);
#if arm_shadowmap_atlas_lod
if (tileSizes.length == 0)
computeTileSizes(maxTileSize, depth);
computeTileSizes(maxTileSize, depth);
#end
#if arm_debug
#if arm_shadowmap_atlas_single_map
this.lightType = "any";
#else
this.lightType = light.data.raw.type;
#end
#end
}
@ -560,11 +611,7 @@ class ShadowMapAtlas {
* @param light of type LightObject to be added to an yatlas
* @return if the light was added succesfully
*/
public static function addLight(light: LightObject): Bool {
// check if light can be added based on culling
if (light.culledLight || light.shadowMapScale == 0.0)
return false;
public static function addLight(light: LightObject) {
var atlasName = shadowMapAtlasName(light.data.raw.type);
var atlas = shadowMapAtlases.get(atlasName);
if (atlas == null) {
@ -575,11 +622,21 @@ class ShadowMapAtlas {
// find a free tile for this light
var mainTile = ShadowMapTile.assignTiles(light, atlas, null);
if (mainTile == null)
return false;
// push main tile to active tiles
if (mainTile == null) {
#if arm_debug
if (!atlas.rejectedLights.contains(light))
atlas.rejectedLights.push(light);
#end
return;
}
atlas.activeTiles.push(mainTile);
return true;
// notify the tile on light remove
light.tileNotifyOnRemove = mainTile.notifyOnLightRemove;
// notify atlas when this tile is freed
mainTile.notifyOnFree = atlas.freeActiveTile;
// "lock" light to make sure it's not eligible to be added again
light.lightInAtlas = true;
}
static inline function shadowMapAtlasSize(light:LightObject):Int {
@ -602,17 +659,17 @@ class ShadowMapAtlas {
}
#if arm_shadowmap_atlas_lod
static function computeTileSizes(maxTileSize: Int, depth: Int): Void {
function computeTileSizes(maxTileSize: Int, depth: Int): Void {
// find the highest value based on the calculation done in the cluster code
var base = LightObject.zToShadowMapScale(0, 16);
var subdiv = base / depth;
for(i in 0...depth){
tileSizes.push(Std.int(maxTileSize / Math.pow(2, i)));
tileSizeFactor.push(base);
this.tileSizes.push(Std.int(maxTileSize / Math.pow(2, i)));
this.tileSizeFactor.push(base);
base -= subdiv;
}
tileSizes.push(0);
tileSizeFactor.push(0.0);
this.tileSizes.push(0);
this.tileSizeFactor.push(0.0);
}
#end
@ -658,7 +715,9 @@ class ShadowMapAtlas {
public static inline function getMaxAtlasSize(type: String): Int {
#if arm_shadowmap_atlas_single_map
#if (rp_shadowmap_atlas_max_size == 1024)
#if (rp_shadowmap_atlas_max_size == 512)
return 512;
#elseif (rp_shadowmap_atlas_max_size == 1024)
return 1024;
#elseif (rp_shadowmap_atlas_max_size == 2048)
return 2048;
@ -685,7 +744,9 @@ class ShadowMapAtlas {
#end
}
case "spot": {
#if (rp_shadowmap_atlas_max_size_spot == 1024)
#if (rp_shadowmap_atlas_max_size_spot == 512)
return 512;
#elseif (rp_shadowmap_atlas_max_size_spot == 1024)
return 1024;
#elseif (rp_shadowmap_atlas_max_size_spot == 2048)
return 2048;
@ -698,7 +759,9 @@ class ShadowMapAtlas {
#end
}
case "sun": {
#if (rp_shadowmap_atlas_max_size_sun == 1024)
#if (rp_shadowmap_atlas_max_size_sun == 512)
return 512;
#elseif (rp_shadowmap_atlas_max_size_sun == 1024)
return 1024;
#elseif (rp_shadowmap_atlas_max_size_sun == 2048)
return 2048;
@ -711,7 +774,9 @@ class ShadowMapAtlas {
#end
}
default: {
#if (rp_shadowmap_atlas_max_size == 1024)
#if (rp_shadowmap_atlas_max_size == 512)
return 512;
#elseif (rp_shadowmap_atlas_max_size == 1024)
return 1024;
#elseif (rp_shadowmap_atlas_max_size == 2048)
return 2048;
@ -727,6 +792,10 @@ class ShadowMapAtlas {
}
#end
}
function freeActiveTile(tile: ShadowMapTile) {
activeTiles.remove(tile);
}
}
class ShadowMapTile {
@ -792,7 +861,6 @@ class ShadowMapTile {
static inline function findCreateTiles(light: LightObject, oldTile: ShadowMapTile, atlas: ShadowMapAtlas, tilesPerLightType: Int, tileSize: Int): Array<ShadowMapTile> {
var tilesFound: Array<ShadowMapTile> = [];
var updateAtlas = false;
while (tilesFound.length < tilesPerLightType) {
findTiles(light, oldTile, atlas.tiles, tileSize, tilesPerLightType, tilesFound);
@ -946,6 +1014,11 @@ class ShadowMapTile {
}
}
public function notifyOnLightRemove() {
unlockLight = true;
freeTile();
}
inline function lockTile(light: LightObject): Void {
if (this.light != null)
return;
@ -959,6 +1032,7 @@ class ShadowMapTile {
}
public var unlockLight: Bool = false;
public var notifyOnFree: ShadowMapTile -> Void;
public function freeTile(): Void {
// prevent duplicates
@ -984,6 +1058,11 @@ class ShadowMapTile {
tempTile.linkedTile = null;
tempTile = linkedTile;
}
// notify atlas that this tile has been freed
if (notifyOnFree != null) {
notifyOnFree(this);
notifyOnFree = null;
}
}
public inline function forEachTileLinked(action: ShadowMapTile->Void): Void {

View file

@ -0,0 +1,226 @@
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 coefficient. **/
public static var rayleighCoeff = new Vec3(5.5e-6, 13.0e-6, 22.4e-6);
/** Rayleigh scattering scale parameter. **/
public static var rayleighScale = 8e3;
/** Mie scattering coefficient. **/
public static var mieCoeff = 2e-5;
/** Mie scattering scale parameter. **/
public static var mieScale = 1.2e3;
/** Ozone scattering coefficient. **/
// 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.
public static var ozoneCoeff = new Vec3(1.59051840791988e-6, 0.00000096707041180970, 0.00000007309568762914);
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;
}
jODepth.mult(jStepSize);
// Precalculate a part of the secondary attenuation.
// For one variable (e.g. x) in the vector, the formula is as follows:
//
// attn.x = exp(-(coeffX * (firstOpticalDepth.x + secondOpticalDepth.x)))
//
// We can split that up via:
//
// attn.x = exp(-(coeffX * firstOpticalDepth.x + coeffX * secondOpticalDepth.x))
// = exp(-(coeffX * firstOpticalDepth.x)) * exp(-(coeffX * secondOpticalDepth.x))
//
// The first factor of the resulting multiplication is calculated in the
// shader, but we can already precalculate the second one. As a side
// effect this keeps the range of the LUT values small because we don't
// store the optical depth but the attenuation.
var jAttenuation = new Vec3();
var mie = mieCoeff * jODepth.y;
jAttenuation.addf(mie, mie, mie);
jAttenuation.add(rayleighCoeff.clone().mult(jODepth.x));
jAttenuation.add(ozoneCoeff.clone().mult(jODepth.z));
jAttenuation.exp(jAttenuation.mult(-1));
return jAttenuation;
}
}

View file

@ -4,347 +4,223 @@ import kha.FastFloat;
import iron.system.Input;
class InputMap {
var commands = new Map<String, Null<Array<InputCommand>>>();
static var inputMaps = new Map<String, InputMap>();
public var keys(default, null) = new Array<InputMapKey>();
public var lastKeyPressed(default, null) = "";
public function new() {}
public function addKeyboard(config: String) {
var command = new KeyboardCommand();
return addCustomCommand(command, config);
public static function getInputMap(inputMap: String): Null<InputMap> {
if (inputMaps.exists(inputMap)) {
return inputMaps[inputMap];
}
return null;
}
public function addGamepad(config: String) {
var command = new GamepadCommand();
return addCustomCommand(command, config);
public static function addInputMap(inputMap: String): InputMap {
return inputMaps[inputMap] = new InputMap();
}
public function addCustomCommand(command: InputCommand, config: String) {
if (commands[config] == null) commands[config] = new Array<InputCommand>();
commands[config].push(command);
return command;
}
}
class ActionMap extends InputMap {
public inline function started(config: String) {
var started = false;
for (c in commands[config]) {
if (c.started()) {
started = true;
break;
public static function getInputMapKey(inputMap: String, key: String): Null<InputMapKey> {
if (inputMaps.exists(inputMap)) {
for (k in inputMaps[inputMap].keys) {
if (k.key == key) {
return k;
}
}
}
return started;
return null;
}
public inline function released(config: String) {
var released = false;
public static function removeInputMapKey(inputMap: String, key: String): Bool {
if (inputMaps.exists(inputMap)) {
var i = inputMaps[inputMap];
for (c in commands[config]) {
if (c.released()) {
released = true;
break;
for (k in i.keys) {
if (k.key == key) {
return i.removeKey(k);
}
}
}
return released;
return false;
}
}
class AxisMap extends InputMap {
var scale: FastFloat = 1.0;
public function addKeyboard(key: String, scale: FastFloat = 1.0): InputMapKey {
return addKey(new KeyboardKey(key, scale));
}
public inline function getAxis(config: String) {
var axis = 0.0;
public function addMouse(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey {
return addKey(new MouseKey(key, scale, deadzone));
}
for (c in commands[config]) {
var tempAxis = c.getAxis();
public function addGamepad(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey {
return addKey(new GamepadKey(key, scale, deadzone));
}
if (tempAxis != 0.0 && tempAxis != axis) {
axis += tempAxis;
scale = c.getScale();
}
public function addKey(key: InputMapKey): InputMapKey {
keys.push(key);
return key;
}
public function removeKey(key: InputMapKey): Bool {
return keys.remove(key);
}
public function value(): FastFloat {
var v = 0.0;
for (k in keys) {
v += k.value();
}
return axis;
}
public inline function getScale() {
return scale;
}
}
class InputCommand {
var keys = new Array<String>();
var modifiers = new Array<String>();
var displacementKeys = new Array<String>();
var displacementModifiers = new Array<String>();
var deadzone: FastFloat = 0.0;
var scale: FastFloat = 1.0;
public function new() {}
public function setKeys(keys: Array<String>) {
return this.keys = keys;
}
public function setMods(modifiers: Array<String>) {
return this.modifiers = modifiers;
}
public function setDisplacementKeys(keys: Array<String>) {
return displacementKeys = keys;
}
public function setDisplacementMods(modifiers: Array<String>) {
return displacementModifiers = modifiers;
}
public function setDeadzone(deadzone: FastFloat) {
return this.deadzone = deadzone;
}
public function setScale(scale: FastFloat) {
return this.scale = scale;
}
public function getScale() {
return scale;
return v;
}
public function started() {
for (k in keys) {
if (k.started()) {
lastKeyPressed = k.key;
return true;
}
}
return false;
}
public function released() {
for (k in keys) {
if (k.released()) {
lastKeyPressed = k.key;
return true;
}
}
return false;
}
}
class InputMapKey {
public var key: String;
public var scale: FastFloat;
public var deadzone: FastFloat;
public function new(key: String, scale = 1.0, deadzone = 0.0) {
this.key = key.toLowerCase();
this.scale = scale;
this.deadzone = deadzone;
}
public function started(): Bool {
return false;
}
public function getAxis(): FastFloat {
public function released(): Bool {
return false;
}
public function value(): FastFloat {
return 0.0;
}
}
class KeyboardCommand extends InputCommand {
var keyboard = Input.getKeyboard();
var mouse = Input.getMouse();
public function setIndex(index: Int) {}
public inline override function started() {
for (k in keys) {
if (keyboard.started(k)) {
for (m in modifiers) {
if (!keyboard.down(m)) return false;
}
function evalDeadzone(value: FastFloat): FastFloat {
var v = 0.0;
for (m in displacementModifiers) {
if (!mouse.down(m)) return false;
}
if (value > deadzone) {
v = value - deadzone;
return true;
}
} else if (value < -deadzone) {
v = value + deadzone;
}
for (k in displacementKeys) {
if (mouse.started(k)) {
for (m in modifiers) {
if (!keyboard.down(m)) return false;
}
for (m in displacementModifiers) {
if (!mouse.down(m)) return false;
}
return true;
}
}
return false;
return v * scale;
}
public inline override function released() {
for (k in keys) {
if (keyboard.released(k)) {
for (m in modifiers) {
if (!keyboard.down(m)) return false;
}
function evalPressure(value: FastFloat): FastFloat {
var v = value - deadzone;
for (m in displacementModifiers) {
if (!mouse.down(m)) return false;
}
if (v > 0.0) {
v /= (1.0 - deadzone);
return true;
}
} else {
v = 0.0;
}
for (k in displacementKeys) {
if (mouse.released(k)) {
for (m in modifiers) {
if (!keyboard.down(m)) return false;
}
for (m in displacementModifiers) {
if (!mouse.down(m)) return false;
}
return true;
}
}
return false;
}
public inline override function getAxis() {
var axis = 0.0;
var movementX = mouse.movementX;
var movementY = mouse.movementY;
var wheelDelta = mouse.wheelDelta;
for (k in keys) {
if (keyboard.down(k)) {
axis++;
break;
}
}
for (m in modifiers) {
if (keyboard.down(m)) {
axis --;
break;
}
}
for (k in displacementKeys) {
switch (k) {
case "moved x": if (movementX > deadzone) axis++;
case "moved y": if (movementY > deadzone) axis--;
case "wheel": if (wheelDelta < -deadzone) axis++;
case "movement x": if (movementX > deadzone) return movementX - deadzone;
case "movement y": if (movementY > deadzone) return movementY - deadzone;
default: {
if (mouse.down(k)) {
axis ++;
break;
}
}
}
}
for (m in displacementModifiers) {
switch (m) {
case "moved x": if (movementX < -deadzone) axis--;
case "moved y": if (movementY < -deadzone) axis++;
case "wheel": if (wheelDelta > deadzone) axis--;
case "movement x": if (movementX < -deadzone) return movementX + deadzone;
case "movement y": if (movementY < -deadzone) return movementY + deadzone;
default: {
if (mouse.down(m)) {
axis --;
break;
}
}
}
}
return axis > 1 ? 1 : axis < -1 ? -1 : axis;
return v;
}
}
class GamepadCommand extends InputCommand {
var gamepad = Input.getGamepad(0);
class KeyboardKey extends InputMapKey {
var kb = Input.getKeyboard();
public inline override function started() {
for (k in keys) {
if (gamepad.started(k)) {
for (m in modifiers) {
if (gamepad.down(m) < deadzone) return false;
}
return true;
}
}
return false;
return kb.started(key);
}
public inline override function released() {
for (k in keys) {
if (gamepad.released(k)) {
for (m in modifiers) {
if (gamepad.down(m) < deadzone) return false;
}
return true;
}
}
return false;
return kb.released(key);
}
public inline override function getAxis() {
var axis = 0.0;
var rsMovementX = gamepad.rightStick.movementX;
var rsMovementY = gamepad.rightStick.movementY;
var lsMovementX = gamepad.leftStick.movementX;
var lsMovementY = gamepad.leftStick.movementY;
var rtPressure = gamepad.down("r2") > 0.0 ? (gamepad.down("r2") - deadzone) / (1 - deadzone) : 0.0;
var ltPressure = gamepad.down("l2") > 0.0 ? (gamepad.down("r2") - deadzone) / (1 - deadzone) : 0.0;
for (k in keys) {
switch(k) {
case "rtPressure": axis += rtPressure;
case "ltPressure": axis += ltPressure;
default: {
if (gamepad.down(k) > deadzone) {
axis++;
break;
}
}
}
}
for (m in modifiers) {
switch (m) {
case "rtPressure": axis -= rtPressure;
case "ltPressure": axis -= ltPressure;
default: {
if (gamepad.down(m) > deadzone) {
axis--;
break;
}
}
}
}
for (k in displacementKeys) {
switch(k) {
case "rs moved x": if (rsMovementX > deadzone) axis++;
case "rs moved y": if (rsMovementY > deadzone) axis++;
case "ls moved x": if (lsMovementX > deadzone) axis++;
case "ls moved y": if (lsMovementY > deadzone) axis++;
case "rs movement x": if (rsMovementX > deadzone) return rsMovementX - deadzone;
case "rs movement y": if (rsMovementY > deadzone) return rsMovementY - deadzone;
case "ls movement x": if (lsMovementX > deadzone) return lsMovementX - deadzone;
case "ls movement y": if (lsMovementY > deadzone) return lsMovementY - deadzone;
}
}
for (m in displacementModifiers) {
switch (m) {
case "rs moved x": if (rsMovementX < -deadzone) axis--;
case "rs moved y": if (rsMovementY < -deadzone) axis--;
case "ls moved x": if (lsMovementX < -deadzone) axis--;
case "ls moved y": if (lsMovementY < -deadzone) axis--;
case "rs movement x": if (rsMovementX < -deadzone) return rsMovementX + deadzone;
case "rs movement y": if (rsMovementY < -deadzone) return rsMovementY + deadzone;
case "ls movement x": if (lsMovementX < -deadzone) return lsMovementX + deadzone;
case "ls movement y": if (lsMovementY < -deadzone) return lsMovementY + deadzone;
}
}
return axis > 1 ? 1 : axis < -1 ? -1 : axis;
public inline override function value(): FastFloat {
return kb.down(key) ? scale : 0.0;
}
}
}
class MouseKey extends InputMapKey {
var m = Input.getMouse();
public inline override function started() {
return m.started(key);
}
public inline override function released() {
return m.released(key);
}
public override function value(): FastFloat {
return switch (key) {
case "movement x": evalDeadzone(m.movementX);
case "movement y": evalDeadzone(m.movementY);
case "wheel": evalDeadzone(m.wheelDelta);
default: m.down(key) ? scale : 0.0;
}
}
}
class GamepadKey extends InputMapKey {
var g = Input.getGamepad();
public inline override function started() {
return g.started(key);
}
public inline override function released() {
return g.released(key);
}
public override function value(): FastFloat {
return switch(key) {
case "ls movement x": evalDeadzone(g.leftStick.movementX);
case "ls movement y": evalDeadzone(g.leftStick.movementY);
case "rs movement x": evalDeadzone(g.rightStick.movementX);
case "rs movement y": evalDeadzone(g.rightStick.movementY);
case "lt pressure": evalDeadzone(evalPressure(g.down("l2")));
case "rt pressure": evalDeadzone(evalPressure(g.down("r2")));
default: evalDeadzone(g.down(key));
}
}
public override function setIndex(index: Int) {
g = Input.getGamepad(index);
}
}

View file

@ -94,6 +94,10 @@ class Logic {
var v = createClassInstance(node.type, [tree]);
nodeMap.set(name, v);
#if arm_patch
tree.nodes.set(name, v);
#end
// Properties
for (i in 0...5) {
for (b in node.buttons) {
@ -103,9 +107,13 @@ class Logic {
}
}
@:privateAccess v.preallocInputs(node.inputs.length);
@:privateAccess v.preallocOutputs(node.outputs.length);
// Create inputs
var inp_node: armory.logicnode.LogicNode = null;
var inp_from = 0;
var from_type: String;
for (i in 0...node.inputs.length) {
var inp = node.inputs[i];
// Is linked - find node
@ -117,6 +125,7 @@ class Logic {
for (i in 0...n.outputs.length) {
if (n.outputs[i] == socket) {
inp_from = i;
from_type = socket.type;
break;
}
}
@ -124,27 +133,33 @@ class Logic {
else { // Not linked - create node with default values
inp_node = build_default_node(inp);
inp_from = 0;
from_type = inp.type;
}
// Add input
v.addInput(inp_node, inp_from);
var link = LogicNode.addLink(inp_node, v, inp_from, i);
#if arm_patch
link.fromType = from_type;
link.toType = inp.type;
link.toValue = getSocketDefaultValue(inp);
#end
}
// Create outputs
for (out in node.outputs) {
var outNodes: Array<armory.logicnode.LogicNode> = [];
for (i in 0...node.outputs.length) {
var out = node.outputs[i];
var ls = getOutputLinks(out);
if (ls != null && ls.length > 0) {
for (l in ls) {
var n = getNode(l.to_id);
var out_name = build_node(n);
outNodes.push(nodeMap.get(out_name));
}
// Linked outputs are already handled after iterating over inputs
// above, so only unconnected outputs are handled here
if (ls == null || ls.length == 0) {
var link = LogicNode.addLink(v, build_default_node(out), i, 0);
#if arm_patch
link.fromType = out.type;
link.toType = out.type;
link.toValue = getSocketDefaultValue(out);
#end
}
else { // Not linked - create node with default values
outNodes.push(build_default_node(out));
}
// Add outputs
v.addOutputs(outNodes);
}
return name;
@ -212,6 +227,22 @@ class Logic {
return v;
}
static function getSocketDefaultValue(socket: TNodeSocket): Any {
var v: armory.logicnode.LogicNode = null;
return switch (socket.type) {
case "OBJECT" | "VALUE" | "INT" | "BOOLEAN" | "STRING":
socket.default_value;
case "VECTOR" | "RGB":
socket.default_value == null ? [0, 0, 0] : [socket.default_value[0], socket.default_value[1], socket.default_value[2]];
case "RGBA":
socket.default_value == null ? [0, 0, 0, 1] : [socket.default_value[0], socket.default_value[1], socket.default_value[2], socket.default_value[3]];
default:
null;
}
}
static function createClassInstance(className: String, args: Array<Dynamic>): Dynamic {
var cname = Type.resolveClass(packageName + "." + className);
if (cname == null) return null;

View file

@ -6,6 +6,9 @@ import iron.math.Vec4;
class ArcBall extends Trait {
@prop
public var axis = new Vec4(0, 0, 1);
public function new() {
super();
@ -17,10 +20,8 @@ class ArcBall extends Trait {
var mouse = Input.getMouse();
if (mouse.down()) {
object.transform.rotate(new Vec4(0, 0, 1), -mouse.movementX / 100);
object.transform.buildMatrix();
object.transform.rotate(axis, -mouse.movementX / 100);
object.transform.rotate(object.transform.world.right(), -mouse.movementY / 100);
object.transform.buildMatrix();
}
}
}

View file

@ -33,7 +33,7 @@ class FollowCamera extends iron.Trait {
trace("FollowCamera error, unable to set target object");
}
if (Std.is(object, iron.object.CameraObject)) {
if (Std.isOfType(object, iron.object.CameraObject)) {
disabled = true;
trace("FollowCamera error, this trait should not be placed directly on a camera objet. It should be placed on another object such as an Empty. The camera should be placed as a child to the Empty object with offset, creating a camera boom.");
}

View file

@ -2,6 +2,7 @@ package armory.trait;
import iron.Trait;
import iron.system.Input;
import iron.math.Vec3;
import iron.math.Vec4;
import iron.math.Mat4;
import iron.math.RayCaster;
@ -14,6 +15,11 @@ class PhysicsDrag extends Trait {
public function new() { super(); }
#else
@prop public var linearLowerLimit = new Vec3(0,0,0);
@prop public var linearUpperLimit = new Vec3(0,0,0);
@prop public var angularLowerLimit = new Vec3(-10,-10,-10);
@prop public var angularUpperLimit = new Vec3(10,10,10);
var pickConstraint: bullet.Bt.Generic6DofConstraint = null;
var pickDist: Float;
var pickedBody: RigidBody = null;
@ -56,10 +62,10 @@ class PhysicsDrag extends Trait {
tr.setOrigin(localPivot);
pickConstraint = new bullet.Bt.Generic6DofConstraint(b.body, tr, false);
pickConstraint.setLinearLowerLimit(new bullet.Bt.Vector3(0, 0, 0));
pickConstraint.setLinearUpperLimit(new bullet.Bt.Vector3(0, 0, 0));
pickConstraint.setAngularLowerLimit(new bullet.Bt.Vector3(-10, -10, -10));
pickConstraint.setAngularUpperLimit(new bullet.Bt.Vector3(10, 10, 10));
pickConstraint.setLinearLowerLimit(new bullet.Bt.Vector3(linearLowerLimit.x, linearLowerLimit.y, linearLowerLimit.z));
pickConstraint.setLinearUpperLimit(new bullet.Bt.Vector3(linearUpperLimit.x, linearUpperLimit.y, linearUpperLimit.z));
pickConstraint.setAngularLowerLimit(new bullet.Bt.Vector3(angularLowerLimit.x, angularLowerLimit.y, angularLowerLimit.z));
pickConstraint.setAngularUpperLimit(new bullet.Bt.Vector3(angularUpperLimit.x, angularUpperLimit.y, angularUpperLimit.z));
physics.world.addConstraint(pickConstraint, false);
/*pickConstraint.setParam(4, 0.8, 0);

View file

@ -11,6 +11,7 @@ class Bridge {
public static var Input = iron.system.Input;
public static var Object = iron.object.Object;
public static var Data = iron.data.Data;
public static var Vec4 = iron.math.Vec4;
public static function log(s: String) { trace(s); };
}

View file

@ -3,7 +3,7 @@ package armory.trait.internal;
import iron.Trait;
#if arm_ui
import zui.Zui;
import zui.Canvas;
import armory.ui.Canvas;
#end
class CanvasScript extends Trait {
@ -29,7 +29,7 @@ class CanvasScript extends Trait {
iron.data.Data.getBlob(canvasName + ".json", function(blob: kha.Blob) {
iron.data.Data.getBlob("_themes.json", function(tBlob: kha.Blob) {
if (tBlob.get_length() != 0) {
if (@:privateAccess tBlob.get_length() != 0) {
Canvas.themes = haxe.Json.parse(tBlob.toString());
}
else {
@ -37,23 +37,30 @@ class CanvasScript extends Trait {
}
if (Canvas.themes.length == 0) {
Canvas.themes.push(zui.Themes.light);
Canvas.themes.push(armory.ui.Themes.light);
}
iron.data.Data.getFont(font, function(f: kha.Font) {
iron.data.Data.getFont(font, function(defaultFont: kha.Font) {
var c: TCanvas = haxe.Json.parse(blob.toString());
if (c.theme == null) c.theme = Canvas.themes[0].NAME;
cui = new Zui({font: f, theme: Canvas.getTheme(c.theme)});
cui = new Zui({font: defaultFont, theme: Canvas.getTheme(c.theme)});
if (c.assets == null || c.assets.length == 0) canvas = c;
else { // Load canvas assets
var loaded = 0;
for (asset in c.assets) {
var file = asset.name;
iron.data.Data.getImage(file, function(image: kha.Image) {
Canvas.assetMap.set(asset.id, image);
if (++loaded >= c.assets.length) canvas = c;
});
if (Canvas.isFontAsset(file)) {
iron.data.Data.getFont(file, function(f: kha.Font) {
Canvas.assetMap.set(asset.id, f);
if (++loaded >= c.assets.length) canvas = c;
});
} else {
iron.data.Data.getImage(file, function(image: kha.Image) {
Canvas.assetMap.set(asset.id, image);
if (++loaded >= c.assets.length) canvas = c;
});
}
}
}
});
@ -62,7 +69,7 @@ class CanvasScript extends Trait {
notifyOnRender2D(function(g: kha.graphics2.Graphics) {
if (canvas == null) return;
setCanvasDimensions(kha.System.windowWidth(), kha.System.windowHeight());
var events = Canvas.draw(cui, canvas, g);
@ -121,7 +128,7 @@ class CanvasScript extends Trait {
public function setCanvasVisibility(visible: Bool){
for (e in canvas.elements) e.visible = visible;
}
/**
* Set dimensions of canvas
* @param x Width
@ -140,7 +147,7 @@ class CanvasScript extends Trait {
}
// Contains data
@:access(zui.Canvas)
@:access(armory.ui.Canvas)
@:access(zui.Handle)
public function getHandle(name: String): Handle {
// Consider this a temporary solution

View file

@ -8,6 +8,10 @@ import iron.object.MeshObject;
import zui.Zui;
import zui.Id;
using armory.object.TransformExtension;
#if arm_shadowmap_atlas
import armory.renderpath.Inc.ShadowMapTile;
import armory.renderpath.Inc.ShadowMapAtlas;
#end
#end
#if arm_debug
@ -61,6 +65,15 @@ class DebugConsole extends Trait {
var shortcut_scale_in = kha.input.KeyCode.OpenBracket;
var shortcut_scale_out = kha.input.KeyCode.CloseBracket;
#if arm_shadowmap_atlas
var lightColorMap: Map<String, Int> = new Map();
var lightColorMapCount = 0;
var smaLogicTime = 0.0;
var smaLogicTimeAvg = 0.0;
var smaRenderTime = 0.0;
var smaRenderTimeAvg = 0.0;
#end
public function new(scaleFactor = 1.0, scaleDebugConsole = 1.0, positionDebugConsole = 2, visibleDebugConsole = 1,
keyCodeVisible = kha.input.KeyCode.Tilde, keyCodeScaleIn = kha.input.KeyCode.OpenBracket, keyCodeScaleOut = kha.input.KeyCode.CloseBracket) {
super();
@ -423,6 +436,7 @@ class DebugConsole extends Trait {
light.data.raw.strength = ui.slider(lightHandle, "Strength", 0.0, 5.0, true) * 10;
#if arm_shadowmap_atlas
ui.text("status: " + (light.culledLight ? "culled" : "rendered"));
ui.text("shadow map size: " + light.data.raw.shadowmap_size);
#end
}
else if (Std.is(selectedObject, iron.object.CameraObject)) {
@ -473,6 +487,16 @@ class DebugConsole extends Trait {
ui.text("Physics");
ui.text(Math.round(physTimeAvg * 10000) / 10 + " ms", Align.Right);
#if arm_shadowmap_atlas
ui.row(lrow);
ui.text("Shadow Map Atlas (Logic)");
ui.text(Math.round(smaLogicTimeAvg * 10000) / 10 + " ms", Align.Right);
ui.row(lrow);
ui.text("Shadow Map Atlas (Render)");
ui.text(Math.round(smaRenderTimeAvg * 10000) / 10 + " ms", Align.Right);
#end
ui.unindent();
}
@ -520,6 +544,182 @@ class DebugConsole extends Trait {
ui.unindent();
}
#if arm_shadowmap_atlas
if (ui.panel(Id.handle({selected: false}), "Shadow Map Atlases")) {
inline function highLightNext(color: kha.Color = null) {
ui.g.color = color != null ? color : -13882324;
ui.g.fillRect(ui._x, ui._y, ui._windowW, ui.ELEMENT_H());
ui.g.color = 0xffffffff;
}
inline function drawScale(text: String, y: Float, fromX: Float, toX: Float, bottom = false) {
var _off = bottom ? -4 : 4;
ui.g.drawLine(fromX, y, toX, y);
ui.g.drawLine(fromX, y, fromX, y + _off);
ui.g.drawLine(toX, y, toX, y + _off);
var _w = ui._w;
ui._w = Std.int(Math.abs(toX - fromX));
ui.text(text, Align.Center);
ui._w = _w;
}
/**
* create a kha Color from HSV (Hue, Saturation, Value)
* @param h expected Hue from [0, 1].
* @param s expected Saturation from [0, 1].
* @param v expected Value from [0, 1].
* @return kha.Color
*/
function colorFromHSV(h: Float, s: Float, v: Float): kha.Color {
// https://stackoverflow.com/a/17243070
var r = 0.0; var g = 0.0; var b = 0.0;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: { r = v; g = t; b = p; }
case 1: { r = q; g = v; b = p; }
case 2: { r = p; g = v; b = t; }
case 3: { r = p; g = q; b = v; }
case 4: { r = t; g = p; b = v; }
case 5: { r = v; g = p; b = q; }
}
return kha.Color.fromFloats(r, g, b);
}
function drawTiles(tile: ShadowMapTile, atlas: ShadowMapAtlas, atlasVisualSize: Float) {
var color: Null<kha.Color> = kha.Color.fromFloats(0.1, 0.1, 0.1);
var borderColor = color;
var tileScale = (tile.size / atlas.sizew) * atlasVisualSize; //* 0.95;
var x = (tile.coordsX / atlas.sizew) * atlasVisualSize;
var y = (tile.coordsY / atlas.sizew) * atlasVisualSize;
if (tile.light != null) {
color = lightColorMap.get(tile.light.name);
if (color == null) {
color = colorFromHSV(Math.random(), 0.7, Math.random() * 0.5 + 0.5);
lightColorMap.set(tile.light.name, color);
lightColorMapCount++;
}
ui.fill(x + tileScale * 0.019, y + tileScale * 0.03, tileScale * 0.96, tileScale * 0.96, color);
}
ui.rect(x, y, tileScale, tileScale, borderColor);
#if arm_shadowmap_atlas_lod
// draw children tiles
for (t in tile.tiles)
drawTiles(t, atlas, atlasVisualSize);
#end
}
ui.indent(false);
ui.text("Constants:");
highLightNext();
ui.text('Tiles Used Per Point Light: ${ ShadowMapTile.tilesLightType("point") }');
ui.text('Tiles Used Per Spot Light: ${ ShadowMapTile.tilesLightType("spot") }');
highLightNext();
ui.text('Tiles Used For Sun: ${ ShadowMapTile.tilesLightType("sun") }');
ui.unindent(false);
ui.indent(false);
var i = 0;
for (atlas in ShadowMapAtlas.shadowMapAtlases) {
if (ui.panel(Id.handle({selected: false}).nest(i), atlas.target )) {
ui.indent(false);
// Draw visual representation of the atlas
var atlasVisualSize = ui._windowW * 0.92;
drawScale('${atlas.sizew}px', ui._y + ui.ELEMENT_H() * 0.9, ui._x, ui._x + atlasVisualSize);
// reset light color map when lights are removed
if (lightColorMapCount > iron.Scene.active.lights.length) {
lightColorMap = new Map();
lightColorMapCount = 0;
}
for (tile in atlas.tiles)
drawTiles(tile, atlas, atlasVisualSize);
// set vertical space for atlas visual representation
ui._y += atlasVisualSize + 3;
var tilesRow = atlas.currTileOffset == 0 ? 1 : atlas.currTileOffset;
var tileScale = atlasVisualSize / tilesRow;
drawScale('${atlas.baseTileSizeConst}px', ui._y, ui._x, ui._x + tileScale, true);
// general atlas information
highLightNext();
ui.text('Max Atlas Size: ${atlas.maxAtlasSizeConst}, ${atlas.maxAtlasSizeConst} px');
highLightNext();
// show detailed information per light
if (ui.panel(Id.handle({selected: false}).nest(i).nest(0), "Lights in Atlas")) {
ui.indent(false);
var j = 1;
for (tile in atlas.activeTiles) {
var textCol = ui.t.TEXT_COL;
var lightCol = lightColorMap.get(tile.light.name);
if (lightCol != null)
ui.t.TEXT_COL = lightCol;
#if arm_shadowmap_atlas_lod
if (ui.panel(Id.handle({selected: false}).nest(i).nest(j), tile.light.name)) {
ui.t.TEXT_COL = textCol;
ui.indent(false);
ui.text('Shadow Map Size: ${tile.size}, ${tile.size} px');
ui.unindent(false);
}
#else
ui.indent(false);
ui.text(tile.light.name);
ui.unindent(false);
#end
ui.t.TEXT_COL = textCol;
j++;
}
ui.unindent(false);
}
// show unused tiles statistics
#if arm_shadowmap_atlas_lod
// WIP
#else
var unusedTiles = atlas.tiles.length;
#if arm_shadowmap_atlas_single_map
for (tile in atlas.activeTiles)
unusedTiles -= ShadowMapTile.tilesLightType(tile.light.data.raw.type);
#else
unusedTiles -= atlas.activeTiles.length * ShadowMapTile.tilesLightType(atlas.lightType);
#end
ui.text('Unused tiles: ${unusedTiles}');
#end
var rejectedLightsNames = "";
if (atlas.rejectedLights.length > 0) {
for (l in atlas.rejectedLights)
rejectedLightsNames += l.name + ", ";
rejectedLightsNames = rejectedLightsNames.substr(0, rejectedLightsNames.length - 2);
highLightNext(kha.Color.fromFloats(0.447, 0.247, 0.188));
ui.text('Not enough space in atlas for ${atlas.rejectedLights.length} light${atlas.rejectedLights.length > 1 ? "s" : ""}:');
ui.indent();
ui.text(${rejectedLightsNames});
ui.unindent(false);
}
ui.unindent(false);
}
i++;
}
ui.unindent(false);
ui.unindent(false);
}
#end
if (ui.panel(Id.handle({selected: false}), "Render Targets")) {
ui.indent();
#if (kha_opengl || kha_webgl)
@ -682,6 +882,14 @@ class DebugConsole extends Trait {
animTimeAvg = animTime / frames;
physTimeAvg = physTime / frames;
#if arm_shadowmap_atlas
smaLogicTimeAvg = smaLogicTime / frames;
smaLogicTime = 0;
smaRenderTimeAvg = smaRenderTime / frames;
smaRenderTime = 0;
#end
totalTime = 0;
renderPathTime = 0;
updateTime = 0;
@ -706,6 +914,10 @@ class DebugConsole extends Trait {
#if arm_physics
physTime += armory.trait.physics.PhysicsWorld.physTime;
#end
#if arm_shadowmap_atlas
smaLogicTime += armory.renderpath.Inc.shadowsLogicTime;
smaRenderTime += armory.renderpath.Inc.shadowsRenderTime;
#end
}
static function roundfp(f: Float, precision = 2): Float {

View file

@ -1,8 +1,17 @@
package armory.trait.internal;
import armory.logicnode.LogicNode;
import armory.logicnode.LogicTree;
#if arm_patch @:expose("LivePatch") #end
@:access(armory.logicnode.LogicNode)
@:access(armory.logicnode.LogicNodeLink)
class LivePatch extends iron.Trait {
#if arm_patch
#if !arm_patch
public function new() { super(); }
#else
static var patchId = 0;
@ -23,9 +32,164 @@ class LivePatch extends iron.Trait {
});
}
#else
public static function patchCreateNodeLink(treeName: String, fromNodeName: String, toNodeName: String, fromIndex: Int, toIndex: Int) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
public function new() { super(); }
for (tree in trees) {
var fromNode = tree.nodes[fromNodeName];
var toNode = tree.nodes[toNodeName];
if (fromNode == null || toNode == null) return;
LogicNode.addLink(fromNode, toNode, fromIndex, toIndex);
}
}
public static function patchSetNodeLinks(treeName: String, nodeName: String, inputDatas: Array<Dynamic>, outputDatas: Array<Array<Dynamic>>) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
var node = tree.nodes[nodeName];
if (node == null) return;
node.clearInputs();
node.clearOutputs();
for (inputData in inputDatas) {
var fromNode: LogicNode;
var fromIndex: Int;
if (inputData.isLinked) {
fromNode = tree.nodes[inputData.fromNode];
if (fromNode == null) continue;
fromIndex = inputData.fromIndex;
}
else {
fromNode = LogicNode.createSocketDefaultNode(node.tree, inputData.socketType, inputData.socketValue);
fromIndex = 0;
}
LogicNode.addLink(fromNode, node, fromIndex, inputData.toIndex);
}
for (outputData in outputDatas) {
for (linkData in outputData) {
var toNode: LogicNode;
var toIndex: Int;
if (linkData.isLinked) {
toNode = tree.nodes[linkData.toNode];
if (toNode == null) continue;
toIndex = linkData.toIndex;
}
else {
toNode = LogicNode.createSocketDefaultNode(node.tree, linkData.socketType, linkData.socketValue);
toIndex = 0;
}
LogicNode.addLink(node, toNode, linkData.fromIndex, toIndex);
}
}
}
}
public static function patchUpdateNodeProp(treeName: String, nodeName: String, propName: String, value: Dynamic) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
var node = tree.nodes[nodeName];
if (node == null) return;
Reflect.setField(node, propName, value);
}
}
public static function patchUpdateNodeInputVal(treeName: String, nodeName: String, socketIndex: Int, value: Dynamic) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
var node = tree.nodes[nodeName];
if (node == null) return;
node.inputs[socketIndex].set(value);
}
}
public static function patchNodeDelete(treeName: String, nodeName: String) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
var node = tree.nodes[nodeName];
if (node == null) return;
node.clearOutputs();
node.clearInputs();
tree.nodes.remove(nodeName);
}
}
public static function patchNodeCreate(treeName: String, nodeName: String, nodeType: String, propDatas: Array<Array<Dynamic>>, inputDatas: Array<Array<Dynamic>>, outputDatas: Array<Array<Dynamic>>) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
// No further constructor parameters required here, all variable nodes
// use optional further parameters and all values are set later in this
// function.
var newNode: LogicNode = Type.createInstance(Type.resolveClass(nodeType), [tree]);
newNode.name = nodeName;
tree.nodes[nodeName] = newNode;
for (propData in propDatas) {
Reflect.setField(newNode, propData[0], propData[1]);
}
var i = 0;
for (inputData in inputDatas) {
LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++);
}
i = 0;
for (outputData in outputDatas) {
LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0);
}
}
}
public static function patchNodeCopy(treeName: String, nodeName: String, newNodeName: String, copyProps: Array<String>, inputDatas: Array<Array<Dynamic>>, outputDatas: Array<Array<Dynamic>>) {
if (!LogicTree.nodeTrees.exists(treeName)) return;
var trees = LogicTree.nodeTrees[treeName];
for (tree in trees) {
var node = tree.nodes[nodeName];
if (node == null) return;
// No further constructor parameters required here, all variable nodes
// use optional further parameters and all values are set later in this
// function.
var newNode: LogicNode = Type.createInstance(Type.getClass(node), [tree]);
newNode.name = newNodeName;
tree.nodes[newNodeName] = newNode;
for (propName in copyProps) {
Reflect.setField(newNode, propName, Reflect.field(node, propName));
}
var i = 0;
for (inputData in inputDatas) {
LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++);
}
i = 0;
for (outputData in outputDatas) {
LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0);
}
}
}
#end
}

View file

@ -2,14 +2,29 @@ package armory.trait.internal;
import kha.Image;
import kha.Video;
import iron.Trait;
import iron.object.MeshObject;
/**
Replaces the diffuse texture of the first material of the trait's object
with a video texture.
@see https://github.com/armory3d/armory_examples/tree/master/material_movie
**/
class MovieTexture extends Trait {
/**
Caches all render targets used by this trait for re-use when having
multiple videos of the same size. The lookup only takes place on trait
initialization.
Map layout: `[width => [height => image]]`
**/
static var imageCache: Map<Int, Map<Int, Image>> = new Map();
var video: Video;
public static var image: Image;
public static var created = false;
var image: Image;
var videoName: String;
@ -33,10 +48,7 @@ class MovieTexture extends Trait {
this.videoName = videoName;
if (!created) {
created = true;
notifyOnInit(init);
}
notifyOnInit(init);
}
function init() {
@ -44,9 +56,21 @@ class MovieTexture extends Trait {
video = vid;
video.play(true);
image = Image.createRenderTarget(getPower2(video.width()), getPower2(video.height()));
var w = getPower2(video.width());
var h = getPower2(video.height());
var o = cast(object, iron.object.MeshObject);
// Lazily fill the outer map
var hMap: Map<Int, Image> = imageCache[w];
if (hMap == null) {
imageCache[w] = new Map<Int, Image>();
}
image = imageCache[w][h];
if (image == null) {
imageCache[w][h] = image = Image.createRenderTarget(w, h);
}
var o = cast(object, MeshObject);
o.materials[0].contexts[0].textures[0] = image; // Override diffuse texture
notifyOnRender2D(render);
});

View file

@ -0,0 +1,320 @@
package armory.trait.internal;
import iron.object.MeshObject;
import iron.Trait;
import kha.Image;
import iron.math.Vec4;
import iron.data.MaterialData;
import iron.Scene;
import iron.object.Object;
import iron.object.Uniforms;
class UniformsManager extends Trait{
static var floatsRegistered = false;
static var floatsMap = new Map<Object, Map<MaterialData, Map<String, Null<kha.FastFloat>>>>();
static var vectorsRegistered = false;
static var vectorsMap = new Map<Object, Map<MaterialData, Map<String, Vec4>>>();
static var texturesRegistered = false;
static var texturesMap = new Map<Object, Map<MaterialData, Map<String, kha.Image>>>();
static var sceneRemoveInitalized = false;
public var uniformExists = false;
public function new(){
super();
notifyOnInit(init);
notifyOnRemove(removeObject);
if(! sceneRemoveInitalized){
Scene.active.notifyOnRemove(removeScene);
}
}
function init() {
var materials = cast(object, MeshObject).materials;
for (material in materials){
var exists = registerShaderUniforms(material);
if(exists) {
uniformExists = true;
}
}
}
static function removeScene() {
removeObjectFromAllMaps(Scene.active.root);
}
function removeObject() {
removeObjectFromAllMaps(object);
}
// Helper method to register float, vec3 and texture getter functions
static function register(type: UniformType){
switch (type){
case Float:{
if(! floatsRegistered){
floatsRegistered = true;
Uniforms.externalFloatLinks.push(floatLink);
}
}
case Vector:{
if(! vectorsRegistered){
vectorsRegistered = true;
Uniforms.externalVec3Links.push(vec3Link);
}
}
case Texture:{
if(! texturesRegistered){
texturesRegistered = true;
Uniforms.externalTextureLinks.push(textureLink);
}
}
}
}
// Register and map shader uniforms if it is an armory shader parameter
public static function registerShaderUniforms(material: MaterialData) : Bool {
var uniformExist = false;
if(! floatsMap.exists(Scene.active.root)) floatsMap.set(Scene.active.root, null);
if(! vectorsMap.exists(Scene.active.root)) vectorsMap.set(Scene.active.root, null);
if(! texturesMap.exists(Scene.active.root)) texturesMap.set(Scene.active.root, null);
for(context in material.shader.raw.contexts){ // For each context in shader
for (constant in context.constants){ // For each constant in the context
if(constant.is_arm_parameter){ // Check if armory parameter
uniformExist = true;
var object = Scene.active.root; // Map default uniforms to scene root
switch (constant.type){
case "float":{
var link = constant.link;
var value = constant.float;
setFloatValue(material, object, link, value);
register(Float);
}
case "vec3":{
var vec = new Vec4();
vec.x = constant.vec3.get(0);
vec.y = constant.vec3.get(1);
vec.z = constant.vec3.get(2);
setVec3Value(material, object, constant.link, vec);
register(Vector);
}
}
}
}
for (texture in context.texture_units){
if(texture.is_arm_parameter){ // Check if armory parameter
uniformExist = true;
var object = Scene.active.root; // Map default texture to scene root
if(texture.default_image_file == null){
setTextureValue(material, object, texture.link, null);
}
else{
iron.data.Data.getImage(texture.default_image_file, function(image: kha.Image) {
setTextureValue(material, object, texture.link, image);
});
}
register(Texture);
}
}
}
return uniformExist;
}
// Method to set map Object -> Material -> Link -> FLoat
public static function setFloatValue(material: MaterialData, object: Object, link: String, value: Null<kha.FastFloat>){
if(object == null || material == null || link == null) return;
var map = floatsMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Method to set map Object -> Material -> Link -> Vec3
public static function setVec3Value(material: MaterialData, object: Object, link: String, value: Vec4){
if(object == null || material == null || link == null) return;
var map = vectorsMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Method to set map Object -> Material -> Link -> Texture
public static function setTextureValue(material: MaterialData, object: Object, link: String, value: kha.Image){
if(object == null || material == null || link == null) return;
var map = texturesMap;
var matMap = map.get(object);
if (matMap == null) {
matMap = new Map();
map.set(object, matMap);
}
var entry = matMap.get(material);
if (entry == null) {
entry = new Map();
matMap.set(material, entry);
}
entry.set(link, value); // parameter name, value
}
// Mehtod to get object specific material parameter float value
static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
if(object == null || mat == null) return null;
if(! floatsMap.exists(object)){
object = Scene.active.root;
}
var material = floatsMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Mehtod to get object specific material parameter vec3 value
static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
if(object == null || mat == null) return null;
if(! vectorsMap.exists(object)){
object = Scene.active.root;
}
var material = vectorsMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Mehtod to get object specific material parameter texture value
static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
if(object == null || mat == null) return null;
if(! texturesMap.exists(object)){
object = Scene.active.root;
}
var material = texturesMap.get(object);
if (material == null) return null;
var entry = material.get(mat);
if (entry == null) return null;
return entry.get(link);
}
// Returns complete map of float value material paramets
public static function getFloatsMap():Map<Object, Map<MaterialData, Map<String, Null<kha.FastFloat>>>>{
return floatsMap;
}
// Returns complete map of vec3 value material paramets
public static function getVectorsMap():Map<Object, Map<MaterialData, Map<String, Vec4>>>{
return vectorsMap;
}
// Returns complete map of texture value material paramets
public static function getTexturesMap():Map<Object, Map<MaterialData, Map<String, kha.Image>>>{
return texturesMap;
}
// Remove all object specific material paramenter keys
public static function removeObjectFromAllMaps(object: Object) {
floatsMap.remove(object);
vectorsMap.remove(object);
texturesMap.remove(object);
}
// Remove object specific material paramenter keys
public static function removeObjectFromMap(object: Object, type: UniformType) {
switch (type){
case Float: floatsMap.remove(object);
case Vector: vectorsMap.remove(object);
case Texture: texturesMap.remove(object);
}
}
}
@:enum abstract UniformType(Int) from Int to Int {
var Float = 0;
var Vector = 1;
var Texture = 2;
}

View file

@ -18,8 +18,9 @@ class PhysicsConstraintExportHelper extends iron.Trait {
var breakingThreshold: Float;
var limits: Array<Float>;
var constraintAdded: Bool = false;
var relativeConstraint: Bool = false;
public function new(body1: String, body2: String, type: Int, disableCollisions: Bool, breakingThreshold: Float, limits: Array<Float> = null) {
public function new(body1: String, body2: String, type: Int, disableCollisions: Bool, breakingThreshold: Float, relatieConstraint: Bool = false, limits: Array<Float> = null) {
super();
this.body1 = body1;
@ -27,14 +28,26 @@ class PhysicsConstraintExportHelper extends iron.Trait {
this.type = type;
this.disableCollisions = disableCollisions;
this.breakingThreshold = breakingThreshold;
this.relativeConstraint = relatieConstraint;
this.limits = limits;
notifyOnInit(init);
notifyOnUpdate(update);
}
function init() {
var target1 = Scene.active.getChild(body1);
var target2 = Scene.active.getChild(body2);
var target1;
var target2;
if(relativeConstraint) {
target1 = object.parent.getChild(body1);
target2 = object.parent.getChild(body2);
}
else {
target1 = Scene.active.getChild(body1);
target2 = Scene.active.getChild(body2);
}
object.addTrait(new PhysicsConstraint(target1, target2, type, disableCollisions, breakingThreshold, limits));
constraintAdded = true;
}

View file

@ -52,7 +52,6 @@ class PhysicsWorld extends Trait {
public var rbMap: Map<Int, RigidBody>;
public var conMap: Map<Int, PhysicsConstraint>;
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();

View file

@ -149,7 +149,7 @@ class RigidBody extends iron.Trait {
if (ready) return;
ready = true;
if (!Std.is(object, MeshObject)) return; // No mesh data
if (!Std.isOfType(object, MeshObject)) return; // No mesh data
transform = object.transform;
physics = armory.trait.physics.PhysicsWorld.active;

415
Sources/armory/ui/Canvas.hx Normal file
View file

@ -0,0 +1,415 @@
package armory.ui;
import zui.Zui;
using kha.graphics2.GraphicsExtension;
@:access(zui.Zui)
class Canvas {
public static var assetMap = new Map<Int, Dynamic>(); // kha.Image | kha.Font
public static var themes = new Array<zui.Themes.TTheme>();
static var events:Array<String> = [];
public static var screenW = -1;
public static var screenH = -1;
public static var locale = "en";
static var _ui: Zui;
static var h = new zui.Zui.Handle(); // TODO: needs one handle per canvas
public static function draw(ui: Zui, canvas: TCanvas, g: kha.graphics2.Graphics): Array<String> {
screenW = kha.System.windowWidth();
screenH = kha.System.windowHeight();
events.resize(0);
_ui = ui;
g.end();
ui.begin(g); // Bake elements
g.begin(false);
ui.g = g;
for (elem in canvas.elements) {
if (elem.parent == null) drawElement(ui, canvas, elem);
}
g.end();
ui.end(); // Finish drawing
g.begin(false);
return events;
}
static function drawElement(ui: Zui, canvas: TCanvas, element: TElement, px = 0.0, py = 0.0) {
if (element == null || element.visible == false) return;
var anchorOffset = getAnchorOffset(canvas, element);
px += anchorOffset[0];
py += anchorOffset[1];
ui._x = canvas.x + scaled(element.x) + px;
ui._y = canvas.y + scaled(element.y) + py;
ui._w = scaled(element.width);
var rotated = element.rotation != null && element.rotation != 0;
if (rotated) ui.g.pushRotation(element.rotation, ui._x + scaled(element.width) / 2, ui._y + scaled(element.height) / 2);
var font = ui.ops.font;
var fontAsset = isFontAsset(element.asset);
if (fontAsset) ui.ops.font = getAsset(canvas, element.asset);
switch (element.type) {
case Text:
var size = ui.fontSize;
ui.fontSize = scaled(element.height);
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.text(getText(canvas, element), element.alignment);
ui.fontSize = size;
case Button:
var eh = ui.t.ELEMENT_H;
var bh = ui.t.BUTTON_H;
ui.t.ELEMENT_H = element.height;
ui.t.BUTTON_H = element.height;
ui.t.BUTTON_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.BUTTON_TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).BUTTON_TEXT_COL);
ui.t.BUTTON_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
ui.t.BUTTON_PRESSED_COL = getColor(element.color_press, getTheme(canvas.theme).BUTTON_PRESSED_COL);
if (ui.button(getText(canvas, element), element.alignment)) {
var e = element.event;
if (e != null && e != "") events.push(e);
}
ui.t.ELEMENT_H = eh;
ui.t.BUTTON_H = bh;
case Image:
var image = getAsset(canvas, element.asset);
if (image != null && !fontAsset) {
ui.imageScrollAlign = false;
var tint = element.color != null ? element.color : 0xffffffff;
if (ui.image(image, tint, scaled(element.height)) == zui.Zui.State.Released) {
var e = element.event;
if (e != null && e != "") events.push(e);
}
ui.imageScrollAlign = true;
}
case FRectangle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.fillRect(ui._x, ui._y, ui._w, scaled(element.height));
ui.g.color = col;
case FCircle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.fillCircle(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2);
ui.g.color = col;
case Rectangle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.drawRect(ui._x, ui._y, ui._w, scaled(element.height), element.strength);
ui.g.color = col;
case Circle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.drawCircle(ui._x+(scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2, element.strength);
ui.g.color = col;
case FTriangle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.fillTriangle(ui._x + (ui._w / 2), ui._y, ui._x, ui._y + scaled(element.height), ui._x + ui._w, ui._y + scaled(element.height));
ui.g.color = col;
case Triangle:
var col = ui.g.color;
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.drawLine(ui._x + (ui._w / 2), ui._y, ui._x, ui._y + scaled(element.height), element.strength);
ui.g.drawLine(ui._x, ui._y + scaled(element.height), ui._x + ui._w, ui._y + scaled(element.height), element.strength);
ui.g.drawLine(ui._x + ui._w, ui._y + scaled(element.height), ui._x + (ui._w / 2), ui._y, element.strength);
ui.g.color = col;
case Check:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
ui.check(h.nest(element.id), getText(canvas, element));
case Radio:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
zui.Ext.inlineRadio(ui, h.nest(element.id), getText(canvas, element).split(";"));
case Combo:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.SEPARATOR_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
ui.combo(h.nest(element.id), getText(canvas, element).split(";"));
case Slider:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
ui.slider(h.nest(element.id), getText(canvas, element), 0.0, 1.0, true, 100, true, element.alignment);
case TextInput:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
ui.textInput(h.nest(element.id), getText(canvas, element), element.alignment);
if (h.nest(element.id).changed) {
var e = element.event;
if (e != null && e != "") events.push(e);
}
case TextArea:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
h.nest(element.id).text = getText(canvas, element);
zui.Ext.textArea(ui,h.nest(element.id), element.alignment,element.editable);
if (h.nest(element.id).changed) {
var e = element.event;
if (e != null && e != "") events.push(e);
}
case KeyInput:
ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL);
ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL);
Ext.keyInput(ui, h.nest(element.id), getText(canvas, element));
case ProgressBar:
var col = ui.g.color;
var progress = element.progress_at;
var totalprogress = element.progress_total;
ui.g.color = getColor(element.color_progress, getTheme(canvas.theme).TEXT_COL);
ui.g.fillRect(ui._x, ui._y, ui._w / totalprogress * Math.min(progress, totalprogress), scaled(element.height));
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.drawRect(ui._x, ui._y, ui._w, scaled(element.height), element.strength);
ui.g.color = col;
case CProgressBar:
var col = ui.g.color;
var progress = element.progress_at;
var totalprogress = element.progress_total;
ui.g.color = getColor(element.color_progress, getTheme(canvas.theme).TEXT_COL);
ui.g.drawArc(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2, -Math.PI / 2, ((Math.PI * 2) / totalprogress * progress) - Math.PI / 2, element.strength);
ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL);
ui.g.fillCircle(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), (ui._w / 2) - 10);
ui.g.color = col;
case Empty:
}
ui.ops.font = font;
if (element.children != null) {
for (id in element.children) {
drawElement(ui, canvas, elemById(canvas, id), scaled(element.x) + px, scaled(element.y) + py);
}
}
if (rotated) ui.g.popTransformation();
}
static inline function getText(canvas: TCanvas, e: TElement): String {
return e.text;
}
public static function getAsset(canvas: TCanvas, asset: String): Dynamic { // kha.Image | kha.Font {
for (a in canvas.assets) if (a.name == asset) return assetMap.get(a.id);
return null;
}
static var elemId = -1;
public static function getElementId(canvas: TCanvas): Int {
if (elemId == -1) for (e in canvas.elements) if (elemId < e.id) elemId = e.id;
return ++elemId;
}
static var assetId = -1;
public static function getAssetId(canvas: TCanvas): Int {
if (assetId == -1) for (a in canvas.assets) if (assetId < a.id) assetId = a.id;
return ++assetId;
}
static function elemById(canvas: TCanvas, id: Int): TElement {
for (e in canvas.elements) if (e.id == id) return e;
return null;
}
static inline function scaled(f: Float): Int {
return Std.int(f * _ui.SCALE());
}
public static inline function isFontAsset(assetName: Null<String>): Bool {
return assetName != null && StringTools.endsWith(assetName.toLowerCase(), ".ttf");
}
public static inline function getColor(color: Null<Int>, defaultColor: Int): Int {
return color != null ? color : defaultColor;
}
public static function getTheme(theme: String): zui.Themes.TTheme {
for (t in Canvas.themes) {
if (t.NAME == theme) return t;
}
return null;
}
/**
Returns the positional scaled offset of the given element based on its anchor setting.
@param canvas The canvas object
@param element The element
@return Array<Float> [xOffset, yOffset]
**/
public static function getAnchorOffset(canvas: TCanvas, element: TElement): Array<Float> {
var boxWidth, boxHeight: Float;
var offsetX = 0.0;
var offsetY = 0.0;
if (element.parent == null) {
boxWidth = canvas.width;
boxHeight = canvas.height;
}
else {
var parent = elemById(canvas, element.parent);
boxWidth = scaled(parent.width);
boxHeight = scaled(parent.height);
}
switch (element.anchor) {
case Top:
offsetX += boxWidth / 2 - scaled(element.width) / 2;
case TopRight:
offsetX += boxWidth - scaled(element.width);
case CenterLeft:
offsetY += boxHeight / 2 - scaled(element.height) / 2;
case Center:
offsetX += boxWidth / 2 - scaled(element.width) / 2;
offsetY += boxHeight / 2 - scaled(element.height) / 2;
case CenterRight:
offsetX += boxWidth - scaled(element.width);
offsetY += boxHeight / 2 - scaled(element.height) / 2;
case BottomLeft:
offsetY += boxHeight - scaled(element.height);
case Bottom:
offsetX += boxWidth / 2 - scaled(element.width) / 2;
offsetY += boxHeight - scaled(element.height);
case BottomRight:
offsetX += boxWidth - scaled(element.width);
offsetY += boxHeight - scaled(element.height);
}
return [offsetX, offsetY];
}
}
typedef TCanvas = {
var name: String;
var x: Float;
var y: Float;
var width: Int;
var height: Int;
var elements: Array<TElement>;
var theme: String;
@:optional var assets: Array<TAsset>;
@:optional var locales: Array<TLocale>;
}
typedef TElement = {
var id: Int;
var type: ElementType;
var name: String;
var x: Float;
var y: Float;
var width: Int;
var height: Int;
@:optional var rotation: Null<kha.FastFloat>;
@:optional var text: String;
@:optional var event: String;
// null = follow theme settings
@:optional var color: Null<Int>;
@:optional var color_text: Null<Int>;
@:optional var color_hover: Null<Int>;
@:optional var color_press: Null<Int>;
@:optional var color_progress: Null<Int>;
@:optional var progress_at: Null<Int>;
@:optional var progress_total: Null<Int>;
@:optional var strength: Null<Int>;
@:optional var alignment: Null<Int>;
@:optional var anchor: Null<Int>;
@:optional var parent: Null<Int>; // id
@:optional var children: Array<Int>; // ids
@:optional var asset: String;
@:optional var visible: Null<Bool>;
@:optional var editable: Null<Bool>;
}
typedef TAsset = {
var id: Int;
var name: String;
var file: String;
}
typedef TLocale = {
var name: String; // "en"
var texts: Array<TTranslatedText>;
}
typedef TTranslatedText = {
var id: Int; // element id
var text: String;
}
@:enum abstract ElementType(Int) from Int to Int {
var Text = 0;
var Image = 1;
var Button = 2;
var Empty = 3;
// var HLayout = 4;
// var VLayout = 5;
var Check = 6;
var Radio = 7;
var Combo = 8;
var Slider = 9;
var TextInput = 10;
var KeyInput = 11;
var FRectangle = 12;
var Rectangle = 13;
var FCircle = 14;
var Circle = 15;
var FTriangle = 16;
var Triangle = 17;
var ProgressBar = 18;
var CProgressBar = 19;
var TextArea = 20;
}
@:enum abstract Anchor(Int) from Int to Int {
var TopLeft = 0;
var Top = 1;
var TopRight = 2;
var CenterLeft = 3;
var Center = 4;
var CenterRight = 5;
var BottomLeft = 6;
var Bottom = 7;
var BottomRight = 8;
}

331
Sources/armory/ui/Ext.hx Normal file
View file

@ -0,0 +1,331 @@
package armory.ui;
import zui.Zui;
import kha.input.Keyboard;
import kha.input.KeyCode;
typedef ListOpts = {
?addCb: String->Void,
?removeCb: Int->Void,
?getNameCb: Int->String,
?setNameCb: Int->String->Void,
?getLabelCb: Int->String,
?itemDrawCb: Handle->Int->Void,
?showRadio: Bool, // false
?editable: Bool, // true
?showAdd: Bool, // true
?addLabel: String // 'Add'
}
@:access(zui.Zui)
class Ext {
public static function keyInput(ui: Zui, handle: Handle, label = "", align: Align = Left): Int {
if (!ui.isVisible(ui.ELEMENT_H())) {
ui.endElement();
return Std.int(handle.value);
}
var hover = ui.getHover();
if (hover && Zui.onTextHover != null) Zui.onTextHover();
ui.g.color = hover ? ui.t.ACCENT_HOVER_COL : ui.t.ACCENT_COL; // Text bg
ui.drawRect(ui.g, ui.t.FILL_ACCENT_BG, ui._x + ui.buttonOffsetY, ui._y + ui.buttonOffsetY, ui._w - ui.buttonOffsetY * 2, ui.BUTTON_H());
var startEdit = ui.getReleased() || ui.tabPressed;
if (ui.textSelectedHandle != handle && startEdit) ui.startTextEdit(handle);
if (ui.textSelectedHandle == handle) Ext.listenToKey(ui, handle);
else handle.changed = false;
if (label != "") {
ui.g.color = ui.t.LABEL_COL; // Label
var labelAlign = align == Align.Right ? Align.Left : Align.Right;
var xOffset = labelAlign == Align.Left ? 7 : 0;
ui.drawString(ui.g, label, xOffset, 0, labelAlign);
}
handle.text = Ext.keycodeToString(Std.int(handle.value));
ui.g.color = ui.t.TEXT_COL; // Text
ui.textSelectedHandle != handle ? ui.drawString(ui.g, handle.text, null, 0, align) : ui.drawString(ui.g, ui.textSelected, null, 0, align);
ui.endElement();
return Std.int(handle.value);
}
static function listenToKey(ui: Zui, handle: Handle) {
if (ui.isKeyDown) {
handle.value = ui.key;
handle.changed = ui.changed = true;
ui.textSelectedHandle = null;
ui.isTyping = false;
if (Keyboard.get() != null) Keyboard.get().hide();
}
else {
ui.textSelected = "Press a key...";
}
}
public static function list(ui: Zui, handle: Handle, ar: Array<Dynamic>, ?opts: ListOpts ): Int {
var selected = 0;
if (opts == null) opts = {};
var addCb = opts.addCb != null ? opts.addCb : function(name: String) ar.push(name);
var removeCb = opts.removeCb != null ? opts.removeCb : function(i: Int) ar.splice(i, 1);
var getNameCb = opts.getNameCb != null ? opts.getNameCb : function(i: Int) return ar[i];
var setNameCb = opts.setNameCb != null ? opts.setNameCb : function(i: Int, name: String) ar[i] = name;
var getLabelCb = opts.getLabelCb != null ? opts.getLabelCb : function(i: Int) return "";
var itemDrawCb = opts.itemDrawCb;
var showRadio = opts.showRadio != null ? opts.showRadio : false;
var editable = opts.editable != null ? opts.editable : true;
var showAdd = opts.showAdd != null ? opts.showAdd : true;
var addLabel = opts.addLabel != null ? opts.addLabel : "Add";
var i = 0;
while (i < ar.length) {
if (showRadio) { // Prepend ratio button
ui.row([0.12, 0.68, 0.2]);
if (ui.radio(handle.nest(0), i, "")) {
selected = i;
}
}
else ui.row([0.8, 0.2]);
var itemHandle = handle.nest(i);
itemHandle.text = getNameCb(i);
editable ? setNameCb(i, ui.textInput(itemHandle, getLabelCb(i))) : ui.text(getNameCb(i));
if (ui.button("X")) removeCb(i);
else i++;
if (itemDrawCb != null) itemDrawCb(itemHandle.nest(i), i - 1);
}
if (showAdd && ui.button(addLabel)) addCb("untitled");
return selected;
}
public static function panelList(ui: Zui, handle: Handle, ar: Array<Dynamic>,
addCb: String->Void = null,
removeCb: Int->Void = null,
getNameCb: Int->String = null,
setNameCb: Int->String->Void = null,
itemDrawCb: Handle->Int->Void = null,
editable = true,
showAdd = true,
addLabel: String = "Add") {
if (addCb == null) addCb = function(name: String) { ar.push(name); };
if (removeCb == null) removeCb = function(i: Int) { ar.splice(i, 1); };
if (getNameCb == null) getNameCb = function(i: Int) { return ar[i]; };
if (setNameCb == null) setNameCb = function(i: Int, name: String) { ar[i] = name; };
var i = 0;
while (i < ar.length) {
ui.row([0.12, 0.68, 0.2]);
var expanded = ui.panel(handle.nest(i), "");
var itemHandle = handle.nest(i);
editable ? setNameCb(i, ui.textInput(itemHandle, getNameCb(i))) : ui.text(getNameCb(i));
if (ui.button("X")) removeCb(i);
else i++;
if (itemDrawCb != null && expanded) itemDrawCb(itemHandle.nest(i), i - 1);
}
if (showAdd && ui.button(addLabel)) {
addCb("untitled");
}
}
public static function colorField(ui: Zui, handle:Handle, alpha = false): Int {
ui.g.color = handle.color;
ui.drawRect(ui.g, true, ui._x + 2, ui._y + ui.buttonOffsetY, ui._w - 4, ui.BUTTON_H());
ui.g.color = ui.getHover() ? ui.t.ACCENT_HOVER_COL : ui.t.ACCENT_COL;
ui.drawRect(ui.g, false, ui._x + 2, ui._y + ui.buttonOffsetY, ui._w - 4, ui.BUTTON_H(), 1.0);
if (ui.getStarted()) {
Popup.showCustom(
new Zui(ui.ops),
function(ui:Zui) {
zui.Ext.colorWheel(ui, handle, alpha);
},
Std.int(ui.inputX), Std.int(ui.inputY), 200, 500);
}
ui.endElement();
return handle.color;
}
public static function colorPicker(ui: Zui, handle: Handle, alpha = false): Int {
var r = ui.slider(handle.nest(0, {value: handle.color.R}), "R", 0, 1, true);
var g = ui.slider(handle.nest(1, {value: handle.color.G}), "G", 0, 1, true);
var b = ui.slider(handle.nest(2, {value: handle.color.B}), "B", 0, 1, true);
var a = handle.color.A;
if (alpha) a = ui.slider(handle.nest(3, {value: a}), "A", 0, 1, true);
var col = kha.Color.fromFloats(r, g, b, a);
ui.text("", Right, col);
return col;
}
/**
Keycodes can be found here: http://api.kha.tech/kha/input/KeyCode.html
**/
static function keycodeToString(keycode: Int): String {
return switch (keycode) {
default: String.fromCharCode(keycode);
case -1: "None";
case KeyCode.Unknown: "Unknown";
case KeyCode.Back: "Back";
case KeyCode.Cancel: "Cancel";
case KeyCode.Help: "Help";
case KeyCode.Backspace: "Backspace";
case KeyCode.Tab: "Tab";
case KeyCode.Clear: "Clear";
case KeyCode.Return: "Return";
case KeyCode.Shift: "Shift";
case KeyCode.Control: "Ctrl";
case KeyCode.Alt: "Alt";
case KeyCode.Pause: "Pause";
case KeyCode.CapsLock: "CapsLock";
case KeyCode.Kana: "Kana";
// case KeyCode.Hangul: "Hangul"; // Hangul == Kana
case KeyCode.Eisu: "Eisu";
case KeyCode.Junja: "Junja";
case KeyCode.Final: "Final";
case KeyCode.Hanja: "Hanja";
// case KeyCode.Kanji: "Kanji"; // Kanji == Hanja
case KeyCode.Escape: "Esc";
case KeyCode.Convert: "Convert";
case KeyCode.NonConvert: "NonConvert";
case KeyCode.Accept: "Accept";
case KeyCode.ModeChange: "ModeChange";
case KeyCode.Space: "Space";
case KeyCode.PageUp: "PageUp";
case KeyCode.PageDown: "PageDown";
case KeyCode.End: "End";
case KeyCode.Home: "Home";
case KeyCode.Left: "Left";
case KeyCode.Up: "Up";
case KeyCode.Right: "Right";
case KeyCode.Down: "Down";
case KeyCode.Select: "Select";
case KeyCode.Print: "Print";
case KeyCode.Execute: "Execute";
case KeyCode.PrintScreen: "PrintScreen";
case KeyCode.Insert: "Insert";
case KeyCode.Delete: "Delete";
case KeyCode.Colon: "Colon";
case KeyCode.Semicolon: "Semicolon";
case KeyCode.LessThan: "LessThan";
case KeyCode.Equals: "Equals";
case KeyCode.GreaterThan: "GreaterThan";
case KeyCode.QuestionMark: "QuestionMark";
case KeyCode.At: "At";
case KeyCode.Win: "Win";
case KeyCode.ContextMenu: "ContextMenu";
case KeyCode.Sleep: "Sleep";
case KeyCode.Numpad0: "Numpad0";
case KeyCode.Numpad1: "Numpad1";
case KeyCode.Numpad2: "Numpad2";
case KeyCode.Numpad3: "Numpad3";
case KeyCode.Numpad4: "Numpad4";
case KeyCode.Numpad5: "Numpad5";
case KeyCode.Numpad6: "Numpad6";
case KeyCode.Numpad7: "Numpad7";
case KeyCode.Numpad8: "Numpad8";
case KeyCode.Numpad9: "Numpad9";
case KeyCode.Multiply: "Multiply";
case KeyCode.Add: "Add";
case KeyCode.Separator: "Separator";
case KeyCode.Subtract: "Subtract";
case KeyCode.Decimal: "Decimal";
case KeyCode.Divide: "Divide";
case KeyCode.F1: "F1";
case KeyCode.F2: "F2";
case KeyCode.F3: "F3";
case KeyCode.F4: "F4";
case KeyCode.F5: "F5";
case KeyCode.F6: "F6";
case KeyCode.F7: "F7";
case KeyCode.F8: "F8";
case KeyCode.F9: "F9";
case KeyCode.F10: "F10";
case KeyCode.F11: "F11";
case KeyCode.F12: "F12";
case KeyCode.F13: "F13";
case KeyCode.F14: "F14";
case KeyCode.F15: "F15";
case KeyCode.F16: "F16";
case KeyCode.F17: "F17";
case KeyCode.F18: "F18";
case KeyCode.F19: "F19";
case KeyCode.F20: "F20";
case KeyCode.F21: "F21";
case KeyCode.F22: "F22";
case KeyCode.F23: "F23";
case KeyCode.F24: "F24";
case KeyCode.NumLock: "NumLock";
case KeyCode.ScrollLock: "ScrollLock";
case KeyCode.WinOemFjJisho: "WinOemFjJisho";
case KeyCode.WinOemFjMasshou: "WinOemFjMasshou";
case KeyCode.WinOemFjTouroku: "WinOemFjTouroku";
case KeyCode.WinOemFjLoya: "WinOemFjLoya";
case KeyCode.WinOemFjRoya: "WinOemFjRoya";
case KeyCode.Circumflex: "Circumflex";
case KeyCode.Exclamation: "Exclamation";
case KeyCode.DoubleQuote: "DoubleQuote";
case KeyCode.Hash: "Hash";
case KeyCode.Dollar: "Dollar";
case KeyCode.Percent: "Percent";
case KeyCode.Ampersand: "Ampersand";
case KeyCode.Underscore: "Underscore";
case KeyCode.OpenParen: "OpenParen";
case KeyCode.CloseParen: "CloseParen";
case KeyCode.Asterisk: "Asterisk";
case KeyCode.Plus: "Plus";
case KeyCode.Pipe: "Pipe";
case KeyCode.HyphenMinus: "HyphenMinus";
case KeyCode.OpenCurlyBracket: "OpenCurlyBracket";
case KeyCode.CloseCurlyBracket: "CloseCurlyBracket";
case KeyCode.Tilde: "Tilde";
case KeyCode.VolumeMute: "VolumeMute";
case KeyCode.VolumeDown: "VolumeDown";
case KeyCode.VolumeUp: "VolumeUp";
case KeyCode.Comma: "Comma";
case KeyCode.Period: "Period";
case KeyCode.Slash: "Slash";
case KeyCode.BackQuote: "BackQuote";
case KeyCode.OpenBracket: "OpenBracket";
case KeyCode.BackSlash: "BackSlash";
case KeyCode.CloseBracket: "CloseBracket";
case KeyCode.Quote: "Quote";
case KeyCode.Meta: "Meta";
case KeyCode.AltGr: "AltGr";
case KeyCode.WinIcoHelp: "WinIcoHelp";
case KeyCode.WinIco00: "WinIco00";
case KeyCode.WinIcoClear: "WinIcoClear";
case KeyCode.WinOemReset: "WinOemReset";
case KeyCode.WinOemJump: "WinOemJump";
case KeyCode.WinOemPA1: "WinOemPA1";
case KeyCode.WinOemPA2: "WinOemPA2";
case KeyCode.WinOemPA3: "WinOemPA3";
case KeyCode.WinOemWSCTRL: "WinOemWSCTRL";
case KeyCode.WinOemCUSEL: "WinOemCUSEL";
case KeyCode.WinOemATTN: "WinOemATTN";
case KeyCode.WinOemFinish: "WinOemFinish";
case KeyCode.WinOemCopy: "WinOemCopy";
case KeyCode.WinOemAuto: "WinOemAuto";
case KeyCode.WinOemENLW: "WinOemENLW";
case KeyCode.WinOemBackTab: "WinOemBackTab";
case KeyCode.ATTN: "ATTN";
case KeyCode.CRSEL: "CRSEL";
case KeyCode.EXSEL: "EXSEL";
case KeyCode.EREOF: "EREOF";
case KeyCode.Play: "Play";
case KeyCode.Zoom: "Zoom";
case KeyCode.PA1: "PA1";
case KeyCode.WinOemClear: "WinOemClear";
}
}
}

127
Sources/armory/ui/Popup.hx Normal file
View file

@ -0,0 +1,127 @@
package armory.ui;
import zui.Zui;
import kha.System;
@:access(zui.Zui)
class Popup {
public static var show = false;
static var ui: Zui = null;
static var hwnd = new Handle();
static var boxTitle = "";
static var boxText = "";
static var boxCommands: Zui->Void = null;
static var modalX = 0;
static var modalY = 0;
static var modalW = 400;
static var modalH = 160;
public static function render(g: kha.graphics2.Graphics) {
if (boxCommands == null) {
ui.begin(g);
if (ui.window(hwnd, modalX, modalY, modalW, modalH)) {
drawTitle(g);
for (line in boxText.split("\n")) {
ui.text(line);
}
ui._y = ui._h - ui.t.BUTTON_H - 10;
ui.row([1/3, 1/3, 1/3]);
ui.endElement();
if (ui.button("OK")) {
show = false;
}
}
ui.end();
}
else {
ui.begin(g);
if (ui.window(hwnd, modalX, modalY, modalW, modalH)) {
drawTitle(g);
ui._y += 10;
boxCommands(ui);
}
ui.end();
}
}
public static function drawTitle(g: kha.graphics2.Graphics) {
if (boxTitle != "") {
g.color = ui.t.SEPARATOR_COL;
ui.drawRect(g, true, ui._x, ui._y, ui._w, ui.t.BUTTON_H);
g.color = ui.t.TEXT_COL;
ui.text(boxTitle);
}
}
public static function update() {
var inUse = ui.comboSelectedHandle != null;
// Close popup
if (ui.inputStarted && !inUse) {
if (ui.inputX < modalX || ui.inputX > modalX + modalW || ui.inputY < modalY || ui.inputY > modalY + modalH) {
show = false;
}
}
}
/**
Displays a message box with a title, a text body and a centered "OK" button.
@param ui the Zui instance for the popup
@param title the title to display
@param text the text to display
**/
public static function showMessage(ui: Zui, title: String, text: String) {
Popup.ui = ui;
init();
boxTitle = title;
boxText = text;
boxCommands = null;
}
/**
Displays a popup box with custom drawing code.
@param ui the Zui instance for the popup
@param commands the function for drawing the popup's content
@param mx the x position of the popup. -1 = screen center (defaults to -1)
@param my the y position of the popup. -1 = screen center (defaults to -1)
@param mw the width of the popup (defaults to 400)
@param mh the height of the popup (defaults to 160)
**/
public static function showCustom(ui: Zui, commands: Zui->Void = null, mx = -1, my = -1, mw = 400, mh = 160) {
Popup.ui = ui;
init(mx, my, mw, mh);
boxTitle = "";
boxText = "";
boxCommands = commands;
}
static function init(mx = -1, my = -1, mw = 400, mh = 160) {
var appW = System.windowWidth();
var appH = System.windowHeight();
modalX = mx;
modalY = my;
modalW = Std.int(mw * ui.SCALE());
modalH = Std.int(mh * ui.SCALE());
// Center popup window if no value is given
if (mx == -1) modalX = Std.int(appW / 2 - modalW / 2);
if (my == -1) modalY = Std.int(appH / 2 - modalH / 2);
// Limit popup position to screen
modalX = Std.int(Math.max(0, Math.min(modalX, appW - modalW)));
modalY = Std.int(Math.max(0, Math.min(modalY, appH - modalH)));
hwnd.dragX = 0;
hwnd.dragY = 0;
hwnd.scrollOffset = 0.0;
show = true;
}
}

View file

@ -0,0 +1,42 @@
package armory.ui;
import zui.Themes;
class Themes {
// 2x scaled, for games
public static var light: TTheme = {
NAME: "Default Light",
WINDOW_BG_COL: 0xffefefef,
WINDOW_TINT_COL: 0xff222222,
ACCENT_COL: 0xffeeeeee,
ACCENT_HOVER_COL: 0xffbbbbbb,
ACCENT_SELECT_COL: 0xffaaaaaa,
BUTTON_COL: 0xffcccccc,
BUTTON_TEXT_COL: 0xff222222,
BUTTON_HOVER_COL: 0xffb3b3b3,
BUTTON_PRESSED_COL: 0xffb1b1b1,
TEXT_COL: 0xff999999,
LABEL_COL: 0xffaaaaaa,
SEPARATOR_COL: 0xff999999,
HIGHLIGHT_COL: 0xff205d9c,
CONTEXT_COL: 0xffaaaaaa,
PANEL_BG_COL: 0xffaaaaaa,
FONT_SIZE: 13 * 2,
ELEMENT_W: 100 * 2,
ELEMENT_H: 24 * 2,
ELEMENT_OFFSET: 4 * 2,
ARROW_SIZE: 5 * 2,
BUTTON_H: 22 * 2,
CHECK_SIZE: 15 * 2,
CHECK_SELECT_SIZE: 8 * 2,
SCROLL_W: 6 * 2,
TEXT_OFFSET: 8 * 2,
TAB_W: 12 * 2,
FILL_WINDOW_BG: false,
FILL_BUTTON_BG: true,
FILL_ACCENT_BG: false,
LINK_STYLE: Line,
FULL_TABS: false
};
}

View file

@ -0,0 +1,44 @@
import importlib
import sys
import types
# This gets cleared if this package/the __init__ module is reloaded
_module_cache: dict[str, types.ModuleType] = {}
def enable_reload(module_name: str):
"""Enable reloading for the next time the module with `module_name`
is executed.
"""
mod = sys.modules[module_name]
setattr(mod, module_name.replace('.', '_') + "_DO_RELOAD_MODULE", True)
def is_reload(module_name: str) -> bool:
"""True if the module given by `module_name` should reload the
modules it imports. This is the case if `enable_reload()` was called
for the module before.
"""
mod = sys.modules[module_name]
return hasattr(mod, module_name.replace('.', '_') + "_DO_RELOAD_MODULE")
def reload_module(module: types.ModuleType) -> types.ModuleType:
"""Wrapper around importlib.reload() to make sure no module is
reloaded twice.
Make sure to call this function in the same order in which the
modules are imported to make sure that the reloading respects the
module dependencies. Otherwise modules could depend on other modules
that are not yet reloaded.
If you import classes or functions from a module, make sure to
re-import them after the module is reloaded.
"""
mod = _module_cache.get(module.__name__, None)
if mod is None:
mod = importlib.reload(module)
_module_cache[module.__name__] = mod
return mod

View file

@ -2,9 +2,16 @@ from typing import Callable, Dict, Optional
from bpy.types import Material, UILayout
import arm
from arm.material.shader import ShaderContext
drivers: Dict[str, Dict] = dict()
if arm.is_reload(__name__):
arm.material.shader = arm.reload_module(arm.material.shader)
from arm.material.shader import ShaderContext
else:
drivers: Dict[str, Dict] = dict()
arm.enable_reload(__name__)
def add_driver(driver_name: str,

View file

@ -6,6 +6,12 @@ import bpy
import arm.log as log
import arm.utils
if arm.is_reload(__name__):
log = arm.reload_module(log)
arm.utils = arm.reload_module(arm.utils)
else:
arm.enable_reload(__name__)
assets = []
reserved_names = ['return.']
khafile_params = []

View file

@ -32,6 +32,19 @@ import arm.material.mat_batch as mat_batch
import arm.utils
import arm.profiler
if arm.is_reload(__name__):
assets = arm.reload_module(assets)
exporter_opt = arm.reload_module(exporter_opt)
log = arm.reload_module(log)
make_renderpath = arm.reload_module(make_renderpath)
cycles = arm.reload_module(cycles)
make_material = arm.reload_module(make_material)
mat_batch = arm.reload_module(mat_batch)
arm.utils = arm.reload_module(arm.utils)
arm.profiler = arm.reload_module(arm.profiler)
else:
arm.enable_reload(__name__)
@unique
class NodeType(Enum):
@ -278,6 +291,8 @@ class ArmoryExporter:
return out_track
def export_object_transform(self, bobject: bpy.types.Object, o):
wrd = bpy.data.worlds['Arm']
# Static transform
o['transform'] = {'values': ArmoryExporter.write_matrix(bobject.matrix_local)}
@ -291,7 +306,7 @@ class ArmoryExporter:
fp = self.get_meshes_file_path('action_' + action_name, compressed=ArmoryExporter.compress_enabled)
assets.add(fp)
ext = '.lz4' if ArmoryExporter.compress_enabled else ''
if ext == '' and not bpy.data.worlds['Arm'].arm_minimize:
if ext == '' and not wrd.arm_minimize:
ext = '.json'
if 'object_actions' not in o:
@ -307,6 +322,7 @@ class ArmoryExporter:
self.export_pose_markers(out_anim, action)
unresolved_data_paths = set()
for fcurve in action.fcurves:
data_path = fcurve.data_path
@ -314,7 +330,11 @@ class ArmoryExporter:
out_track = self.export_animation_track(fcurve, frame_range, FCURVE_TARGET_NAMES[data_path][fcurve.array_index])
except KeyError:
if data_path not in FCURVE_TARGET_NAMES:
log.warn(f"Action {action_name}: The data path '{data_path}' is not supported (yet)!")
# This can happen if the target is simply not
# supported or the action shares both bone
# and object transform data (FCURVE_TARGET_NAMES
# only contains object transform targets)
unresolved_data_paths.add(data_path)
continue
# Missing target entry for array_index or something else
else:
@ -322,8 +342,20 @@ class ArmoryExporter:
out_anim['tracks'].append(out_track)
if len(unresolved_data_paths) > 0:
warning = (
f'The action "{action_name}" has fcurve channels with data paths that could not be resolved.'
' This can be caused by the following things:\n'
' - The data paths are not supported.\n'
' - The action exists on both armature and non-armature objects or has both bone and object transform data.'
)
if wrd.arm_verbose_output:
warning += f'\n Unresolved data paths: {unresolved_data_paths}'
else:
warning += '\n To see the list of unresolved data paths please recompile with Armory Project > Verbose Output enabled.'
log.warn(warning)
if True: # not action.arm_cached or not os.path.exists(fp):
wrd = bpy.data.worlds['Arm']
if wrd.arm_verbose_output:
print('Exporting object action ' + action_name)
@ -1157,7 +1189,16 @@ class ArmoryExporter:
else:
invscale_tex = 1 * 32767
if has_tang:
exportMesh.calc_tangents(uvmap=lay0.name)
try:
exportMesh.calc_tangents(uvmap=lay0.name)
except Exception as e:
if hasattr(e, 'message'):
log.error(e.message)
else:
# Assume it was caused because of encountering n-gons
log.error(f"""object {bobject.name} contains n-gons in its mesh, so it's impossible to compute tanget space for normal mapping.
Make sure the mesh only has tris/quads.""")
tangdata = np.empty(num_verts * 3, dtype='<f4')
if has_col:
cdata = np.empty(num_verts * 3, dtype='<f4')
@ -1502,24 +1543,18 @@ class ArmoryExporter:
# Less bias for bigger maps
out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024)
elif objtype == 'POINT':
out_light['strength'] *= 2.6
if bpy.app.version >= (2, 80, 72):
out_light['strength'] *= 0.01
out_light['strength'] *= 0.01
out_light['fov'] = 1.5708 # pi/2
out_light['shadowmap_cube'] = True
if light_ref.shadow_soft_size > 0.1:
out_light['light_size'] = light_ref.shadow_soft_size * 10
elif objtype == 'SPOT':
out_light['strength'] *= 2.6
if bpy.app.version >= (2, 80, 72):
out_light['strength'] *= 0.01
out_light['strength'] *= 0.01
out_light['spot_size'] = math.cos(light_ref.spot_size / 2)
# Cycles defaults to 0.15
out_light['spot_blend'] = light_ref.spot_blend / 10
elif objtype == 'AREA':
out_light['strength'] *= 80.0 / (light_ref.size * light_ref.size_y)
if bpy.app.version >= (2, 80, 72):
out_light['strength'] *= 0.01
out_light['strength'] *= 0.01
out_light['size'] = light_ref.size
out_light['size_y'] = light_ref.size_y
@ -1565,9 +1600,11 @@ class ArmoryExporter:
asset_name = arm.utils.asset_name(bobject)
# Collection is in the same file
if collection.library is None:
# collection is in the same file, but (likely) on another scene
if asset_name not in scene_objects:
# Only export linked objects (from other scenes for example),
# all other objects (in scene_objects) are already exported.
if bobject.name not in scene_objects:
self.process_bobject(bobject)
self.export_object(bobject, self.scene)
else:
@ -2308,6 +2345,10 @@ class ArmoryExporter:
return instanced_type, instanced_data
@staticmethod
def rigid_body_static(rb):
return (not rb.enabled and not rb.kinematic) or (rb.type == 'PASSIVE' and not rb.kinematic)
def post_export_object(self, bobject: bpy.types.Object, o, type):
# Export traits
self.export_traits(bobject, o)
@ -2334,7 +2375,7 @@ class ArmoryExporter:
elif rb.collision_shape == 'CAPSULE':
shape = 6
body_mass = rb.mass
is_static = (not rb.enabled and not rb.kinematic) or (rb.type == 'PASSIVE' and not rb.kinematic)
is_static = self.rigid_body_static(rb)
if is_static:
body_mass = 0
x = {}
@ -2405,7 +2446,7 @@ class ArmoryExporter:
# Rigid body constraint
rbc = bobject.rigid_body_constraint
if rbc is not None and rbc.enabled:
self.add_rigidbody_constraint(o, rbc)
self.add_rigidbody_constraint(o, bobject, rbc)
# Camera traits
if type is NodeType.CAMERA:
@ -2426,6 +2467,13 @@ class ArmoryExporter:
self.material_to_object_dict[mat] = [bobject]
self.material_to_arm_object_dict[mat] = [o]
# Add UniformsManager trait
if type is NodeType.MESH:
uniformManager = {}
uniformManager['type'] = 'Script'
uniformManager['class_name'] = 'armory.trait.internal.UniformsManager'
o['traits'].append(uniformManager)
# Export constraints
if len(bobject.constraints) > 0:
o['constraints'] = []
@ -2600,7 +2648,7 @@ class ArmoryExporter:
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)
@ -2636,7 +2684,7 @@ class ArmoryExporter:
}
self.output['traits'].append(out_trait)
if wrd.arm_live_patch:
if arm.utils.is_livepatch_enabled():
if 'traits' not in self.output:
self.output['traits'] = []
out_trait = {'type': 'Script', 'class_name': 'armory.trait.internal.LivePatch'}
@ -2712,7 +2760,7 @@ class ArmoryExporter:
o['traits'].append(out_trait)
@staticmethod
def add_rigidbody_constraint(o, rbc):
def add_rigidbody_constraint(o, bobject, rbc):
rb1 = rbc.object1
rb2 = rbc.object2
if rb1 is None or rb2 is None:
@ -2731,7 +2779,8 @@ class ArmoryExporter:
"'" + rb1.name + "'",
"'" + rb2.name + "'",
str(rbc.disable_collisions).lower(),
str(breaking_threshold)
str(breaking_threshold),
str(bobject.arm_relative_physics_constraint).lower()
]
}
if rbc.type == "FIXED":
@ -2847,6 +2896,7 @@ class ArmoryExporter:
out_world['sun_direction'] = list(world.arm_envtex_sun_direction)
out_world['turbidity'] = world.arm_envtex_turbidity
out_world['ground_albedo'] = world.arm_envtex_ground_albedo
out_world['nishita_density'] = list(world.arm_nishita_density)
disable_hdr = world.arm_envtex_name.endswith('.jpg')
@ -2861,16 +2911,9 @@ class ArmoryExporter:
rpdat = arm.utils.get_rp()
solid_mat = rpdat.arm_material_model == 'Solid'
arm_irradiance = rpdat.arm_irradiance and not solid_mat
arm_radiance = False
radtex = world.arm_envtex_name.rsplit('.', 1)[0]
arm_radiance = rpdat.arm_radiance
radtex = world.arm_envtex_name.rsplit('.', 1)[0] # Remove file extension
irrsharmonics = world.arm_envtex_irr_name
# Radiance
if '_EnvTex' in world.world_defs:
arm_radiance = rpdat.arm_radiance
elif '_EnvSky' in world.world_defs:
arm_radiance = rpdat.arm_radiance
radtex = 'hosek'
num_mips = world.arm_envtex_num_mips
strength = world.arm_envtex_strength

View file

@ -1,11 +1,20 @@
"""
Exports smaller geometry but is slower.
To be replaced with https://github.com/zeux/meshoptimizer
"""
from mathutils import *
import numpy as np
import arm.utils
import arm.log as log
import arm.utils
if arm.is_reload(__name__):
log = arm.reload_module(log)
arm.utils = arm.reload_module(arm.utils)
else:
arm.enable_reload(__name__)
# Exports smaller geometry but is slower
# To be replaced with https://github.com/zeux/meshoptimizer
class Vertex:
__slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index")

View file

@ -1,11 +1,14 @@
import importlib
import os
import queue
import sys
import bpy
from bpy.app.handlers import persistent
import arm
import arm.api
import arm.live_patch as live_patch
import arm.logicnode.arm_nodes as arm_nodes
import arm.nodes_logic
import arm.make as make
@ -13,6 +16,19 @@ import arm.make_state as state
import arm.props as props
import arm.utils
if arm.is_reload(__name__):
arm.api = arm.reload_module(arm.api)
live_patch = arm.reload_module(live_patch)
arm_nodes = arm.reload_module(arm_nodes)
arm.nodes_logic = arm.reload_module(arm.nodes_logic)
make = arm.reload_module(make)
state = arm.reload_module(state)
props = arm.reload_module(props)
arm.utils = arm.reload_module(arm.utils)
else:
arm.enable_reload(__name__)
@persistent
def on_depsgraph_update_post(self):
if state.proc_build != None:
@ -40,12 +56,10 @@ def on_depsgraph_update_post(self):
# Send last operator to Krom
wrd = bpy.data.worlds['Arm']
if state.proc_play != None and \
state.target == 'krom' and \
wrd.arm_live_patch:
if state.proc_play is not None and state.target == 'krom' and wrd.arm_live_patch:
ops = bpy.context.window_manager.operators
if len(ops) > 0 and ops[-1] != None:
send_operator(ops[-1])
if len(ops) > 0 and ops[-1] is not None:
live_patch.on_operator(ops[-1].bl_idname)
# Hacky solution to update armory props after operator executions
last_operator = bpy.context.active_operator
@ -89,22 +103,51 @@ def send_operator(op):
else: # Rebuild
make.patch()
def always():
def always() -> float:
# Force ui redraw
if state.redraw_ui and context_screen != None:
if state.redraw_ui and context_screen is not None:
for area in context_screen.areas:
if area.type == 'VIEW_3D' or area.type == 'PROPERTIES':
area.tag_redraw()
state.redraw_ui = False
# TODO: depsgraph.updates only triggers material trees
space = arm.utils.logic_editor_space(context_screen)
if space != None:
if space is not None:
space.node_tree.arm_cached = False
return 0.5
def poll_threads() -> float:
"""Polls the thread callback queue and if a thread has finished, it
is joined with the main thread and the corresponding callback is
executed in the main thread.
"""
try:
thread, callback = make.thread_callback_queue.get(block=False)
except queue.Empty:
return 0.25
thread.join()
try:
callback()
except Exception as e:
# If there is an exception, we can no longer return the time to
# the next call to this polling function, so to keep it running
# we re-register it and then raise the original exception.
bpy.app.timers.unregister(poll_threads)
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
raise e
# Quickly check if another thread has finished
return 0.01
appended_py_paths = []
context_screen = None
@persistent
def on_load_post(context):
global appended_py_paths
@ -181,7 +224,9 @@ def register():
bpy.app.handlers.load_post.append(on_load_post)
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update_post)
# bpy.app.handlers.undo_post.append(on_undo_post)
bpy.app.timers.register(always, persistent=True)
bpy.app.timers.register(poll_threads, persistent=True)
if arm.utils.get_fp() != '':
appended_py_paths = []
@ -198,6 +243,9 @@ def register():
def unregister():
bpy.app.timers.unregister(poll_threads)
bpy.app.timers.unregister(always)
bpy.app.handlers.load_post.remove(on_load_post)
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update_post)
# bpy.app.handlers.undo_post.remove(on_undo_post)

View file

@ -1,8 +1,16 @@
import bpy
import arm
import arm.props_ui as props_ui
if arm.is_reload(__name__):
props_ui = arm.reload_module(props_ui)
else:
arm.enable_reload(__name__)
arm_keymaps = []
def register():
wm = bpy.context.window_manager
addon_keyconfig = wm.keyconfigs.addon
@ -17,8 +25,11 @@ def register():
km = addon_keyconfig.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW")
km.keymap_items.new(props_ui.ArmoryPlayButton.bl_idname, type='F5', value='PRESS')
km.keymap_items.new("tlm.build_lightmaps", type='F6', value='PRESS')
km.keymap_items.new("tlm.clean_lightmaps", type='F7', value='PRESS')
arm_keymaps.append(km)
def unregister():
wm = bpy.context.window_manager
for km in arm_keymaps:

View file

@ -20,9 +20,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import struct
import io
import numpy as np
import struct
def _pack_integer(obj, fp):
if obj < 0:

View file

@ -1,7 +1,7 @@
import atexit
import http.server
import socketserver
import subprocess
import atexit
haxe_server = None

View file

@ -1 +1 @@
__all__ = ('Operators', 'Properties', 'Utility', 'Keymap')
__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap')

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,7 +0,0 @@
from . import keymap
def register():
keymap.register()
def unregister():
keymap.unregister()

View file

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

View file

@ -6,10 +6,11 @@ classes = [
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 @@ classes = [
tlm.TLM_StartServer,
tlm.TLM_BuildEnvironmentProbes,
tlm.TLM_CleanBuildEnvironmentProbes,
tlm.TLM_PrepareUVMaps,
tlm.TLM_LoadLightmaps,
imagetools.TLM_ImageUpscale,
imagetools.TLM_ImageDownscale

View file

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

View file

@ -21,7 +21,10 @@ class TLM_Install_OpenCV(bpy.types.Operator):
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")

View file

@ -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 @@ class TLM_CleanLightmaps(bpy.types.Operator):
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 @@ class TLM_CleanLightmaps(bpy.types.Operator):
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 @@ class TLM_CleanLightmaps(bpy.types.Operator):
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 @@ class TLM_CleanLightmaps(bpy.types.Operator):
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 @@ class TLM_ExploreLightmaps(bpy.types.Operator):
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
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
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
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":
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)
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:
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 @@ class TLM_AtlastListDeleteItem(bpy.types.Operator):
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 @@ class TLM_PostAtlastListDeleteItem(bpy.types.Operator):
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 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator):
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 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator):
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 @@ class TLM_MergeAdjacentActors(bpy.types.Operator):
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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more