diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index fe81fad3..fbe01534 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -21,7 +21,9 @@ uniform sampler2D gbufferD; uniform sampler2D gbuffer0; uniform sampler2D gbuffer1; +#ifdef _gbuffer2 uniform sampler2D gbuffer2; +#endif #ifdef _VoxelAOvar uniform sampler3D voxels; @@ -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); diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index d3d04e53..b4465351 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -60,7 +60,7 @@ float random(vec2 coords) { vec3 nishita_lookupLUT(const float height, const float sunTheta) { vec2 coords = vec2( - sqrt(height * (1 / nishita_atmo_radius)), + 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; @@ -125,7 +125,7 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa // Idea behind this: "Rotate" everything by iPos (-> iPos is the new zenith) and then all calculations for the // inner integral only depend on the sample height (iHeight) and sunTheta (angle between sun and new zenith). float sunTheta = acos(dot(normalize(iPos), normalize(pSun))); - vec3 jODepth = nishita_lookupLUT(iHeight, sunTheta);// * vec3(14000000 / 255, 14000000 / 255, 2000000 / 255); + vec3 jODepth = nishita_lookupLUT(iHeight, sunTheta); // Apply dithering to reduce visible banding jODepth += mix(-1000, 1000, random(r.xy)); diff --git a/Sources/armory/logicnode/BoneFKNode.hx b/Sources/armory/logicnode/BoneFKNode.hx index 8d8cabb1..9d72c876 100644 --- a/Sources/armory/logicnode/BoneFKNode.hx +++ b/Sources/armory/logicnode/BoneFKNode.hx @@ -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) { diff --git a/Sources/armory/logicnode/BoneIKNode.hx b/Sources/armory/logicnode/BoneIKNode.hx index de187f0a..1f1cfd76 100644 --- a/Sources/armory/logicnode/BoneIKNode.hx +++ b/Sources/armory/logicnode/BoneIKNode.hx @@ -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) { diff --git a/Sources/armory/logicnode/CompareNode.hx b/Sources/armory/logicnode/CompareNode.hx index 88dc97be..cfc02823 100644 --- a/Sources/armory/logicnode/CompareNode.hx +++ b/Sources/armory/logicnode/CompareNode.hx @@ -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": diff --git a/Sources/armory/logicnode/GateNode.hx b/Sources/armory/logicnode/GateNode.hx index ca310419..ae0014e8 100644 --- a/Sources/armory/logicnode/GateNode.hx +++ b/Sources/armory/logicnode/GateNode.hx @@ -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": diff --git a/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx b/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx new file mode 100644 index 00000000..e27dffa3 --- /dev/null +++ b/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx @@ -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 + } +} diff --git a/Sources/armory/logicnode/GetBoneTransformNode.hx b/Sources/armory/logicnode/GetBoneTransformNode.hx new file mode 100644 index 00000000..098fd519 --- /dev/null +++ b/Sources/armory/logicnode/GetBoneTransformNode.hx @@ -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 + } +} diff --git a/Sources/armory/logicnode/GetGamepadStartedNode.hx b/Sources/armory/logicnode/GetGamepadStartedNode.hx new file mode 100644 index 00000000..f94b9e59 --- /dev/null +++ b/Sources/armory/logicnode/GetGamepadStartedNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetGamepadStartedNode extends LogicNode { + + var buttonStarted: Null; + + 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; + } +} diff --git a/Sources/armory/logicnode/GetInputMapKeyNode.hx b/Sources/armory/logicnode/GetInputMapKeyNode.hx new file mode 100644 index 00000000..251a6d12 --- /dev/null +++ b/Sources/armory/logicnode/GetInputMapKeyNode.hx @@ -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; + } +} diff --git a/Sources/armory/logicnode/GetKeyboardStartedNode.hx b/Sources/armory/logicnode/GetKeyboardStartedNode.hx new file mode 100644 index 00000000..ebe752e5 --- /dev/null +++ b/Sources/armory/logicnode/GetKeyboardStartedNode.hx @@ -0,0 +1,32 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetKeyboardStartedNode extends LogicNode { + + var kb = Input.getKeyboard(); + var keyStarted: Null; + + 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; + } +} diff --git a/Sources/armory/logicnode/GetLocationNode.hx b/Sources/armory/logicnode/GetLocationNode.hx index 961f8280..00684473 100644 --- a/Sources/armory/logicnode/GetLocationNode.hx +++ b/Sources/armory/logicnode/GetLocationNode.hx @@ -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; } } diff --git a/Sources/armory/logicnode/GetMouseStartedNode.hx b/Sources/armory/logicnode/GetMouseStartedNode.hx new file mode 100644 index 00000000..8f2d5f32 --- /dev/null +++ b/Sources/armory/logicnode/GetMouseStartedNode.hx @@ -0,0 +1,32 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetMouseStartedNode extends LogicNode { + + var m = Input.getMouse(); + var buttonStarted: Null; + + 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; + } +} diff --git a/Sources/armory/logicnode/GetTraitNameNode.hx b/Sources/armory/logicnode/GetTraitNameNode.hx index dd1ded50..ca04f375 100644 --- a/Sources/armory/logicnode/GetTraitNameNode.hx +++ b/Sources/armory/logicnode/GetTraitNameNode.hx @@ -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 diff --git a/Sources/armory/logicnode/MergeNode.hx b/Sources/armory/logicnode/MergeNode.hx index 60eb6ca8..c2a22e6a 100644 --- a/Sources/armory/logicnode/MergeNode.hx +++ b/Sources/armory/logicnode/MergeNode.hx @@ -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; + } } diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx index 8b3ff763..0fae83e5 100644 --- a/Sources/armory/logicnode/OnCanvasElementNode.hx +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -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 { diff --git a/Sources/armory/logicnode/OnInputMapNode.hx b/Sources/armory/logicnode/OnInputMapNode.hx new file mode 100644 index 00000000..3e354f29 --- /dev/null +++ b/Sources/armory/logicnode/OnInputMapNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +import armory.system.InputMap; + +class OnInputMapNode extends LogicNode { + + var inputMap: Null; + + 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; + } +} diff --git a/Sources/armory/logicnode/OncePerFrameNode.hx b/Sources/armory/logicnode/OncePerFrameNode.hx new file mode 100644 index 00000000..0e2aadd5 --- /dev/null +++ b/Sources/armory/logicnode/OncePerFrameNode.hx @@ -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; + } +} diff --git a/Sources/armory/logicnode/PauseTraitNode.hx b/Sources/armory/logicnode/PauseTraitNode.hx index a040f5d1..7dc3c084 100644 --- a/Sources/armory/logicnode/PauseTraitNode.hx +++ b/Sources/armory/logicnode/PauseTraitNode.hx @@ -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(); diff --git a/Sources/armory/logicnode/RemoveInputMapKeyNode.hx b/Sources/armory/logicnode/RemoveInputMapKeyNode.hx new file mode 100644 index 00000000..44708c6d --- /dev/null +++ b/Sources/armory/logicnode/RemoveInputMapKeyNode.hx @@ -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); + } + } +} diff --git a/Sources/armory/logicnode/ResumeTraitNode.hx b/Sources/armory/logicnode/ResumeTraitNode.hx index c3c8b26b..c8a62011 100644 --- a/Sources/armory/logicnode/ResumeTraitNode.hx +++ b/Sources/armory/logicnode/ResumeTraitNode.hx @@ -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(); diff --git a/Sources/armory/logicnode/SelectNode.hx b/Sources/armory/logicnode/SelectNode.hx new file mode 100644 index 00000000..10ca5252 --- /dev/null +++ b/Sources/armory/logicnode/SelectNode.hx @@ -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; + } +} diff --git a/Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx b/Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx new file mode 100644 index 00000000..4d9b59fb --- /dev/null +++ b/Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx @@ -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 + } +} diff --git a/Sources/armory/logicnode/SetInputMapKeyNode.hx b/Sources/armory/logicnode/SetInputMapKeyNode.hx new file mode 100644 index 00000000..e657456d --- /dev/null +++ b/Sources/armory/logicnode/SetInputMapKeyNode.hx @@ -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); + } +} diff --git a/Sources/armory/logicnode/SetLocationNode.hx b/Sources/armory/logicnode/SetLocationNode.hx index 709e457b..4f879586 100644 --- a/Sources/armory/logicnode/SetLocationNode.hx +++ b/Sources/armory/logicnode/SetLocationNode.hx @@ -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); } -} +} \ No newline at end of file diff --git a/Sources/armory/logicnode/SetMaterialImageParamNode.hx b/Sources/armory/logicnode/SetMaterialImageParamNode.hx index abee9941..c74e4553 100644 --- a/Sources/armory/logicnode/SetMaterialImageParamNode.hx +++ b/Sources/armory/logicnode/SetMaterialImageParamNode.hx @@ -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>(); - + 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; + + 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); - } } diff --git a/Sources/armory/logicnode/SetMaterialRgbParamNode.hx b/Sources/armory/logicnode/SetMaterialRgbParamNode.hx index 2db7bbc9..a5289b06 100644 --- a/Sources/armory/logicnode/SetMaterialRgbParamNode.hx +++ b/Sources/armory/logicnode/SetMaterialRgbParamNode.hx @@ -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>(); - + 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; + + 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); - } } diff --git a/Sources/armory/logicnode/SetMaterialValueParamNode.hx b/Sources/armory/logicnode/SetMaterialValueParamNode.hx index 40729a64..737365cf 100644 --- a/Sources/armory/logicnode/SetMaterialValueParamNode.hx +++ b/Sources/armory/logicnode/SetMaterialValueParamNode.hx @@ -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>>(); - + 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; + + 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 { - if (mat == null) return null; - var entry = map.get(mat); - if (entry == null) return null; - return entry.get(link); - } } diff --git a/Sources/armory/logicnode/SetParentNode.hx b/Sources/armory/logicnode/SetParentNode.hx index 457facad..4ad179df 100644 --- a/Sources/armory/logicnode/SetParentNode.hx +++ b/Sources/armory/logicnode/SetParentNode.hx @@ -14,7 +14,8 @@ class SetParentNode extends LogicNode { var parent: Object; var isUnparent = false; - if (Std.is(inputs[2].fromNode, ObjectNode)) { + + if (Std.isOfType(inputs[2].fromNode, ObjectNode)) { var parentNode = cast(inputs[2].fromNode, ObjectNode); isUnparent = parentNode.objectName == ""; } diff --git a/Sources/armory/logicnode/SetTraitPausedNode.hx b/Sources/armory/logicnode/SetTraitPausedNode.hx index 87099948..53327e1f 100644 --- a/Sources/armory/logicnode/SetTraitPausedNode.hx +++ b/Sources/armory/logicnode/SetTraitPausedNode.hx @@ -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(); diff --git a/Sources/armory/logicnode/VectorToObjectOrientationNode.hx b/Sources/armory/logicnode/VectorToObjectOrientationNode.hx index 2a72590a..27a4b994 100644 --- a/Sources/armory/logicnode/VectorToObjectOrientationNode.hx +++ b/Sources/armory/logicnode/VectorToObjectOrientationNode.hx @@ -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); } } diff --git a/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx b/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx index 493ed615..75f128c1 100644 --- a/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx +++ b/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx @@ -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()); diff --git a/Sources/armory/renderpath/Inc.hx b/Sources/armory/renderpath/Inc.hx index ad44d2d8..0e37fdef 100644 --- a/Sources/armory/renderpath/Inc.hx +++ b/Sources/armory/renderpath/Inc.hx @@ -715,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; @@ -742,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; @@ -755,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; @@ -768,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; diff --git a/Sources/armory/system/InputMap.hx b/Sources/armory/system/InputMap.hx index 79b998e1..2bd2b493 100644 --- a/Sources/armory/system/InputMap.hx +++ b/Sources/armory/system/InputMap.hx @@ -4,347 +4,223 @@ import kha.FastFloat; import iron.system.Input; class InputMap { - var commands = new Map>>(); + + static var inputMaps = new Map(); + + public var keys(default, null) = new Array(); + 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 { + 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(); - 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 { + 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(); - var modifiers = new Array(); - var displacementKeys = new Array(); - var displacementModifiers = new Array(); - var deadzone: FastFloat = 0.0; - var scale: FastFloat = 1.0; - - public function new() {} - - public function setKeys(keys: Array) { - return this.keys = keys; - } - - public function setMods(modifiers: Array) { - return this.modifiers = modifiers; - } - - public function setDisplacementKeys(keys: Array) { - return displacementKeys = keys; - } - - public function setDisplacementMods(modifiers: Array) { - 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; } -} \ No newline at end of file +} + +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); + } +} diff --git a/Sources/armory/trait/FollowCamera.hx b/Sources/armory/trait/FollowCamera.hx index 6cfdf84e..99df3c70 100644 --- a/Sources/armory/trait/FollowCamera.hx +++ b/Sources/armory/trait/FollowCamera.hx @@ -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."); } diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 25bfdc6e..cf7da8cf 100755 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -594,7 +594,7 @@ class DebugConsole extends Trait { } function drawTiles(tile: ShadowMapTile, atlas: ShadowMapAtlas, atlasVisualSize: Float) { - var color = kha.Color.fromFloats(0.1, 0.1, 0.1); + var color: Null = 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; diff --git a/Sources/armory/trait/internal/MovieTexture.hx b/Sources/armory/trait/internal/MovieTexture.hx index 1a3dc03a..3adee41e 100644 --- a/Sources/armory/trait/internal/MovieTexture.hx +++ b/Sources/armory/trait/internal/MovieTexture.hx @@ -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> = 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 = imageCache[w]; + if (hMap == null) { + imageCache[w] = new Map(); + } + + 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); }); diff --git a/Sources/armory/trait/internal/UniformsManager.hx b/Sources/armory/trait/internal/UniformsManager.hx new file mode 100644 index 00000000..b33624d9 --- /dev/null +++ b/Sources/armory/trait/internal/UniformsManager.hx @@ -0,0 +1,313 @@ +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>>>(); + + static var vectorsRegistered = false; + static var vectorsMap = new Map>>(); + + static var texturesRegistered = false; + static var texturesMap = new Map>>(); + + 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 + + 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){ + + 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 { + + 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>>>{ + + return floatsMap; + } + + // Returns complete map of vec3 value material paramets + public static function getVectorsMap():Map>>{ + + return vectorsMap; + } + + // Returns complete map of texture value material paramets + public static function getTexturesMap():Map>>{ + + 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; +} \ No newline at end of file diff --git a/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx b/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx index 11a0ac24..bb536bf2 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx @@ -18,8 +18,9 @@ class PhysicsConstraintExportHelper extends iron.Trait { var breakingThreshold: Float; var limits: Array; var constraintAdded: Bool = false; + var relativeConstraint: Bool = false; - public function new(body1: String, body2: String, type: Int, disableCollisions: Bool, breakingThreshold: Float, limits: Array = null) { + public function new(body1: String, body2: String, type: Int, disableCollisions: Bool, breakingThreshold: Float, relatieConstraint: Bool = false, limits: Array = 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; } diff --git a/Sources/armory/trait/physics/bullet/RigidBody.hx b/Sources/armory/trait/physics/bullet/RigidBody.hx index 5ec1eb83..3c48adc7 100644 --- a/Sources/armory/trait/physics/bullet/RigidBody.hx +++ b/Sources/armory/trait/physics/bullet/RigidBody.hx @@ -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; diff --git a/Sources/armory/ui/Canvas.hx b/Sources/armory/ui/Canvas.hx index 2c66cf68..dc798adc 100644 --- a/Sources/armory/ui/Canvas.hx +++ b/Sources/armory/ui/Canvas.hx @@ -58,18 +58,18 @@ class Canvas { 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 font = ui.ops.font; var size = ui.fontSize; - var fontAsset = element.asset != null && StringTools.endsWith(element.asset, ".ttf"); - if (fontAsset) ui.ops.font = getAsset(canvas, element.asset); 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.ops.font = font; ui.fontSize = size; case Button: @@ -90,7 +90,6 @@ class Canvas { case Image: var image = getAsset(canvas, element.asset); - var fontAsset = element.asset != null && StringTools.endsWith(element.asset, ".ttf"); if (image != null && !fontAsset) { ui.imageScrollAlign = false; var tint = element.color != null ? element.color : 0xffffffff; @@ -218,6 +217,8 @@ class Canvas { 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); @@ -257,6 +258,10 @@ class Canvas { return Std.int(f * _ui.SCALE()); } + static inline function isFontAsset(assetName: Null): Bool { + return assetName != null && StringTools.endsWith(assetName.toLowerCase(), ".ttf"); + } + public static inline function getColor(color: Null, defaultColor: Int): Int { return color != null ? color : defaultColor; } diff --git a/Sources/armory/ui/Ext.hx b/Sources/armory/ui/Ext.hx index 0104ddd5..1197ef46 100644 --- a/Sources/armory/ui/Ext.hx +++ b/Sources/armory/ui/Ext.hx @@ -172,160 +172,160 @@ class Ext { Keycodes can be found here: http://api.kha.tech/kha/input/KeyCode.html **/ static function keycodeToString(keycode: Int): String { - switch (keycode) { - case -1: return "None"; - case KeyCode.Unknown: return "Unknown"; - case KeyCode.Back: return "Back"; - case KeyCode.Cancel: return "Cancel"; - case KeyCode.Help: return "Help"; - case KeyCode.Backspace: return "Backspace"; - case KeyCode.Tab: return "Tab"; - case KeyCode.Clear: return "Clear"; - case KeyCode.Return: return "Return"; - case KeyCode.Shift: return "Shift"; - case KeyCode.Control: return "Ctrl"; - case KeyCode.Alt: return "Alt"; - case KeyCode.Pause: return "Pause"; - case KeyCode.CapsLock: return "CapsLock"; - case KeyCode.Kana: return "Kana"; - // case KeyCode.Hangul: return "Hangul"; // Hangul == Kana - case KeyCode.Eisu: return "Eisu"; - case KeyCode.Junja: return "Junja"; - case KeyCode.Final: return "Final"; - case KeyCode.Hanja: return "Hanja"; - // case KeyCode.Kanji: return "Kanji"; // Kanji == Hanja - case KeyCode.Escape: return "Esc"; - case KeyCode.Convert: return "Convert"; - case KeyCode.NonConvert: return "NonConvert"; - case KeyCode.Accept: return "Accept"; - case KeyCode.ModeChange: return "ModeChange"; - case KeyCode.Space: return "Space"; - case KeyCode.PageUp: return "PageUp"; - case KeyCode.PageDown: return "PageDown"; - case KeyCode.End: return "End"; - case KeyCode.Home: return "Home"; - case KeyCode.Left: return "Left"; - case KeyCode.Up: return "Up"; - case KeyCode.Right: return "Right"; - case KeyCode.Down: return "Down"; - case KeyCode.Select: return "Select"; - case KeyCode.Print: return "Print"; - case KeyCode.Execute: return "Execute"; - case KeyCode.PrintScreen: return "PrintScreen"; - case KeyCode.Insert: return "Insert"; - case KeyCode.Delete: return "Delete"; - case KeyCode.Colon: return "Colon"; - case KeyCode.Semicolon: return "Semicolon"; - case KeyCode.LessThan: return "LessThan"; - case KeyCode.Equals: return "Equals"; - case KeyCode.GreaterThan: return "GreaterThan"; - case KeyCode.QuestionMark: return "QuestionMark"; - case KeyCode.At: return "At"; - case KeyCode.Win: return "Win"; - case KeyCode.ContextMenu: return "ContextMenu"; - case KeyCode.Sleep: return "Sleep"; - case KeyCode.Numpad0: return "Numpad0"; - case KeyCode.Numpad1: return "Numpad1"; - case KeyCode.Numpad2: return "Numpad2"; - case KeyCode.Numpad3: return "Numpad3"; - case KeyCode.Numpad4: return "Numpad4"; - case KeyCode.Numpad5: return "Numpad5"; - case KeyCode.Numpad6: return "Numpad6"; - case KeyCode.Numpad7: return "Numpad7"; - case KeyCode.Numpad8: return "Numpad8"; - case KeyCode.Numpad9: return "Numpad9"; - case KeyCode.Multiply: return "Multiply"; - case KeyCode.Add: return "Add"; - case KeyCode.Separator: return "Separator"; - case KeyCode.Subtract: return "Subtract"; - case KeyCode.Decimal: return "Decimal"; - case KeyCode.Divide: return "Divide"; - case KeyCode.F1: return "F1"; - case KeyCode.F2: return "F2"; - case KeyCode.F3: return "F3"; - case KeyCode.F4: return "F4"; - case KeyCode.F5: return "F5"; - case KeyCode.F6: return "F6"; - case KeyCode.F7: return "F7"; - case KeyCode.F8: return "F8"; - case KeyCode.F9: return "F9"; - case KeyCode.F10: return "F10"; - case KeyCode.F11: return "F11"; - case KeyCode.F12: return "F12"; - case KeyCode.F13: return "F13"; - case KeyCode.F14: return "F14"; - case KeyCode.F15: return "F15"; - case KeyCode.F16: return "F16"; - case KeyCode.F17: return "F17"; - case KeyCode.F18: return "F18"; - case KeyCode.F19: return "F19"; - case KeyCode.F20: return "F20"; - case KeyCode.F21: return "F21"; - case KeyCode.F22: return "F22"; - case KeyCode.F23: return "F23"; - case KeyCode.F24: return "F24"; - case KeyCode.NumLock: return "NumLock"; - case KeyCode.ScrollLock: return "ScrollLock"; - case KeyCode.WinOemFjJisho: return "WinOemFjJisho"; - case KeyCode.WinOemFjMasshou: return "WinOemFjMasshou"; - case KeyCode.WinOemFjTouroku: return "WinOemFjTouroku"; - case KeyCode.WinOemFjLoya: return "WinOemFjLoya"; - case KeyCode.WinOemFjRoya: return "WinOemFjRoya"; - case KeyCode.Circumflex: return "Circumflex"; - case KeyCode.Exclamation: return "Exclamation"; - case KeyCode.DoubleQuote: return "DoubleQuote"; - case KeyCode.Hash: return "Hash"; - case KeyCode.Dollar: return "Dollar"; - case KeyCode.Percent: return "Percent"; - case KeyCode.Ampersand: return "Ampersand"; - case KeyCode.Underscore: return "Underscore"; - case KeyCode.OpenParen: return "OpenParen"; - case KeyCode.CloseParen: return "CloseParen"; - case KeyCode.Asterisk: return "Asterisk"; - case KeyCode.Plus: return "Plus"; - case KeyCode.Pipe: return "Pipe"; - case KeyCode.HyphenMinus: return "HyphenMinus"; - case KeyCode.OpenCurlyBracket: return "OpenCurlyBracket"; - case KeyCode.CloseCurlyBracket: return "CloseCurlyBracket"; - case KeyCode.Tilde: return "Tilde"; - case KeyCode.VolumeMute: return "VolumeMute"; - case KeyCode.VolumeDown: return "VolumeDown"; - case KeyCode.VolumeUp: return "VolumeUp"; - case KeyCode.Comma: return "Comma"; - case KeyCode.Period: return "Period"; - case KeyCode.Slash: return "Slash"; - case KeyCode.BackQuote: return "BackQuote"; - case KeyCode.OpenBracket: return "OpenBracket"; - case KeyCode.BackSlash: return "BackSlash"; - case KeyCode.CloseBracket: return "CloseBracket"; - case KeyCode.Quote: return "Quote"; - case KeyCode.Meta: return "Meta"; - case KeyCode.AltGr: return "AltGr"; - case KeyCode.WinIcoHelp: return "WinIcoHelp"; - case KeyCode.WinIco00: return "WinIco00"; - case KeyCode.WinIcoClear: return "WinIcoClear"; - case KeyCode.WinOemReset: return "WinOemReset"; - case KeyCode.WinOemJump: return "WinOemJump"; - case KeyCode.WinOemPA1: return "WinOemPA1"; - case KeyCode.WinOemPA2: return "WinOemPA2"; - case KeyCode.WinOemPA3: return "WinOemPA3"; - case KeyCode.WinOemWSCTRL: return "WinOemWSCTRL"; - case KeyCode.WinOemCUSEL: return "WinOemCUSEL"; - case KeyCode.WinOemATTN: return "WinOemATTN"; - case KeyCode.WinOemFinish: return "WinOemFinish"; - case KeyCode.WinOemCopy: return "WinOemCopy"; - case KeyCode.WinOemAuto: return "WinOemAuto"; - case KeyCode.WinOemENLW: return "WinOemENLW"; - case KeyCode.WinOemBackTab: return "WinOemBackTab"; - case KeyCode.ATTN: return "ATTN"; - case KeyCode.CRSEL: return "CRSEL"; - case KeyCode.EXSEL: return "EXSEL"; - case KeyCode.EREOF: return "EREOF"; - case KeyCode.Play: return "Play"; - case KeyCode.Zoom: return "Zoom"; - case KeyCode.PA1: return "PA1"; - case KeyCode.WinOemClear: return "WinOemClear"; + 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"; } - return String.fromCharCode(keycode); } } diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index c177ebdc..ff740963 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2434,7 +2434,7 @@ Make sure the mesh only has tris/quads.""") # 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: @@ -2454,6 +2454,13 @@ Make sure the mesh only has tris/quads.""") else: 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: @@ -2741,7 +2748,7 @@ Make sure the mesh only has tris/quads.""") 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: @@ -2760,7 +2767,8 @@ Make sure the mesh only has tris/quads.""") "'" + 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": diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py index 364ae17e..24786a07 100644 --- a/blender/arm/handlers.py +++ b/blender/arm/handlers.py @@ -1,5 +1,6 @@ import importlib import os +import queue import sys import bpy @@ -70,20 +71,56 @@ def on_operator_post(operator_id: str) -> None: target_obj.arm_rb_collision_filter_mask = source_obj.arm_rb_collision_filter_mask -def always(): +def send_operator(op): + if hasattr(bpy.context, 'object') and bpy.context.object != None: + obj = bpy.context.object.name + if op.name == 'Move': + vec = bpy.context.object.location + js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.loc.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;' + make.write_patch(js) + elif op.name == 'Resize': + vec = bpy.context.object.scale + js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.scale.set(' + str(vec[0]) + ', ' + str(vec[1]) + ', ' + str(vec[2]) + '); o.transform.dirty = true;' + make.write_patch(js) + elif op.name == 'Rotate': + vec = bpy.context.object.rotation_euler.to_quaternion() + js = 'var o = iron.Scene.active.getChild("' + obj + '"); o.transform.rot.set(' + str(vec[1]) + ', ' + str(vec[2]) + ', ' + str(vec[3]) + ' ,' + str(vec[0]) + '); o.transform.dirty = true;' + make.write_patch(js) + else: # Rebuild + make.patch() + + +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() + callback() + + # Quickly check if another thread has finished + return 0.01 + + appended_py_paths = [] context_screen = None @@ -164,7 +201,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 = [] @@ -181,6 +220,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) diff --git a/blender/arm/logicnode/animation/LN_bone_ik.py b/blender/arm/logicnode/animation/LN_bone_ik.py index e677b9d8..66e4d32d 100644 --- a/blender/arm/logicnode/animation/LN_bone_ik.py +++ b/blender/arm/logicnode/animation/LN_bone_ik.py @@ -1,16 +1,50 @@ from arm.logicnode.arm_nodes import * class BoneIKNode(ArmLogicTreeNode): - """Applies inverse kinematics in the given object bone.""" + """Performs inverse kinematics on the selected armature with specified bone. + + @input Object: Armature on which IK should be performed. + + @input Bone: Effector or tip bone for the inverse kinematics + + @input Goal Position: Position in world coordinates the effector bone will track to + + @input Enable Pole: Bend IK solution towards pole location + + @input Pole Position: Location of the pole in world coordinates + + @input Chain Length: Number of bones to include in the IK solver including the effector. If set to 0, all bones from effector to the root bone of the armature will be considered. + + @input Max Iterations: Maximum allowed FABRIK iterations to solve for IK. For longer chains, more iterations are needed. + + @input Precision: Presition of IK to stop at. It is described as a tolerence in length. Typically 0.01 is a good value. + + @input Roll Angle: Roll the bones along their local axis with specified radians. set 0 for no extra roll. + """ bl_idname = 'LNBoneIKNode' bl_label = 'Bone IK' - arm_version = 1 + arm_version = 2 arm_section = 'armature' def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmStringSocket', 'Bone') - self.add_input('ArmVectorSocket', 'Goal') + self.add_input('ArmVectorSocket', 'Goal Position') + self.add_input('ArmBoolSocket', 'Enable Pole') + self.add_input('ArmVectorSocket', 'Pole Position') + self.add_input('ArmIntSocket', 'Chain Length') + self.add_input('ArmIntSocket', 'Max Iterations', 10) + self.add_input('ArmFloatSocket', 'Precision', 0.01) + self.add_input('ArmFloatSocket', 'Roll Angle') self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNBoneIKNode', self.arm_version, 'LNBoneIKNode', 2, + in_socket_mapping={0:0, 1:1, 2:2, 3:3}, out_socket_mapping={0:0} + ) diff --git a/blender/arm/logicnode/animation/LN_get_bone_fk_ik_only.py b/blender/arm/logicnode/animation/LN_get_bone_fk_ik_only.py new file mode 100644 index 00000000..165b946d --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_bone_fk_ik_only.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetBoneFkIkOnlyNode(ArmLogicTreeNode): + """Get if a particular bone is animated by Forward kinematics or Inverse kinematics only.""" + bl_idname = 'LNGetBoneFkIkOnlyNode' + bl_label = 'Get Bone FK IK Only' + arm_version = 1 + arm_section = 'armature' + + def init(self, context): + super(GetBoneFkIkOnlyNode, self).init(context) + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('NodeSocketString', 'Bone') + self.add_output('NodeSocketBool', 'FK or IK only') \ No newline at end of file diff --git a/blender/arm/logicnode/animation/LN_get_bone_transform.py b/blender/arm/logicnode/animation/LN_get_bone_transform.py new file mode 100644 index 00000000..0d21ea23 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_bone_transform.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetBoneTransformNode(ArmLogicTreeNode): + """Returns bone transform in world space.""" + bl_idname = 'LNGetBoneTransformNode' + bl_label = 'Get Bone Transform' + arm_version = 1 + arm_section = 'armature' + + def init(self, context): + super(GetBoneTransformNode, self).init(context) + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('NodeSocketString', 'Bone') + self.add_output('NodeSocketShader', 'Transform') \ No newline at end of file diff --git a/blender/arm/logicnode/animation/LN_set_bone_fk_ik_only.py b/blender/arm/logicnode/animation/LN_set_bone_fk_ik_only.py new file mode 100644 index 00000000..2594183a --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_bone_fk_ik_only.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class SetBoneFkIkOnlyNode(ArmLogicTreeNode): + """Set particular bone to be animated by Forward kinematics or Inverse kinematics only. All other animations will be ignored""" + bl_idname = 'LNSetBoneFkIkOnlyNode' + bl_label = 'Set Bone FK IK Only' + arm_version = 1 + arm_section = 'armature' + + def init(self, context): + super(SetBoneFkIkOnlyNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('NodeSocketString', 'Bone') + self.add_input('NodeSocketBool', 'FK or IK only') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 5fd365a6..85ae3730 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -347,6 +347,30 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): return{'FINISHED'} +class ArmNodeCallFuncButton(bpy.types.Operator): + """Operator that calls a function on a specified + node (used for dynamic callbacks).""" + bl_idname = 'arm.node_call_func' + bl_label = 'Execute' + bl_options = {'UNDO', 'INTERNAL'} + + node_index: StringProperty(name='Node Index', default='') + callback_name: StringProperty(name='Callback Name', default='') + + def execute(self, context): + node = array_nodes[self.node_index] + if hasattr(node, self.callback_name): + getattr(node, self.callback_name)() + else: + return {'CANCELLED'} + + # Reset to default again for subsequent calls of this operator + self.node_index = '' + self.callback_name = '' + + return {'FINISHED'} + + class ArmNodeSearch(bpy.types.Operator): bl_idname = "arm.node_search" bl_label = "Search..." @@ -537,12 +561,16 @@ def reset_globals(): category_items = OrderedDict() -bpy.utils.register_class(ArmNodeSearch) -bpy.utils.register_class(ArmNodeAddInputButton) -bpy.utils.register_class(ArmNodeAddInputValueButton) -bpy.utils.register_class(ArmNodeRemoveInputButton) -bpy.utils.register_class(ArmNodeRemoveInputValueButton) -bpy.utils.register_class(ArmNodeAddOutputButton) -bpy.utils.register_class(ArmNodeRemoveOutputButton) -bpy.utils.register_class(ArmNodeAddInputOutputButton) -bpy.utils.register_class(ArmNodeRemoveInputOutputButton) +REG_CLASSES = ( + ArmNodeSearch, + ArmNodeAddInputButton, + ArmNodeAddInputValueButton, + ArmNodeRemoveInputButton, + ArmNodeRemoveInputValueButton, + ArmNodeAddOutputButton, + ArmNodeRemoveOutputButton, + ArmNodeAddInputOutputButton, + ArmNodeRemoveInputOutputButton, + ArmNodeCallFuncButton +) +register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) diff --git a/blender/arm/logicnode/input/LN_get_gamepad_started.py b/blender/arm/logicnode/input/LN_get_gamepad_started.py new file mode 100644 index 00000000..edfbc040 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_gamepad_started.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetGamepadStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetGamepadStartedNode' + bl_label = 'Get Gamepad Started' + arm_version = 1 + + def init(self, context): + super(GetGamepadStartedNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('NodeSocketInt', 'Index') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('NodeSocketString', 'Button') diff --git a/blender/arm/logicnode/input/LN_get_input_map_key.py b/blender/arm/logicnode/input/LN_get_input_map_key.py new file mode 100644 index 00000000..12d98935 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_input_map_key.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetInputMapKeyNode(ArmLogicTreeNode): + """Get key data if it exists in the input map.""" + bl_idname = 'LNGetInputMapKeyNode' + bl_label = 'Get Input Map Key' + arm_version = 1 + + def init(self, context): + super(GetInputMapKeyNode, self).init(context) + self.add_input('NodeSocketString', 'Input Map') + self.add_input('NodeSocketString', 'Key') + + self.add_output('NodeSocketFloat', 'Scale', default_value = 1.0) + self.add_output('NodeSocketFloat', 'Deadzone') \ No newline at end of file diff --git a/blender/arm/logicnode/input/LN_get_keyboard_started.py b/blender/arm/logicnode/input/LN_get_keyboard_started.py new file mode 100644 index 00000000..0f655b04 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_keyboard_started.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetKeyboardStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetKeyboardStartedNode' + bl_label = 'Get Keyboard Started' + arm_version = 1 + + def init(self, context): + super(GetKeyboardStartedNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('NodeSocketString', 'Key') diff --git a/blender/arm/logicnode/input/LN_get_mouse_started.py b/blender/arm/logicnode/input/LN_get_mouse_started.py new file mode 100644 index 00000000..368aa433 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_mouse_started.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetMouseStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetMouseStartedNode' + bl_label = 'Get Mouse Started' + arm_version = 1 + + def init(self, context): + super(GetMouseStartedNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('NodeSocketString', 'Button') diff --git a/blender/arm/logicnode/input/LN_on_input_map.py b/blender/arm/logicnode/input/LN_on_input_map.py new file mode 100644 index 00000000..0cc540f5 --- /dev/null +++ b/blender/arm/logicnode/input/LN_on_input_map.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class OnInputMapNode(ArmLogicTreeNode): + """Send a signal if any input map key is started or released.""" + bl_idname = 'LNOnInputMapNode' + bl_label = 'On Input Map' + arm_version = 1 + + def init(self, context): + super(OnInputMapNode, self).init(context) + self.add_input('NodeSocketString', 'Input Map') + + self.add_output('ArmNodeSocketAction', 'Started') + self.add_output('ArmNodeSocketAction', 'Released') + self.add_output('NodeSocketFloat', 'Value') + self.add_output('NodeSocketString', 'Key Pressed') \ No newline at end of file diff --git a/blender/arm/logicnode/input/LN_remove_input_map_key.py b/blender/arm/logicnode/input/LN_remove_input_map_key.py new file mode 100644 index 00000000..8821ef05 --- /dev/null +++ b/blender/arm/logicnode/input/LN_remove_input_map_key.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class RemoveInputMapKeyNode(ArmLogicTreeNode): + """Remove input map key.""" + bl_idname = 'LNRemoveInputMapKeyNode' + bl_label = 'Remove Input Map Key' + arm_version = 1 + + def init(self, context): + super(RemoveInputMapKeyNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('NodeSocketString', 'Input Map') + self.add_input('NodeSocketString', 'Key') + + self.add_output('ArmNodeSocketAction', 'Out') \ No newline at end of file diff --git a/blender/arm/logicnode/input/LN_set_input_map_key.py b/blender/arm/logicnode/input/LN_set_input_map_key.py new file mode 100644 index 00000000..a2157745 --- /dev/null +++ b/blender/arm/logicnode/input/LN_set_input_map_key.py @@ -0,0 +1,28 @@ +from arm.logicnode.arm_nodes import * + +class SetInputMapKeyNode(ArmLogicTreeNode): + """Set input map key.""" + bl_idname = 'LNSetInputMapKeyNode' + bl_label = 'Set Input Map Key' + arm_version = 1 + + property0: EnumProperty( + items = [('keyboard', 'Keyboard', 'Keyboard input'), + ('mouse', 'Mouse', 'Mouse input'), + ('gamepad', 'Gamepad', 'Gamepad input')], + name='', default='keyboard') + + def init(self, context): + super(SetInputMapKeyNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('NodeSocketString', 'Input Map') + self.add_input('NodeSocketString', 'Key') + self.add_input('NodeSocketFloat', 'Scale', default_value=1.0) + self.add_input('NodeSocketFloat', 'Deadzone') + self.add_input('NodeSocketInt', 'Index') + + self.add_output('ArmNodeSocketAction', 'Out') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') \ No newline at end of file diff --git a/blender/arm/logicnode/logic/LN_merge.py b/blender/arm/logicnode/logic/LN_merge.py index c5e369de..5f6b1f73 100644 --- a/blender/arm/logicnode/logic/LN_merge.py +++ b/blender/arm/logicnode/logic/LN_merge.py @@ -1,14 +1,45 @@ from arm.logicnode.arm_nodes import * + class MergeNode(ArmLogicTreeNode): - """Activates the output when any connected input is activated. + """Activates the output when at least one connected input is activated. + If multiple inputs are active, the behaviour is specified by the + `Execution Mode` option. + + @output Active Input Index: [*Available if Execution Mode is set to + Once Per Input*] The index of the last input that activated the output, + -1 if there was no execution yet on the current frame. + + @option Execution Mode: The node's behaviour if multiple inputs are + active on the same frame. + + - `Once Per Input`: If multiple inputs are active on one frame, activate + the output for each active input individually (simple forwarding). + + - `Once Per Frame`: If multiple inputs are active on one frame, + trigger the output only once. @option New: Add a new input socket. @option X Button: Remove the lowermost input socket.""" bl_idname = 'LNMergeNode' bl_label = 'Merge' arm_section = 'flow' - arm_version = 1 + arm_version = 2 + + def update_exec_mode(self, context): + self.outputs['Active Input Index'].hide = self.property0 == 'once_per_frame' + + property0: EnumProperty( + name='Execution Mode', + description='The node\'s behaviour if multiple inputs are active on the same frame', + items=[('once_per_input', 'Once Per Input', + 'If multiple inputs are active on one frame, activate the' + ' output for each active input individually (simple forwarding)'), + ('once_per_frame', 'Once Per Frame', + 'If multiple inputs are active on one frame, trigger the output only once')], + default='once_per_input', + update=update_exec_mode, + ) def __init__(self): super(MergeNode, self).__init__() @@ -16,10 +47,12 @@ class MergeNode(ArmLogicTreeNode): def arm_init(self, context): self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('NodeSocketInt', 'Active Input Index') def draw_buttons(self, context, layout): - row = layout.row(align=True) + layout.prop(self, 'property0', text='') + row = layout.row(align=True) op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) op.node_index = str(id(self)) op.socket_type = 'ArmNodeSocketAction' @@ -31,3 +64,24 @@ class MergeNode(ArmLogicTreeNode): return self.bl_label return f'{self.bl_label}: [{len(self.inputs)}]' + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + newnode = node_tree.nodes.new('LNMergeNode') + newnode.property0 = self.property0 + + # Recreate all original inputs + array_nodes[str(id(newnode))] = newnode + for idx, input in enumerate(self.inputs): + bpy.ops.arm.node_add_input('EXEC_DEFAULT', node_index=str(id(newnode)), socket_type='ArmNodeSocketAction') + + for link in input.links: + node_tree.links.new(link.from_socket, newnode.inputs[idx]) + + # Recreate outputs + for link in self.outputs[0].links: + node_tree.links.new(newnode.outputs[0], link.to_socket) + + return newnode diff --git a/blender/arm/logicnode/logic/LN_once_per_frame.py b/blender/arm/logicnode/logic/LN_once_per_frame.py new file mode 100644 index 00000000..b7dba2f1 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_once_per_frame.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class OncePerFrameNode(ArmLogicTreeNode): + """Activates the output only once per frame if receives one or more inputs in that frame + If there is no input, there will be no output""" + bl_idname = 'LNOncePerFrameNode' + bl_label = 'Once Per Frame' + arm_section = 'flow' + arm_version = 1 + + def init(self, context): + super(OncePerFrameNode, self).init(context) + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_select.py b/blender/arm/logicnode/logic/LN_select.py new file mode 100644 index 00000000..4ea3762a --- /dev/null +++ b/blender/arm/logicnode/logic/LN_select.py @@ -0,0 +1,122 @@ +from bpy.types import NodeSocketInterfaceInt + +from arm.logicnode.arm_nodes import * + + +class SelectNode(ArmLogicTreeNode): + """Selects one of multiple values (of arbitrary types) based on some + input state. The exact behaviour of this node is specified by the + `Execution Mode` option (see below). + + @output Out: [*Available if Execution Mode is set to From Input*] + Activated after the node was executed. + + @output Value: The last selected value. This value is not reset + until the next execution of this node. + + @option Execution Mode: Specifies the condition that determines + what value to choose. + + - `From Index`: Select the value at the given index. If there is + no value at that index, the value plugged in to the + `Default` input is used instead (`null` if unconnected). + + - `From Input`: This mode uses input pairs of one action socket + and one value socket. Depending on which action socket is + activated, the associated value socket (the value with the + same index as the activated action input) is forwarded to + the `Value` output. + + @option New: Add a new value to the list of values. + @option X Button: Remove the value with the highest index.""" + bl_idname = 'LNSelectNode' + bl_label = 'Select' + arm_version = 1 + min_inputs = 2 + + def update_exec_mode(self, context): + self.set_mode() + + property0: EnumProperty( + name='Execution Mode', + description="The node's behaviour.", + items=[ + ('from_index', 'From Index', 'Choose the value from the given index'), + ('from_input', 'From Input', 'Choose the value with the same position as the active input')], + default='from_index', + update=update_exec_mode, + ) + + # The number of choices, NOT of individual inputs. This needs to be + # a property in order to be saved with each individual node + num_choices: IntProperty(default=1, min=0) + + def __init__(self): + super().__init__() + array_nodes[str(id(self))] = self + + def init(self, context): + super().init(context) + + self.set_mode() + + def set_mode(self): + self.inputs.clear() + self.outputs.clear() + + if self.property0 == 'from_index': + self.add_input('NodeSocketInt', 'Index') + self.add_input('NodeSocketShader', 'Default') + self.num_choices = 0 + + # from_input + else: + # We could also start with index 1 here, but we need to use + # 0 for the "from_index" mode and it makes the code simpler + # if we stick to the same convention for both exec modes + self.add_input('ArmNodeSocketAction', 'Input 0') + self.add_input('NodeSocketShader', 'Value 0') + self.num_choices = 1 + + self.add_output('ArmNodeSocketAction', 'Out') + + self.add_output('NodeSocketShader', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0', text='') + + row = layout.row(align=True) + + op = row.operator('arm.node_call_func', text='New', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'add_input_func' + + op = row.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'remove_input_func' + + def add_input_func(self): + if self.property0 == 'from_input': + self.add_input('ArmNodeSocketAction', f'Input {self.num_choices}') + + # Move new action input up to the end of all other action inputs + self.inputs.move(from_index=len(self.inputs) - 1, to_index=self.num_choices) + + self.add_input('NodeSocketShader', f'Value {self.num_choices}') + + self.num_choices += 1 + + def remove_input_func(self): + if self.property0 == 'from_input': + if len(self.inputs) > self.min_inputs: + self.inputs.remove(self.inputs[self.num_choices - 1]) + + if len(self.inputs) > self.min_inputs: + self.inputs.remove(self.inputs[-1]) + self.num_choices -= 1 + + def draw_label(self) -> str: + if self.num_choices == 0: + return self.bl_label + + return f'{self.bl_label}: [{self.num_choices}]' diff --git a/blender/arm/logicnode/material/LN_set_material_image_param.py b/blender/arm/logicnode/material/LN_set_material_image_param.py index 38ae1bf3..686562c1 100644 --- a/blender/arm/logicnode/material/LN_set_material_image_param.py +++ b/blender/arm/logicnode/material/LN_set_material_image_param.py @@ -1,16 +1,42 @@ from arm.logicnode.arm_nodes import * class SetMaterialImageParamNode(ArmLogicTreeNode): - """TO DO.""" + """Set an image value material parameter to the specified object. + + @seeNode Get Scene Root + + @input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally. + + @input Per Object: + - `Enabled`: Set material parameter specific to this object. Global parameter will be ignored. + - `Disabled`: Set parameter globally, including this object. + + @input Material: Material whose parameter to be set. + + @input Node: Name of the parameter. + + @input Image: Name of the image. + """ bl_idname = 'LNSetMaterialImageParamNode' bl_label = 'Set Material Image Param' arm_section = 'params' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Per Object') self.add_input('ArmDynamicSocket', 'Material') self.add_input('ArmStringSocket', 'Node') self.add_input('ArmStringSocket', 'Image') self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNSetMaterialImageParamNode', self.arm_version, 'LNSetMaterialImageParamNode', 2, + in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0} + ) diff --git a/blender/arm/logicnode/material/LN_set_material_rgb_param.py b/blender/arm/logicnode/material/LN_set_material_rgb_param.py index b8a4b5f6..607c0ed2 100644 --- a/blender/arm/logicnode/material/LN_set_material_rgb_param.py +++ b/blender/arm/logicnode/material/LN_set_material_rgb_param.py @@ -1,16 +1,42 @@ from arm.logicnode.arm_nodes import * class SetMaterialRgbParamNode(ArmLogicTreeNode): - """TO DO.""" + """Set a color or vector value material parameter to the specified object. + + @seeNode Get Scene Root + + @input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally. + + @input Per Object: + - `Enabled`: Set material parameter specific to this object. Global parameter will be ignored. + - `Disabled`: Set parameter globally, including this object. + + @input Material: Material whose parameter to be set. + + @input Node: Name of the parameter. + + @input Color: Color or vector input. + """ bl_idname = 'LNSetMaterialRgbParamNode' bl_label = 'Set Material RGB Param' arm_section = 'params' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Per Object') self.add_input('ArmDynamicSocket', 'Material') self.add_input('ArmStringSocket', 'Node') self.add_input('ArmColorSocket', 'Color') self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNSetMaterialRgbParamNode', self.arm_version, 'LNSetMaterialRgbParamNode', 2, + in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0} + ) diff --git a/blender/arm/logicnode/material/LN_set_material_value_param.py b/blender/arm/logicnode/material/LN_set_material_value_param.py index cabc7b19..e3f2164c 100644 --- a/blender/arm/logicnode/material/LN_set_material_value_param.py +++ b/blender/arm/logicnode/material/LN_set_material_value_param.py @@ -1,16 +1,43 @@ from arm.logicnode.arm_nodes import * class SetMaterialValueParamNode(ArmLogicTreeNode): - """TO DO.""" + """Set a float value material parameter to the specified object. + + @seeNode Get Scene Root + + @input Object: Object whose material parameter should change. Use `Get Scene Root` node to set parameter globally. + + @input Per Object: + - `Enabled`: Set material parameter specific to this object. Global parameter will be ignored. + - `Disabled`: Set parameter globally, including this object. + + @input Material: Material whose parameter to be set. + + @input Node: Name of the parameter. + + @input Float: float value. + """ bl_idname = 'LNSetMaterialValueParamNode' bl_label = 'Set Material Value Param' arm_section = 'params' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Per Object') self.add_input('ArmDynamicSocket', 'Material') self.add_input('ArmStringSocket', 'Node') self.add_input('ArmFloatSocket', 'Float') self.add_output('ArmNodeSocketAction', 'Out') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNSetMaterialValueParamNode', self.arm_version, 'LNSetMaterialValueParamNode', 2, + in_socket_mapping={0:0, 1:3, 2:4, 3:5}, out_socket_mapping={0:0} + ) diff --git a/blender/arm/logicnode/navmesh/LN_go_to_location.py b/blender/arm/logicnode/navmesh/LN_go_to_location.py index 7285c2cf..e60e743f 100644 --- a/blender/arm/logicnode/navmesh/LN_go_to_location.py +++ b/blender/arm/logicnode/navmesh/LN_go_to_location.py @@ -9,7 +9,7 @@ class GoToLocationNode(ArmLogicTreeNode): def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') - self.add_input('ArmDynamicSocket', 'Location') + self.add_input('ArmVectorSocket', 'Location') self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/replacement.py b/blender/arm/logicnode/replacement.py index 05ab2426..b703d83b 100644 --- a/blender/arm/logicnode/replacement.py +++ b/blender/arm/logicnode/replacement.py @@ -249,7 +249,8 @@ def replace_all(): print(f"A node whose class doesn't exist was found in node tree \"{tree_name}\"", file=reportf) elif error_type == 'update failed': print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, " - f"because there is no (longer?) an update routine for this version of the node.", file=reportf) + f"because there is no (longer?) an update routine for this version of the node. Original exception:" + "\n" + tb + "\n", file=reportf) elif error_type == 'future version': print(f"A node of type {node_class} in tree \"{tree_name}\" seemingly comes from a future version of armory. " f"Please check whether your version of armory is up to date", file=reportf) diff --git a/blender/arm/logicnode/transform/LN_get_object_location.py b/blender/arm/logicnode/transform/LN_get_object_location.py index f2e3fb58..0f12aa5b 100644 --- a/blender/arm/logicnode/transform/LN_get_object_location.py +++ b/blender/arm/logicnode/transform/LN_get_object_location.py @@ -1,13 +1,26 @@ from arm.logicnode.arm_nodes import * class GetLocationNode(ArmLogicTreeNode): - """Returns the current location of the given object in world coordinates.""" + """Get the location of the given object in world coordinates. + + @input Parent Relative: If enabled, transforms the world coordinates into object parent local coordinates + + @seeNode Set Object Location + @seeNode World Vector to Local Space + @seeNode Vector to Object Orientation + """ bl_idname = 'LNGetLocationNode' bl_label = 'Get Object Location' arm_section = 'location' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('NodeSocketBool', 'Parent Relative') self.add_output('ArmVectorSocket', 'Location') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/transform/LN_set_object_location.py b/blender/arm/logicnode/transform/LN_set_object_location.py index 84473c80..5ee43596 100644 --- a/blender/arm/logicnode/transform/LN_set_object_location.py +++ b/blender/arm/logicnode/transform/LN_set_object_location.py @@ -1,15 +1,28 @@ from arm.logicnode.arm_nodes import * class SetLocationNode(ArmLogicTreeNode): - """Sets the location of the given object.""" + """Set the location of the given object in world coordinates. + + @input Parent Relative: If enabled, transforms the world coordinates into object parent local coordinates + + @seeNode Get Object Location + @seeNode World Vector to Local Space + @seeNode Vector to Object Orientation + """ bl_idname = 'LNSetLocationNode' bl_label = 'Set Object Location' arm_section = 'location' - arm_version = 1 + arm_version = 2 def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmVectorSocket', 'Location') + self.add_input('NodeSocketBool', 'Parent Relative') self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + return NodeReplacement.Identity(self) \ No newline at end of file diff --git a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py index e9f0ef6c..80e5794d 100644 --- a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py +++ b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py @@ -1,10 +1,9 @@ from arm.logicnode.arm_nodes import * class VectorToObjectOrientationNode(ArmLogicTreeNode): - """Converts the given world vector to a vector oriented by the given object. - The object scale is taken in count. + """Transform world coordinates into object oriented coordinates (in other words: apply object rotation to it). - @seeNode World Vector To Object Space + @seeNode World Vector to Object Space @seeNode Get World Orientation @seeNode Vector From Transform """ diff --git a/blender/arm/logicnode/transform/LN_world_vector_to_local_space.py b/blender/arm/logicnode/transform/LN_world_vector_to_local_space.py index c0e0d20a..5bc8442c 100644 --- a/blender/arm/logicnode/transform/LN_world_vector_to_local_space.py +++ b/blender/arm/logicnode/transform/LN_world_vector_to_local_space.py @@ -1,10 +1,9 @@ from arm.logicnode.arm_nodes import * class WorldVectorToLocalSpaceNode(ArmLogicTreeNode): - """Converts the given world vector to a object space vector. - The object scale is taken in count. + """Transform world coordinates into object local coordinates. - @seeNode Vector To Object Orientation + @seeNode Vector to Object Orientation @seeNode Get World Orientation @seeNode Vector From Transform """ diff --git a/blender/arm/make.py b/blender/arm/make.py index ae63286d..a4da2dbf 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -1,15 +1,16 @@ +import errno import glob import json import os +from queue import Queue +import shlex import shutil -import time import stat import subprocess import threading +import time +from typing import Callable import webbrowser -import shlex -import errno -import math import bpy @@ -29,15 +30,42 @@ import arm.write_data as write_data scripts_mtime = 0 # Monitor source changes profile_time = 0 -def run_proc(cmd, done): - def fn(p, done): - p.wait() - if done != None: +# Queue of threads and their done callbacks. Item format: [thread, done] +thread_callback_queue = Queue(maxsize=0) + + +def run_proc(cmd, done: Callable) -> subprocess.Popen: + """Creates a subprocess with the given command and returns it. + + If Blender is not running in background mode, a thread is spawned + that waits until the subprocess has finished executing to not freeze + the UI, otherwise (in background mode) execution is blocked until + the subprocess has finished. + + If `done` is not `None`, it is called afterwards in the main thread. + """ + use_thread = not bpy.app.background + + def wait_for_proc(proc: subprocess.Popen): + proc.wait() + + if use_thread: + # Put the done callback into the callback queue so that it + # can be received by a polling function in the main thread + thread_callback_queue.put([threading.current_thread(), done], block=True) + else: done() + p = subprocess.Popen(cmd) - threading.Thread(target=fn, args=(p, done)).start() + + if use_thread: + threading.Thread(target=wait_for_proc, args=(p,)).start() + else: + wait_for_proc(p) + return p + def compile_shader_pass(res, raw_shaders_path, shader_name, defs, make_variants): os.chdir(raw_shaders_path + '/' + shader_name) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 1366b66f..625c24c2 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -204,6 +204,8 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st 'BSDF_VELVET': nodes_shader.parse_bsdfvelvet, } + state.reset_outs() + if node.type in node_parser_funcs: node_parser_funcs[node.type](node, socket, state) @@ -558,7 +560,7 @@ def to_uniform(inp: bpy.types.NodeSocket): def store_var_name(node: bpy.types.Node): return node_name(node.name) + '_store' -def texture_store(node, tex, tex_name, to_linear=False, tex_link=None): +def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_arm_mat_param=None): curshader = state.curshader tex_store = store_var_name(node) @@ -567,7 +569,7 @@ def texture_store(node, tex, tex_name, to_linear=False, tex_link=None): state.parsed.add(tex_store) mat_bind_texture(tex) state.con.add_elem('tex', 'short2norm') - curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link) + curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link, default_value=default_value, is_arm_mat_param=is_arm_mat_param) triplanar = node.projection == 'BOX' if node.inputs[0].is_linked: uv_name = parse_vector_input(node.inputs[0]) diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py index 69072bcd..335e56f6 100644 --- a/blender/arm/material/cycles_nodes/nodes_input.py +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -92,7 +92,13 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.N def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if node.arm_material_param: nn = 'param_' + c.node_name(node.name) - state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}') + v = out_socket.default_value + value = [] + value.append(float(v[0])) + value.append(float(v[1])) + value.append(float(v[2])) + is_arm_mat_param = True + state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value = value, is_arm_mat_param = is_arm_mat_param) return nn else: return c.to_vec3(out_socket.default_value) @@ -351,7 +357,9 @@ def parse_lightpath(node: bpy.types.ShaderNodeLightPath, out_socket: bpy.types.N def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: if node.arm_material_param: nn = 'param_' + c.node_name(node.name) - state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name)) + value = c.to_vec1(node.outputs[0].default_value) + is_arm_mat_param = True + state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name), default_value=value, is_arm_mat_param=is_arm_mat_param) return nn else: return c.to_vec1(node.outputs[0].default_value) diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index 516d555b..9952a0cb 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -114,15 +114,22 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No tex_name = c.node_name(node.name) tex = c.make_texture(node, tex_name) - tex_link = node.name if node.arm_material_param else None + tex_link = None + tex_default_file = None + is_arm_mat_param = None + if node.arm_material_param: + tex_link = node.name + is_arm_mat_param = True + if tex['file'] is not None: + tex_default_file = tex['file'] if tex is not None: state.curshader.write_textures += 1 if use_color_out: to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB' - res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link)}.rgb' + res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.rgb' else: - res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link)}.a' + res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.a' state.curshader.write_textures -= 1 return res diff --git a/blender/arm/material/make_attrib.py b/blender/arm/material/make_attrib.py index 1d01e99b..bc23a620 100644 --- a/blender/arm/material/make_attrib.py +++ b/blender/arm/material/make_attrib.py @@ -1,9 +1,12 @@ +from typing import Optional + import arm.material.cycles as cycles import arm.material.mat_state as mat_state import arm.material.make_skin as make_skin import arm.material.make_particle as make_particle import arm.material.make_inst as make_inst -import arm.material.shader as shader +import arm.material.make_tess as make_tess +from arm.material.shader import Shader, ShaderContext import arm.utils @@ -33,17 +36,45 @@ def write_vertpos(vert): vert.write('gl_Position = WVP * spos;') -def write_norpos(con_mesh: shader.ShaderContext, vert: shader.Shader, declare=False, write_nor=True): - prep = '' - if declare: - prep = 'vec3 ' +def write_norpos(con_mesh: ShaderContext, vert: Shader, declare=False, write_nor=True): is_bone = con_mesh.is_elem('bone') if is_bone: make_skin.skin_pos(vert) if write_nor: + prep = 'vec3 ' if declare else '' if is_bone: make_skin.skin_nor(vert, prep) else: vert.write_attrib(prep + 'wnormal = normalize(N * vec3(nor.xy, pos.w));') if con_mesh.is_elem('ipos'): make_inst.inst_pos(con_mesh, vert) + + +def write_tex_coords(con_mesh: ShaderContext, vert: Shader, frag: Shader, tese: Optional[Shader]): + rpdat = arm.utils.get_rp() + + if con_mesh.is_elem('tex'): + vert.add_out('vec2 texCoord') + vert.add_uniform('float texUnpack', link='_texUnpack') + if mat_state.material.arm_tilesheet_flag: + if mat_state.material.arm_particle_flag and rpdat.arm_particles == 'On': + make_particle.write_tilesheet(vert) + else: + vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset') + vert.write_attrib('texCoord = tex * texUnpack + tilesheetOffset;') + else: + vert.write_attrib('texCoord = tex * texUnpack;') + + if tese is not None: + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord', 2, declare_out=frag.contains('texCoord')) + tese.write_pre = False + + if con_mesh.is_elem('tex1'): + vert.add_out('vec2 texCoord1') + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.write_attrib('texCoord1 = tex1 * texUnpack;') + if tese is not None: + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord1', 2, declare_out=frag.contains('texCoord1')) + tese.write_pre = False diff --git a/blender/arm/material/make_depth.py b/blender/arm/material/make_depth.py index 03ae858d..aa1f0864 100644 --- a/blender/arm/material/make_depth.py +++ b/blender/arm/material/make_depth.py @@ -33,8 +33,6 @@ def make(context_id, rpasses, shadowmap=False): parse_custom_particle = (cycles.node_by_name(mat_state.nodes, 'ArmCustomParticleNode') is not None) if parse_opacity: - frag.write('vec3 n;') # Discard at compile time - frag.write('float dotNV;') frag.write('float opacity;') if con_depth.is_elem('bone'): diff --git a/blender/arm/material/make_finalize.py b/blender/arm/material/make_finalize.py index 1eca9455..afeeee8f 100644 --- a/blender/arm/material/make_finalize.py +++ b/blender/arm/material/make_finalize.py @@ -1,7 +1,10 @@ import bpy -import arm.material.make_tess as make_tess -def make(con_mesh): +import arm.material.make_tess as make_tess +from arm.material.shader import ShaderContext + + +def make(con_mesh: ShaderContext): vert = con_mesh.vert frag = con_mesh.frag geom = con_mesh.geom @@ -13,9 +16,26 @@ def make(con_mesh): if frag.contains('dotNV') and not frag.contains('float dotNV'): frag.write_init('float dotNV = max(dot(n, vVec), 0.0);') + # n is not always defined yet (in some shadowmap shaders e.g.) + if not frag.contains('vec3 n'): + vert.add_out('vec3 wnormal') + vert.add_uniform('mat3 N', '_normalMatrix') + vert.write_attrib('wnormal = normalize(N * vec3(nor.xy, pos.w));') + frag.write_attrib('vec3 n = normalize(wnormal);') + + # If not yet added, add nor vertex data + vertex_elems = con_mesh.data['vertex_elements'] + has_normals = False + for elem in vertex_elems: + if elem['name'] == 'nor': + has_normals = True + break + if not has_normals: + vertex_elems.append({'name': 'nor', 'data': 'short2norm'}) + write_wpos = False if frag.contains('vVec') and not frag.contains('vec3 vVec'): - if tese != None: + if tese is not None: tese.add_out('vec3 eyeDir') tese.add_uniform('vec3 eye', '_cameraPosition') tese.write('eyeDir = eye - wposition;') @@ -31,7 +51,7 @@ def make(con_mesh): export_wpos = False if frag.contains('wposition') and not frag.contains('vec3 wposition'): export_wpos = True - if tese != None: + if tese is not None: export_wpos = True if vert.contains('wposition'): write_wpos = True @@ -50,7 +70,7 @@ def make(con_mesh): vert.add_uniform('float posUnpack', link='_posUnpack') vert.write_attrib('mposition = spos.xyz * posUnpack;') - if tese != None: + if tese is not None: if frag_mpos: make_tess.interpolate(tese, 'mposition', 3, declare_out=True) elif tese.contains('mposition') and not tese.contains('vec3 mposition'): @@ -72,7 +92,7 @@ def make(con_mesh): vert.write_attrib('if (dim.y == 0) bposition.y = 0;') vert.write_attrib('if (dim.x == 0) bposition.x = 0;') - if tese != None: + if tese is not None: if frag_bpos: make_tess.interpolate(tese, 'bposition', 3, declare_out=True) elif tese.contains('bposition') and not tese.contains('vec3 bposition'): @@ -93,7 +113,7 @@ def make(con_mesh): vert.write('wtangent = normalize(N * tang.xyz);') vert.write_pre = False - if tese != None: + if tese is not None: if frag_wtan: make_tess.interpolate(tese, 'wtangent', 3, declare_out=True) elif tese.contains('wtangent') and not tese.contains('vec3 wtangent'): diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 00932ef0..db9a2c28 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -132,31 +132,7 @@ def make_base(con_mesh, parse_opacity): if not is_displacement and not vattr_written: make_attrib.write_vertpos(vert) - if con_mesh.is_elem('tex'): - vert.add_out('vec2 texCoord') - vert.add_uniform('float texUnpack', link='_texUnpack') - if mat_state.material.arm_tilesheet_flag: - if mat_state.material.arm_particle_flag and rpdat.arm_particles == 'On': - make_particle.write_tilesheet(vert) - else: - vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset') - vert.write_attrib('texCoord = tex * texUnpack + tilesheetOffset;') - else: - vert.write_attrib('texCoord = tex * texUnpack;') - - if tese is not None: - tese.write_pre = True - make_tess.interpolate(tese, 'texCoord', 2, declare_out=frag.contains('texCoord')) - tese.write_pre = False - - if con_mesh.is_elem('tex1'): - vert.add_out('vec2 texCoord1') - vert.add_uniform('float texUnpack', link='_texUnpack') - vert.write_attrib('texCoord1 = tex1 * texUnpack;') - if tese is not None: - tese.write_pre = True - make_tess.interpolate(tese, 'texCoord1', 2, declare_out=frag.contains('texCoord1')) - tese.write_pre = False + make_attrib.write_tex_coords(con_mesh, vert, frag, tese) if con_mesh.is_elem('col'): vert.add_out('vec3 vcolor') @@ -296,8 +272,6 @@ def make_forward_mobile(con_mesh): vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') frag.ins = vert.outs - make_attrib.write_vertpos(vert) - frag.add_include('compiled.inc') frag.write('vec3 basecol;') frag.write('float roughness;') @@ -320,14 +294,7 @@ def make_forward_mobile(con_mesh): opac = mat_state.material.arm_discard_opacity frag.write('if (opacity < {0}) discard;'.format(opac)) - if con_mesh.is_elem('tex'): - vert.add_out('vec2 texCoord') - vert.add_uniform('float texUnpack', link='_texUnpack') - if mat_state.material.arm_tilesheet_flag: - vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset') - vert.write('texCoord = tex * texUnpack + tilesheetOffset;') - else: - vert.write('texCoord = tex * texUnpack;') + make_attrib.write_tex_coords(con_mesh, vert, frag, tese) if con_mesh.is_elem('col'): vert.add_out('vec3 vcolor') @@ -344,6 +311,8 @@ def make_forward_mobile(con_mesh): make_attrib.write_norpos(con_mesh, vert) frag.write_attrib('vec3 n = normalize(wnormal);') + make_attrib.write_vertpos(vert) + frag.add_include('std/math.glsl') frag.add_include('std/brdf.glsl') @@ -474,8 +443,6 @@ def make_forward_solid(con_mesh): vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') frag.ins = vert.outs - make_attrib.write_vertpos(vert) - frag.add_include('compiled.inc') frag.write('vec3 basecol;') frag.write('float roughness;') @@ -512,6 +479,7 @@ def make_forward_solid(con_mesh): vert.write('vcolor = col.rgb;') make_attrib.write_norpos(con_mesh, vert, write_nor=False) + make_attrib.write_vertpos(vert) frag.add_out('vec4 fragColor') if blend and parse_opacity: diff --git a/blender/arm/material/parser_state.py b/blender/arm/material/parser_state.py index bcbb59c8..8606e63f 100644 --- a/blender/arm/material/parser_state.py +++ b/blender/arm/material/parser_state.py @@ -64,6 +64,16 @@ class ParserState: self.out_opacity: floatstr = '1.0' self.out_emission: floatstr = '0.0' + def reset_outs(self): + """Reset the shader output values to their default values.""" + self.out_basecol = 'vec3(0.8)' + self.out_roughness = '0.0' + self.out_metallic = '0.0' + self.out_occlusion = '1.0' + self.out_specular = '1.0' + self.out_opacity = '1.0' + self.out_emission = '0.0' + def get_outs(self) -> Tuple[vec3str, floatstr, floatstr, floatstr, floatstr, floatstr, floatstr]: """Return the shader output values as a tuple.""" return (self.out_basecol, self.out_roughness, self.out_metallic, self.out_occlusion, self.out_specular, diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index be2af682..8e107628 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -109,19 +109,27 @@ class ShaderContext: def get(self): return self.data - def add_constant(self, ctype, name, link=None): + def add_constant(self, ctype, name, link=None, default_value=None, is_arm_mat_param=None): for c in self.constants: if c['name'] == name: return - c = { 'name': name, 'type': ctype } - if link != None: + c = { 'name': name, 'type': ctype} + if link is not None: c['link'] = link + if default_value is not None: + if ctype == 'float': + c['float'] = default_value + if ctype == 'vec3': + c['vec3'] = default_value + if is_arm_mat_param is not None: + c['is_arm_parameter'] = 'true' self.constants.append(c) def add_texture_unit(self, name, link=None, is_image=None, addr_u=None, addr_v=None, - filter_min=None, filter_mag=None, mipmap_filter=None): + filter_min=None, filter_mag=None, mipmap_filter=None, + default_value=None, is_arm_mat_param=None): for c in self.tunits: if c['name'] == name: return @@ -141,6 +149,10 @@ class ShaderContext: c['filter_mag'] = filter_mag if mipmap_filter is not None: c['mipmap_filter'] = mipmap_filter + if default_value is not None: + c['default_image_file'] = default_value + if is_arm_mat_param is not None: + c['is_arm_parameter'] = 'true' self.tunits.append(c) @@ -238,7 +250,7 @@ class Shader: def add_uniform(self, s, link=None, included=False, top=False, tex_addr_u=None, tex_addr_v=None, tex_filter_min=None, tex_filter_mag=None, - tex_mipmap_filter=None): + tex_mipmap_filter=None, default_value=None, is_arm_mat_param=None): ar = s.split(' ') # layout(RGBA8) image3D voxels utype = ar[-2] @@ -257,7 +269,8 @@ class Shader: self.context.add_texture_unit( uname, link, is_image, tex_addr_u, tex_addr_v, - tex_filter_min, tex_filter_mag, tex_mipmap_filter) + tex_filter_min, tex_filter_mag, tex_mipmap_filter, + default_value=default_value, is_arm_mat_param=is_arm_mat_param) else: # Prefer vec4[] for d3d to avoid padding if ar[0] == 'float' and '[' in ar[1]: @@ -266,7 +279,7 @@ class Shader: elif ar[0] == 'vec4' and '[' in ar[1]: ar[0] = 'floats' ar[1] = ar[1].split('[', 1)[0] - self.context.add_constant(ar[0], ar[1], link=link) + self.context.add_constant(ar[0], ar[1], link=link, default_value=default_value, is_arm_mat_param=is_arm_mat_param) if top: if not included and s not in self.uniforms_top: self.uniforms_top.append(s) diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 12f605fb..87b73869 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -363,6 +363,7 @@ class ReplaceNodesOperator(bpy.types.Operator): def register(): + arm.logicnode.arm_nodes.register() arm.logicnode.arm_sockets.register() bpy.utils.register_class(ArmLogicTree) @@ -403,3 +404,4 @@ def unregister(): bpy.utils.register_class(ARM_MT_NodeAddOverride.overridden_menu) arm.logicnode.arm_sockets.unregister() + arm.logicnode.arm_nodes.unregister() diff --git a/blender/arm/props.py b/blender/arm/props.py index 8c48ce9f..8f3d9af2 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -11,7 +11,7 @@ import arm.proxy import arm.utils # Armory version -arm_version = '2021.5' +arm_version = '2021.7' arm_commit = '$Id$' def get_project_html5_copy(self): @@ -286,6 +286,7 @@ def init_properties(): default=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False), size=20, subtype='LAYER') + bpy.types.Object.arm_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False) bpy.types.Object.arm_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True) bpy.types.Object.arm_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='') bpy.types.Object.arm_tilesheet_action = StringProperty(name="Tilesheet Action", description="Set startup action", default='') diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 880c7f9a..e368dcc2 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -211,8 +211,11 @@ class ARM_PT_PhysicsPropsPanel(bpy.types.Panel): layout.prop(obj, 'arm_rb_trigger') layout.prop(obj, 'arm_rb_ccd') - if obj.soft_body != None: + if obj.soft_body is not None: layout.prop(obj, 'arm_soft_body_margin') + + if obj.rigid_body_constraint is not None: + layout.prop(obj, 'arm_relative_physics_constraint') # Menu in data region class ARM_PT_DataPropsPanel(bpy.types.Panel): @@ -626,7 +629,6 @@ class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): row = layout.row(align=True) row.alignment = 'EXPAND' row.scale_y = 1.3 - row.enabled = wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 row.operator("arm.build_project", icon="MOD_BUILD") # row.operator("arm.patch_project") row.operator("arm.publish_project", icon="EXPORT") @@ -1053,10 +1055,15 @@ class ArmoryStopButton(bpy.types.Operator): return{'FINISHED'} class ArmoryBuildProjectButton(bpy.types.Operator): - '''Build and compile project''' + """Build and compile project""" bl_idname = 'arm.build_project' bl_label = 'Build' + @classmethod + def poll(cls, context): + wrd = bpy.data.worlds['Arm'] + return wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 + def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): @@ -1093,10 +1100,15 @@ class ArmoryBuildProjectButton(bpy.types.Operator): return{'FINISHED'} class ArmoryPublishProjectButton(bpy.types.Operator): - '''Build project ready for publishing''' + """Build project ready for publishing.""" bl_idname = 'arm.publish_project' bl_label = 'Publish' + @classmethod + def poll(cls, context): + wrd = bpy.data.worlds['Arm'] + return wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 + def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 2003a905..52d836a0 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -300,7 +300,7 @@ script_warnings: Dict[str, List[Tuple[str, str]]] = {} # Script name -> List of # See https://regex101.com/r/bbrCzN/8 RX_MODIFIERS = r'(?P(?:public\s+|private\s+|static\s+|inline\s+|final\s+)*)?' # Optional modifiers RX_IDENTIFIER = r'(?P[_$a-z]+[_a-z0-9]*)' # Variable name, follow Haxe rules -RX_TYPE = r'(?::\s+(?P[_a-z]+[\._a-z0-9]*))?' # Optional type annotation +RX_TYPE = r'(?:\s*:\s*(?P[_a-z]+[\._a-z0-9]*))?' # Optional type annotation RX_VALUE = r'(?:\s*=\s*(?P(?:\".*\")|(?:[^;]+)|))?' # Optional default value PROP_REGEX_RAW = fr'@prop\s+{RX_MODIFIERS}(?Pvar|final)\s+{RX_IDENTIFIER}{RX_TYPE}{RX_VALUE};'