From f6e6003428e53cb6dbf269678dd7861255e03d01 Mon Sep 17 00:00:00 2001 From: e2002e Date: Tue, 16 May 2023 20:45:37 +0200 Subject: [PATCH 001/175] history cleanup --- .gitattributes | 2 + .github/FUNDING.yml | 4 + .github/ISSUE_TEMPLATE/bug_report.md | 27 + .github/ISSUE_TEMPLATE/feature_request.md | 10 + .github/workflows/krom.yml | 22 + .gitignore | 2 + .vscode/settings.json | 8 + Assets/blue_noise64.png | Bin 0 -> 16419 bytes Assets/brdf.png | Bin 0 -> 20162 bytes Assets/clouds_base.raw | 1 + Assets/clouds_detail.raw | 1 + Assets/clouds_map.png | Bin 0 -> 975591 bytes Assets/font_default.ttf | Bin 0 -> 22544 bytes Assets/font_default_full.ttf | Bin 0 -> 171676 bytes Assets/font_license_roboto.txt | 202 + Assets/font_mono.ttf | Bin 0 -> 19792 bytes Assets/font_mono_full.ttf | Bin 0 -> 114624 bytes Assets/hosek/hosek_radiance.hdr | Bin 0 -> 37680 bytes Assets/hosek/hosek_radiance_0.hdr | 7 + Assets/hosek/hosek_radiance_1.hdr | 7 + Assets/hosek/hosek_radiance_2.hdr | 7 + Assets/hosek/hosek_radiance_3.hdr | Bin 0 -> 486 bytes Assets/hosek/hosek_radiance_4.hdr | Bin 0 -> 241 bytes Assets/hosek/hosek_radiance_5.hdr | 7 + Assets/hosek/hosek_radiance_6.hdr | 7 + Assets/hosek/hosek_radiance_7.hdr | 7 + Assets/ies/JellyFish.ies | 80 + Assets/ies/load_ies.py | 173 + Assets/noise256.png | Bin 0 -> 67406 bytes Assets/noise64.png | Bin 0 -> 5766 bytes Assets/noise8.png | Bin 0 -> 1167 bytes Assets/smaa_area.png | Bin 0 -> 22194 bytes Assets/smaa_search.png | Bin 0 -> 313 bytes Assets/water_base.png | Bin 0 -> 420235 bytes Assets/water_detail.png | Bin 0 -> 472331 bytes Assets/water_foam.png | Bin 0 -> 62316 bytes LICENSE.md | 7 + README.md | 8 + Shaders/blend_pass/blend_pass.json | 17 + Shaders/bloom_pass/bloom_pass.json | 62 + Shaders/bloom_pass/downsample_pass.frag.glsl | 79 + Shaders/bloom_pass/upsample_pass.frag.glsl | 39 + .../blur_adaptive_pass.frag.glsl | 32 + .../blur_adaptive_pass.json | 84 + .../blur_bilat_blend_pass.frag.glsl | 50 + .../blur_bilat_blend_pass.json | 29 + .../blur_bilat_pass/blur_bilat_pass.frag.glsl | 49 + Shaders/blur_bilat_pass/blur_bilat_pass.json | 67 + .../blur_edge_pass/blur_edge_pass.frag.glsl | 44 + Shaders/blur_edge_pass/blur_edge_pass.json | 74 + Shaders/blur_pass/blur_pass.frag.glsl | 23 + Shaders/blur_pass/blur_pass.json | 66 + .../chromatic_aberration_pass.frag.glsl | 78 + .../chromatic_aberration_pass.json | 21 + .../clear_color_depth_pass.frag.glsl | 9 + .../clear_color_depth_pass.json | 15 + .../clear_color_pass.frag.glsl | 8 + .../clear_color_pass/clear_color_pass.json | 15 + .../clear_depth_pass.frag.glsl | 8 + .../clear_depth_pass/clear_depth_pass.json | 19 + .../compositor_pass/compositor_pass.frag.glsl | 617 +++ Shaders/compositor_pass/compositor_pass.json | 245 ++ .../compositor_pass/compositor_pass.vert.glsl | 34 + .../copy_mrt2_pass/copy_mrt2_pass.frag.glsl | 12 + Shaders/copy_mrt2_pass/copy_mrt2_pass.json | 14 + .../copy_mrt3_pass/copy_mrt3_pass.frag.glsl | 14 + Shaders/copy_mrt3_pass/copy_mrt3_pass.json | 14 + Shaders/copy_pass/copy_pass.json | 14 + .../custom_mat_deferred.frag.glsl | 55 + .../custom_mat_deferred.vert.glsl | 20 + .../custom_mat_forward.frag.glsl | 9 + .../custom_mat_forward.vert.glsl | 11 + Shaders/debug_draw/line.frag.glsl | 8 + Shaders/debug_draw/line.vert.glsl | 12 + Shaders/debug_draw/line_deferred.frag.glsl | 15 + .../deferred_light/deferred_light.frag.glsl | 519 +++ Shaders/deferred_light/deferred_light.json | 246 ++ .../deferred_light.frag.glsl | 283 ++ .../deferred_light_mobile.json | 190 + .../deferred_light.frag.glsl | 13 + .../deferred_light_solid.json | 14 + .../downsample_depth.frag.glsl | 18 + .../downsample_depth/downsample_depth.json | 19 + Shaders/fxaa_pass/fxaa_pass.frag.glsl | 58 + Shaders/fxaa_pass/fxaa_pass.json | 19 + .../histogram_pass/histogram_pass.frag.glsl | 18 + Shaders/histogram_pass/histogram_pass.json | 17 + Shaders/include/pass.vert.glsl | 18 + Shaders/include/pass_copy.frag.glsl | 10 + Shaders/include/pass_viewray.vert.glsl | 31 + Shaders/include/pass_viewray2.vert.glsl | 26 + Shaders/include/pass_volume.vert.glsl | 12 + .../motion_blur_pass.frag.glsl | 54 + .../motion_blur_pass/motion_blur_pass.json | 39 + .../motion_blur_veloc_pass.frag.glsl | 33 + .../motion_blur_veloc_pass.json | 24 + Shaders/probe_cubemap/probe_cubemap.frag.glsl | 54 + Shaders/probe_cubemap/probe_cubemap.json | 36 + Shaders/probe_planar/probe_planar.frag.glsl | 54 + Shaders/probe_planar/probe_planar.json | 36 + .../smaa_blend_weight.frag.glsl | 457 ++ .../smaa_blend_weight/smaa_blend_weight.json | 31 + .../smaa_blend_weight.vert.glsl | 36 + .../smaa_edge_detect.frag.glsl | 207 + .../smaa_edge_detect/smaa_edge_detect.json | 19 + .../smaa_edge_detect.vert.glsl | 33 + .../smaa_neighborhood_blend.frag.glsl | 92 + .../smaa_neighborhood_blend.json | 19 + .../smaa_neighborhood_blend.vert.glsl | 29 + Shaders/ssao_pass/ssao_pass.frag.glsl | 64 + Shaders/ssao_pass/ssao_pass.json | 40 + Shaders/ssao_pass/ssgi_pass_.frag.glsl | 61 + Shaders/ssao_pass/ssgi_pass_.json | 35 + .../ssgi_blur_pass/ssgi_blur_pass.frag.glsl | 46 + Shaders/ssgi_blur_pass/ssgi_blur_pass.json | 74 + Shaders/ssgi_pass/ssgi_pass.frag.glsl | 107 + Shaders/ssgi_pass/ssgi_pass.json | 31 + Shaders/ssr_pass/ssr_pass.frag.glsl | 126 + Shaders/ssr_pass/ssr_pass.json | 41 + Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 132 + Shaders/ssrefr_pass/ssrefr_pass.json | 48 + Shaders/sss_pass/sss_pass.frag.glsl | 118 + Shaders/sss_pass/sss_pass.json | 42 + Shaders/std/brdf.glsl | 139 + Shaders/std/clusters.glsl | 18 + Shaders/std/colorgrading.glsl | 144 + Shaders/std/conetrace.glsl | 122 + Shaders/std/denoise.glsl | 56 + Shaders/std/dof.glsl | 92 + Shaders/std/filters.glsl | 60 + Shaders/std/gbuffer.glsl | 153 + Shaders/std/ies.glsl | 32 + Shaders/std/light.glsl | 243 ++ Shaders/std/light_common.glsl | 31 + Shaders/std/light_mobile.glsl | 135 + Shaders/std/ltc.glsl | 157 + Shaders/std/mapping.glsl | 41 + Shaders/std/math.glsl | 48 + Shaders/std/morph_target.glsl | 53 + Shaders/std/normals.glsl | 31 + Shaders/std/resample.glsl | 228 + Shaders/std/shadows.glsl | 371 ++ Shaders/std/shirr.glsl | 30 + Shaders/std/skinning.glsl | 28 + Shaders/std/sky.glsl | 155 + Shaders/std/ssrs.glsl | 36 + Shaders/std/sss.glsl | 26 + Shaders/std/tonemap.glsl | 38 + Shaders/std/vr.glsl | 26 + .../supersample_resolve.frag.glsl | 14 + .../supersample_resolve.json | 19 + Shaders/taa_pass/taa_pass.frag.glsl | 44 + Shaders/taa_pass/taa_pass.json | 14 + .../translucent_resolve.frag.glsl | 28 + .../translucent_resolve.json | 24 + .../volumetric_light.frag.glsl | 166 + .../volumetric_light/volumetric_light.json | 150 + Shaders/water_pass/water_pass.frag.glsl | 200 + Shaders/water_pass/water_pass.json | 76 + Sources/armory/data/Config.hx | 53 + Sources/armory/data/ConstData.hx | 23 + Sources/armory/import.hx | 1 + Sources/armory/logicnode/ActiveCameraNode.hx | 10 + Sources/armory/logicnode/ActiveSceneNode.hx | 10 + Sources/armory/logicnode/AddGroupNode.hx | 33 + .../armory/logicnode/AddObjectToGroupNode.hx | 20 + .../logicnode/AddPhysicsConstraintNode.hx | 114 + Sources/armory/logicnode/AddRigidBodyNode.hx | 100 + Sources/armory/logicnode/AddTraitNode.hx | 28 + Sources/armory/logicnode/AlternateNode.hx | 18 + Sources/armory/logicnode/AnimActionNode.hx | 21 + .../armory/logicnode/AnimationStateNode.hx | 27 + .../armory/logicnode/AppendTransformNode.hx | 30 + .../logicnode/ApplyForceAtLocationNode.hx | 38 + Sources/armory/logicnode/ApplyForceNode.hx | 31 + .../logicnode/ApplyImpulseAtLocationNode.hx | 37 + Sources/armory/logicnode/ApplyImpulseNode.hx | 31 + .../logicnode/ApplyTorqueImpulseNode.hx | 31 + Sources/armory/logicnode/ApplyTorqueNode.hx | 31 + Sources/armory/logicnode/ArrayAddNode.hx | 47 + Sources/armory/logicnode/ArrayBooleanNode.hx | 27 + Sources/armory/logicnode/ArrayColorNode.hx | 29 + Sources/armory/logicnode/ArrayCompareNode.hx | 16 + Sources/armory/logicnode/ArrayConcatNode.hx | 18 + Sources/armory/logicnode/ArrayCountNode.hx | 33 + Sources/armory/logicnode/ArrayDisplayNode.hx | 31 + Sources/armory/logicnode/ArrayDistinctNode.hx | 27 + Sources/armory/logicnode/ArrayFilterNode.hx | 137 + Sources/armory/logicnode/ArrayFloatNode.hx | 27 + Sources/armory/logicnode/ArrayGetNextNode.hx | 26 + Sources/armory/logicnode/ArrayGetNode.hx | 29 + .../logicnode/ArrayGetPreviousNextNode.hx | 33 + Sources/armory/logicnode/ArrayInArrayNode.hx | 16 + Sources/armory/logicnode/ArrayIndexNode.hx | 16 + Sources/armory/logicnode/ArrayIntegerNode.hx | 27 + Sources/armory/logicnode/ArrayLengthNode.hx | 13 + Sources/armory/logicnode/ArrayLoopNode.hx | 40 + Sources/armory/logicnode/ArrayNode.hx | 27 + Sources/armory/logicnode/ArrayObjectNode.hx | 29 + Sources/armory/logicnode/ArrayPopNode.hx | 15 + Sources/armory/logicnode/ArrayRemoveNode.hx | 27 + .../armory/logicnode/ArrayRemoveValueNode.hx | 26 + Sources/armory/logicnode/ArrayResizeNode.hx | 19 + Sources/armory/logicnode/ArrayReverseNode.hx | 19 + Sources/armory/logicnode/ArraySampleNode.hx | 23 + Sources/armory/logicnode/ArraySetNode.hx | 21 + Sources/armory/logicnode/ArrayShiftNode.hx | 15 + Sources/armory/logicnode/ArrayShuffleNode.hx | 27 + Sources/armory/logicnode/ArraySliceNode.hx | 27 + Sources/armory/logicnode/ArraySortNode.hx | 22 + Sources/armory/logicnode/ArraySpliceNode.hx | 20 + Sources/armory/logicnode/ArrayStringNode.hx | 27 + Sources/armory/logicnode/ArrayVectorNode.hx | 29 + Sources/armory/logicnode/BitwiseMathNode.hx | 33 + Sources/armory/logicnode/BlendActionNode.hx | 25 + Sources/armory/logicnode/BloomGetNode.hx | 18 + Sources/armory/logicnode/BloomSetNode.hx | 21 + Sources/armory/logicnode/BoneFKNode.hx | 64 + Sources/armory/logicnode/BoneIKNode.hx | 62 + Sources/armory/logicnode/BooleanNode.hx | 21 + Sources/armory/logicnode/BranchNode.hx | 13 + Sources/armory/logicnode/CallFunctionNode.hx | 35 + Sources/armory/logicnode/CallGroupNode.hx | 9 + .../armory/logicnode/CallHaxeStaticNode.hx | 34 + Sources/armory/logicnode/CameraGetNode.hx | 28 + Sources/armory/logicnode/CameraSetNode.hx | 27 + .../armory/logicnode/CanvasGetCheckboxNode.hx | 28 + .../logicnode/CanvasGetInputTextNode.hx | 28 + .../armory/logicnode/CanvasGetLocationNode.hx | 36 + Sources/armory/logicnode/CanvasGetPBNode.hx | 36 + .../armory/logicnode/CanvasGetPositionNode.hx | 28 + .../armory/logicnode/CanvasGetRotationNode.hx | 33 + .../armory/logicnode/CanvasGetScaleNode.hx | 36 + .../armory/logicnode/CanvasGetSliderNode.hx | 28 + Sources/armory/logicnode/CanvasGetTextNode.hx | 28 + .../armory/logicnode/CanvasGetVisibleNode.hx | 30 + .../armory/logicnode/CanvasSetAssetNode.hx | 25 + .../armory/logicnode/CanvasSetCheckBoxNode.hx | 42 + .../armory/logicnode/CanvasSetColorNode.hx | 39 + .../logicnode/CanvasSetInputTextFocusNode.hx | 43 + .../logicnode/CanvasSetInputTextNode.hx | 25 + .../armory/logicnode/CanvasSetLocationNode.hx | 29 + Sources/armory/logicnode/CanvasSetPBNode.hx | 29 + .../CanvasSetProgressBarColorNode.hx | 28 + .../armory/logicnode/CanvasSetRotationNode.hx | 25 + .../armory/logicnode/CanvasSetScaleNode.hx | 29 + .../armory/logicnode/CanvasSetSliderNode.hx | 42 + .../logicnode/CanvasSetTextColorNode.hx | 30 + Sources/armory/logicnode/CanvasSetTextNode.hx | 25 + .../armory/logicnode/CanvasSetVisibleNode.hx | 25 + Sources/armory/logicnode/CaseIndexNode.hx | 21 + Sources/armory/logicnode/CaseStringNode.hx | 24 + .../armory/logicnode/CastPhysicsRayNode.hx | 45 + .../armory/logicnode/CastPhysicsRayOnNode.hx | 56 + .../logicnode/ChromaticAberrationGetNode.hx | 16 + .../logicnode/ChromaticAberrationSetNode.hx | 16 + Sources/armory/logicnode/ClampNode.hx | 18 + Sources/armory/logicnode/ClearConsoleNode.hx | 19 + Sources/armory/logicnode/ClearMapNode.hx | 19 + Sources/armory/logicnode/ClearParentNode.hx | 27 + Sources/armory/logicnode/ColorNode.hx | 24 + .../logicnode/ColorgradingGetGlobalNode.hx | 21 + .../logicnode/ColorgradingGetHighlightNode.hx | 22 + .../logicnode/ColorgradingGetMidtoneNode.hx | 21 + .../logicnode/ColorgradingGetShadowNode.hx | 21 + .../logicnode/ColorgradingSetGlobalNode.hx | 65 + .../logicnode/ColorgradingSetHighlightNode.hx | 71 + .../logicnode/ColorgradingSetMidtoneNode.hx | 67 + .../logicnode/ColorgradingSetShadowNode.hx | 71 + .../logicnode/ColorgradingShadowNode.hx | 71 + .../armory/logicnode/CombineColorHSVNode.hx | 36 + Sources/armory/logicnode/CombineColorNode.hx | 24 + Sources/armory/logicnode/CompareNode.hx | 57 + .../armory/logicnode/ConcatenateStringNode.hx | 17 + .../armory/logicnode/ContainsStringNode.hx | 27 + Sources/armory/logicnode/CreateMapNode.hx | 114 + .../logicnode/CreateRenderTargetNode.hx | 39 + .../armory/logicnode/CursorInRegionNode.hx | 115 + Sources/armory/logicnode/DefaultIfNullNode.hx | 16 + Sources/armory/logicnode/DegToRadNode.hx | 13 + .../logicnode/DetectMobileBrowserNode.hx | 23 + Sources/armory/logicnode/DisplayInfoNode.hx | 15 + Sources/armory/logicnode/DrawArcNode.hx | 36 + Sources/armory/logicnode/DrawCameraNode.hx | 104 + .../armory/logicnode/DrawCameraTextureNode.hx | 56 + Sources/armory/logicnode/DrawCircleNode.hx | 34 + Sources/armory/logicnode/DrawCurveNode.hx | 28 + Sources/armory/logicnode/DrawEllipseNode.hx | 47 + Sources/armory/logicnode/DrawImageNode.hx | 53 + .../armory/logicnode/DrawImageSequenceNode.hx | 106 + Sources/armory/logicnode/DrawLineNode.hx | 22 + Sources/armory/logicnode/DrawPolygonNode.hx | 49 + Sources/armory/logicnode/DrawRectNode.hx | 42 + Sources/armory/logicnode/DrawStringNode.hx | 55 + .../logicnode/DrawTextAreaStringNode.hx | 130 + .../logicnode/DrawToMaterialImageNode.hx | 43 + Sources/armory/logicnode/DrawTriangleNode.hx | 36 + Sources/armory/logicnode/DynamicNode.hx | 19 + Sources/armory/logicnode/ExpressionNode.hx | 28 + .../logicnode/FloatDeltaInterpolateNode.hx | 24 + Sources/armory/logicnode/FloatNode.hx | 21 + Sources/armory/logicnode/FunctionNode.hx | 22 + .../armory/logicnode/FunctionOutputNode.hx | 16 + Sources/armory/logicnode/GamepadCoordsNode.hx | 44 + Sources/armory/logicnode/GamepadSticksNode.hx | 130 + Sources/armory/logicnode/GateNode.hx | 56 + Sources/armory/logicnode/GetAgentDataNode.hx | 25 + .../armory/logicnode/GetBoneFkIkOnlyNode.hx | 33 + .../armory/logicnode/GetBoneTransformNode.hx | 33 + .../armory/logicnode/GetCameraAspectNode.hx | 18 + Sources/armory/logicnode/GetCameraFovNode.hx | 18 + .../armory/logicnode/GetCameraScaleNode.hx | 18 + .../armory/logicnode/GetCameraStartEndNode.hx | 18 + Sources/armory/logicnode/GetCameraTypeNode.hx | 18 + Sources/armory/logicnode/GetChildNode.hx | 53 + Sources/armory/logicnode/GetChildrenNode.hx | 18 + Sources/armory/logicnode/GetContactsNode.hx | 28 + .../armory/logicnode/GetCursorLocationNode.hx | 20 + .../armory/logicnode/GetCursorStateNode.hx | 25 + Sources/armory/logicnode/GetDateTimeNode.hx | 67 + .../logicnode/GetDebugConsoleSettings.hx | 26 + Sources/armory/logicnode/GetDimensionNode.hx | 18 + Sources/armory/logicnode/GetDistanceNode.hx | 19 + Sources/armory/logicnode/GetFPSNode.hx | 19 + .../armory/logicnode/GetFirstContactNode.hx | 25 + .../armory/logicnode/GetGamepadStartedNode.hx | 33 + .../logicnode/GetGlobalCanvasFontSizeNode.hx | 22 + .../logicnode/GetGlobalCanvasScaleNode.hx | 22 + Sources/armory/logicnode/GetGravityNode.hx | 19 + Sources/armory/logicnode/GetGroupNode.hx | 13 + .../armory/logicnode/GetHaxePropertyNode.hx | 17 + .../armory/logicnode/GetInputMapKeyNode.hx | 24 + .../logicnode/GetKeyboardStartedNode.hx | 32 + Sources/armory/logicnode/GetLocationNode.hx | 31 + Sources/armory/logicnode/GetMapValueNode.hx | 17 + Sources/armory/logicnode/GetMaterialNode.hx | 36 + Sources/armory/logicnode/GetMeshNode.hx | 18 + Sources/armory/logicnode/GetMouseLockNode.hx | 16 + .../armory/logicnode/GetMouseMovementNode.hx | 28 + .../armory/logicnode/GetMouseStartedNode.hx | 32 + .../armory/logicnode/GetMouseVisibleNode.hx | 18 + Sources/armory/logicnode/GetNameNode.hx | 18 + .../armory/logicnode/GetObjectByUidNode.hx | 22 + .../armory/logicnode/GetObjectGroupNode.hx | 28 + Sources/armory/logicnode/GetObjectNode.hx | 20 + .../logicnode/GetObjectOffscreenNode.hx | 24 + .../armory/logicnode/GetObjectTraitsNode.hx | 16 + Sources/armory/logicnode/GetParentNode.hx | 18 + .../armory/logicnode/GetPointVelocityNode.hx | 29 + Sources/armory/logicnode/GetPropertyNode.hx | 23 + .../armory/logicnode/GetRigidBodyDataNode.hx | 51 + Sources/armory/logicnode/GetRotationNode.hx | 34 + Sources/armory/logicnode/GetScaleNode.hx | 18 + Sources/armory/logicnode/GetSystemLanguage.hx | 13 + Sources/armory/logicnode/GetSystemName.hx | 26 + .../armory/logicnode/GetTilesheetStateNode.hx | 25 + .../armory/logicnode/GetTouchLocationNode.hx | 24 + .../armory/logicnode/GetTouchMovementNode.hx | 26 + Sources/armory/logicnode/GetTraitNameNode.hx | 44 + Sources/armory/logicnode/GetTraitNode.hx | 23 + .../armory/logicnode/GetTraitPausedNode.hx | 16 + Sources/armory/logicnode/GetTransformNode.hx | 18 + Sources/armory/logicnode/GetUidNode.hx | 18 + Sources/armory/logicnode/GetVelocityNode.hx | 36 + Sources/armory/logicnode/GetVisibleNode.hx | 24 + Sources/armory/logicnode/GetWorldNode.hx | 26 + Sources/armory/logicnode/GlobalObjectNode.hx | 12 + Sources/armory/logicnode/GoToLocationNode.hx | 82 + Sources/armory/logicnode/GroupInputsNode.hx | 16 + Sources/armory/logicnode/GroupNode.hx | 16 + Sources/armory/logicnode/GroupOutputsNode.hx | 16 + .../armory/logicnode/HasContactArrayNode.hx | 35 + Sources/armory/logicnode/HasContactNode.hx | 26 + .../armory/logicnode/IntFromBooleanNode.hx | 16 + Sources/armory/logicnode/IntegerNode.hx | 21 + Sources/armory/logicnode/InverseNode.hx | 20 + Sources/armory/logicnode/IsFalseNode.hx | 13 + Sources/armory/logicnode/IsNoneNode.hx | 13 + Sources/armory/logicnode/IsNotNoneNode.hx | 13 + .../armory/logicnode/IsRigidBodyActiveNode.hx | 22 + Sources/armory/logicnode/IsTrueNode.hx | 13 + .../armory/logicnode/KeyInterpolateNode.hx | 32 + Sources/armory/logicnode/LengthStringNode.hx | 15 + .../armory/logicnode/LenstextureGetNode.hx | 19 + .../armory/logicnode/LenstextureSetNode.hx | 19 + Sources/armory/logicnode/LetterboxGetNode.hx | 16 + Sources/armory/logicnode/LetterboxSetNode.hx | 17 + Sources/armory/logicnode/LoadUrlNode.hx | 16 + Sources/armory/logicnode/LogicNode.hx | 187 + Sources/armory/logicnode/LogicTree.hx | 47 + Sources/armory/logicnode/LookAtNode.hx | 42 + Sources/armory/logicnode/LoopBreakNode.hx | 12 + Sources/armory/logicnode/LoopContinueNode.hx | 12 + Sources/armory/logicnode/LoopNode.hx | 35 + Sources/armory/logicnode/MapKeyExistsNode.hx | 25 + Sources/armory/logicnode/MapLoopNode.hx | 41 + Sources/armory/logicnode/MapRangeNode.hx | 22 + Sources/armory/logicnode/MaskNode.hx | 23 + Sources/armory/logicnode/MaterialNode.hx | 31 + .../armory/logicnode/MathExpressionNode.hx | 1914 ++++++++ Sources/armory/logicnode/MathNode.hx | 107 + Sources/armory/logicnode/MatrixMathNode.hx | 28 + Sources/armory/logicnode/MergeNode.hx | 32 + Sources/armory/logicnode/MergedGamepadNode.hx | 51 + .../armory/logicnode/MergedKeyboardNode.hx | 40 + Sources/armory/logicnode/MergedMouseNode.hx | 44 + Sources/armory/logicnode/MergedSurfaceNode.hx | 43 + .../logicnode/MergedVirtualButtonNode.hx | 42 + Sources/armory/logicnode/MeshNode.hx | 29 + Sources/armory/logicnode/MixNode.hx | 58 + Sources/armory/logicnode/MouseCoordsNode.hx | 29 + .../armory/logicnode/NavigableLocationNode.hx | 25 + Sources/armory/logicnode/NetworkClientNode.hx | 34 + .../logicnode/NetworkCloseConnectionNode.hx | 74 + Sources/armory/logicnode/NetworkEventNode.hx | 88 + .../logicnode/NetworkHostCloseClientNode.hx | 41 + .../armory/logicnode/NetworkHostGetIpNode.hx | 49 + Sources/armory/logicnode/NetworkHostNode.hx | 70 + .../logicnode/NetworkHttpRequestNode.hx | 94 + .../logicnode/NetworkMessageParserNode.hx | 100 + .../logicnode/NetworkOpenConnectionNode.hx | 137 + .../logicnode/NetworkSendMessageNode.hx | 802 ++++ Sources/armory/logicnode/NoneNode.hx | 12 + Sources/armory/logicnode/NotNode.hx | 15 + Sources/armory/logicnode/NullNode.hx | 10 + Sources/armory/logicnode/ObjectNode.hx | 28 + .../armory/logicnode/OnActionMarkerNode.hx | 23 + .../logicnode/OnApplicationStateNode.hx | 20 + .../armory/logicnode/OnCanvasElementNode.hx | 116 + .../armory/logicnode/OnContactArrayNode.hx | 58 + Sources/armory/logicnode/OnContactNode.hx | 57 + Sources/armory/logicnode/OnEventNode.hx | 55 + Sources/armory/logicnode/OnGamepadNode.hx | 34 + Sources/armory/logicnode/OnInitNode.hx | 28 + Sources/armory/logicnode/OnInputMapNode.hx | 35 + Sources/armory/logicnode/OnKeyboardNode.hx | 28 + Sources/armory/logicnode/OnMouseNode.hx | 30 + Sources/armory/logicnode/OnRender2DNode.hx | 18 + Sources/armory/logicnode/OnSurfaceNode.hx | 29 + Sources/armory/logicnode/OnSwipeNode.hx | 201 + Sources/armory/logicnode/OnTapScreen.hx | 108 + Sources/armory/logicnode/OnTimerNode.hx | 27 + Sources/armory/logicnode/OnUpdateNode.hx | 27 + .../armory/logicnode/OnVirtualButtonNode.hx | 29 + .../armory/logicnode/OnVolumeTriggerNode.hx | 52 + Sources/armory/logicnode/OncePerFrameNode.hx | 22 + Sources/armory/logicnode/ParseFloatNode.hx | 15 + Sources/armory/logicnode/ParseIntNode.hx | 15 + Sources/armory/logicnode/PauseActionNode.hx | 22 + .../logicnode/PauseActiveCameraRenderNode.hx | 17 + Sources/armory/logicnode/PauseSoundNode.hx | 17 + .../armory/logicnode/PauseTilesheetNode.hx | 20 + Sources/armory/logicnode/PauseTraitNode.hx | 17 + .../armory/logicnode/PhysicsConstraintNode.hx | 41 + .../armory/logicnode/PhysicsConvexCastNode.hx | 50 + .../logicnode/PhysicsConvexCastOnNode.hx | 68 + Sources/armory/logicnode/PickLocationNode.hx | 33 + Sources/armory/logicnode/PickObjectNode.hx | 36 + .../armory/logicnode/PlayActionFromNode.hx | 136 + Sources/armory/logicnode/PlayActionNode.hx | 26 + Sources/armory/logicnode/PlaySoundNode.hx | 17 + Sources/armory/logicnode/PlaySoundRawNode.hx | 82 + Sources/armory/logicnode/PlayTilesheetNode.hx | 23 + Sources/armory/logicnode/PrintNode.hx | 20 + Sources/armory/logicnode/PulseNode.hx | 42 + .../armory/logicnode/QuaternionMathNode.hx | 137 + Sources/armory/logicnode/QuaternionNode.hx | 47 + Sources/armory/logicnode/RadToDegNode.hx | 13 + Sources/armory/logicnode/RandomBooleanNode.hx | 12 + Sources/armory/logicnode/RandomChoiceNode.hx | 14 + Sources/armory/logicnode/RandomColorNode.hx | 20 + Sources/armory/logicnode/RandomFloatNode.hx | 14 + Sources/armory/logicnode/RandomIntegerNode.hx | 14 + Sources/armory/logicnode/RandomOutputNode.hx | 12 + Sources/armory/logicnode/RandomStringNode.hx | 23 + Sources/armory/logicnode/RandomVectorNode.hx | 22 + .../logicnode/RaycastClosestObjectNode.hx | 39 + Sources/armory/logicnode/RaycastObjectNode.hx | 32 + Sources/armory/logicnode/ReadFileNode.hx | 30 + Sources/armory/logicnode/ReadJsonNode.hx | 30 + Sources/armory/logicnode/ReadStorageNode.hx | 47 + .../armory/logicnode/RemoveActiveSceneNode.hx | 13 + Sources/armory/logicnode/RemoveGroupNode.hx | 23 + .../armory/logicnode/RemoveInputMapKeyNode.hx | 19 + Sources/armory/logicnode/RemoveMapKeyNode.hx | 19 + .../logicnode/RemoveObjectFromGroupNode.hx | 20 + Sources/armory/logicnode/RemoveObjectNode.hx | 37 + .../armory/logicnode/RemoveParentBoneNode.hx | 27 + Sources/armory/logicnode/RemovePhysicsNode.hx | 25 + Sources/armory/logicnode/RemoveTraitNode.hx | 16 + .../armory/logicnode/RemoveTraitObjectNode.hx | 29 + Sources/armory/logicnode/ResumeActionNode.hx | 22 + .../armory/logicnode/ResumeTilesheetNode.hx | 20 + Sources/armory/logicnode/ResumeTraitNode.hx | 17 + Sources/armory/logicnode/RetainValueNode.hx | 20 + .../logicnode/RotateObjectAroundAxisNode.hx | 30 + Sources/armory/logicnode/RotateObjectNode.hx | 39 + .../logicnode/RotateRenderTargetNode.hx | 28 + Sources/armory/logicnode/RotationMathNode.hx | 96 + Sources/armory/logicnode/RotationNode.hx | 101 + Sources/armory/logicnode/RpConfigNode.hx | 30 + Sources/armory/logicnode/RpMSAANode.hx | 29 + .../armory/logicnode/RpShadowQualityNode.hx | 26 + Sources/armory/logicnode/RpSuperSampleNode.hx | 27 + Sources/armory/logicnode/SSAOGetNode.hx | 18 + Sources/armory/logicnode/SSAOSetNode.hx | 17 + Sources/armory/logicnode/SSRGetNode.hx | 20 + Sources/armory/logicnode/SSRSetNode.hx | 19 + Sources/armory/logicnode/ScaleObjectNode.hx | 29 + Sources/armory/logicnode/SceneNode.hx | 18 + Sources/armory/logicnode/SceneRootNode.hx | 12 + .../logicnode/ScreenToWorldSpaceNode.hx | 74 + Sources/armory/logicnode/ScriptNode.hx | 42 + Sources/armory/logicnode/SelectNode.hx | 37 + Sources/armory/logicnode/SelfNode.hx | 12 + Sources/armory/logicnode/SelfTraitNode.hx | 12 + Sources/armory/logicnode/SendEventNode.hx | 26 + .../armory/logicnode/SendGlobalEventNode.hx | 21 + Sources/armory/logicnode/SensorCoordsNode.hx | 20 + .../armory/logicnode/SeparateColorHSVNode.hx | 47 + Sources/armory/logicnode/SeparateColorNode.hx | 23 + .../logicnode/SeparateQuaternionNode.hx | 23 + .../armory/logicnode/SeparateRotationNode.hx | 60 + .../armory/logicnode/SeparateTransformNode.hx | 26 + .../armory/logicnode/SeparateVectorNode.hx | 19 + Sources/armory/logicnode/SequenceNode.hx | 12 + .../armory/logicnode/SetActionPausedNode.hx | 25 + .../armory/logicnode/SetActionSpeedNode.hx | 23 + .../logicnode/SetActivationStateNode.hx | 43 + .../armory/logicnode/SetAreaLightSizeNode.hx | 23 + .../armory/logicnode/SetBoneFkIkOnlyNode.hx | 35 + .../armory/logicnode/SetCameraAspectNode.hx | 22 + Sources/armory/logicnode/SetCameraFovNode.hx | 22 + Sources/armory/logicnode/SetCameraNode.hx | 20 + .../armory/logicnode/SetCameraScaleNode.hx | 30 + .../armory/logicnode/SetCameraStartEndNode.hx | 39 + Sources/armory/logicnode/SetCameraTypeNode.hx | 42 + .../armory/logicnode/SetCursorStateNode.hx | 29 + .../logicnode/SetDebugConsoleSettings.hx | 32 + Sources/armory/logicnode/SetFrictionNode.hx | 27 + .../logicnode/SetGlobalCanvasFontSizeNode.hx | 25 + .../logicnode/SetGlobalCanvasScaleNode.hx | 25 + .../armory/logicnode/SetGravityEnabledNode.hx | 36 + Sources/armory/logicnode/SetGravityNode.hx | 23 + .../armory/logicnode/SetHaxePropertyNode.hx | 20 + .../armory/logicnode/SetInputMapKeyNode.hx | 46 + Sources/armory/logicnode/SetLightColorNode.hx | 23 + .../armory/logicnode/SetLightStrengthNode.hx | 21 + Sources/armory/logicnode/SetLocationNode.hx | 41 + Sources/armory/logicnode/SetMapValueNode.hx | 20 + .../logicnode/SetMaterialImageParamNode.hx | 48 + Sources/armory/logicnode/SetMaterialNode.hx | 24 + .../logicnode/SetMaterialRgbParamNode.hx | 45 + .../armory/logicnode/SetMaterialSlotNode.hx | 24 + .../logicnode/SetMaterialValueParamNode.hx | 45 + Sources/armory/logicnode/SetMeshNode.hx | 22 + Sources/armory/logicnode/SetMouseLockNode.hx | 15 + Sources/armory/logicnode/SetNameNode.hx | 21 + .../armory/logicnode/SetObjectShapeKeyNode.hx | 25 + Sources/armory/logicnode/SetParentBoneNode.hx | 29 + Sources/armory/logicnode/SetParentNode.hx | 28 + .../armory/logicnode/SetParticleSpeedNode.hx | 27 + Sources/armory/logicnode/SetPropertyNode.hx | 22 + Sources/armory/logicnode/SetRotationNode.hx | 34 + Sources/armory/logicnode/SetScaleNode.hx | 29 + Sources/armory/logicnode/SetSceneNode.hx | 31 + .../armory/logicnode/SetShaderUniformNode.hx | 65 + .../armory/logicnode/SetSpotLightBlendNode.hx | 21 + .../armory/logicnode/SetSpotLightSizeNode.hx | 21 + .../logicnode/SetTilesheetPausedNode.hx | 21 + Sources/armory/logicnode/SetTimeScaleNode.hx | 14 + .../armory/logicnode/SetTraitPausedNode.hx | 19 + Sources/armory/logicnode/SetTransformNode.hx | 28 + Sources/armory/logicnode/SetVariableNode.hx | 17 + Sources/armory/logicnode/SetVelocityNode.hx | 33 + Sources/armory/logicnode/SetVibrateNode.hx | 16 + Sources/armory/logicnode/SetVisibleNode.hx | 44 + Sources/armory/logicnode/ShowMouseNode.hx | 15 + Sources/armory/logicnode/ShutdownNode.hx | 12 + Sources/armory/logicnode/SleepNode.hx | 31 + .../armory/logicnode/SpawnCollectionNode.hx | 90 + .../armory/logicnode/SpawnObjectByNameNode.hx | 72 + Sources/armory/logicnode/SpawnObjectNode.hx | 48 + Sources/armory/logicnode/SpawnSceneNode.hx | 34 + Sources/armory/logicnode/SplitStringNode.hx | 16 + Sources/armory/logicnode/StopAgentNode.hx | 23 + Sources/armory/logicnode/StopSoundNode.hx | 17 + Sources/armory/logicnode/StringNode.hx | 21 + Sources/armory/logicnode/StringReplaceNode.hx | 17 + Sources/armory/logicnode/SubStringNode.hx | 17 + Sources/armory/logicnode/SurfaceCoordsNode.hx | 29 + Sources/armory/logicnode/SwitchNode.hx | 22 + Sources/armory/logicnode/TimeNode.hx | 13 + Sources/armory/logicnode/TimerNode.hx | 66 + Sources/armory/logicnode/ToBoolNode.hx | 21 + Sources/armory/logicnode/TouchInRegionNode.hx | 115 + Sources/armory/logicnode/TraitNode.hx | 25 + Sources/armory/logicnode/TransformMathNode.hx | 55 + Sources/armory/logicnode/TransformNode.hx | 37 + .../armory/logicnode/TranslateObjectNode.hx | 41 + .../logicnode/TranslateOnLocalAxisNode.hx | 35 + Sources/armory/logicnode/TweenFloatNode.hx | 114 + Sources/armory/logicnode/TweenRotationNode.hx | 116 + .../armory/logicnode/TweenTransformNode.hx | 129 + Sources/armory/logicnode/TweenVectorNode.hx | 116 + Sources/armory/logicnode/ValueChangedNode.hx | 37 + .../armory/logicnode/VectorClampToSizeNode.hx | 33 + .../armory/logicnode/VectorFromBooleanNode.hx | 36 + .../logicnode/VectorFromTransformNode.hx | 55 + Sources/armory/logicnode/VectorMathNode.hx | 138 + Sources/armory/logicnode/VectorMixNode.hx | 63 + .../armory/logicnode/VectorMoveTowardsNode.hx | 46 + Sources/armory/logicnode/VectorNode.hx | 32 + .../VectorToObjectOrientationNode.hx | 22 + Sources/armory/logicnode/VolumeTriggerNode.hx | 39 + Sources/armory/logicnode/WhileNode.hx | 28 + Sources/armory/logicnode/WindowInfoNode.hx | 13 + .../logicnode/WorldToScreenSpaceNode.hx | 30 + .../logicnode/WorldVectorToLocalSpaceNode.hx | 27 + Sources/armory/logicnode/WriteFileNode.hx | 20 + Sources/armory/logicnode/WriteJsonNode.hx | 22 + Sources/armory/logicnode/WriteStorageNode.hx | 21 + Sources/armory/math/Helper.hx | 121 + Sources/armory/math/Rotator.hx | 208 + Sources/armory/network/Buffer.hx | 173 + Sources/armory/network/Connect.hx | 315 ++ Sources/armory/network/Handler.hx | 12 + Sources/armory/network/HttpHeader.hx | 15 + Sources/armory/network/HttpRequest.hx | 52 + Sources/armory/network/HttpResponse.hx | 64 + Sources/armory/network/LICENSE.md | 24 + Sources/armory/network/Log.hx | 51 + Sources/armory/network/OpCode.hx | 14 + Sources/armory/network/SecureSocketImpl.hx | 3 + Sources/armory/network/SocketImpl.hx | 19 + Sources/armory/network/State.hx | 10 + Sources/armory/network/Types.hx | 22 + Sources/armory/network/Utf8Encoder.hx | 15 + Sources/armory/network/Util.hx | 9 + Sources/armory/network/WebSocket.hx | 365 ++ Sources/armory/network/WebSocketCommon.hx | 357 ++ Sources/armory/network/WebSocketHandler.hx | 86 + .../armory/network/WebSocketSecureServer.hx | 39 + Sources/armory/network/WebSocketServer.hx | 196 + Sources/armory/network/nodejs/NodeSocket.hx | 110 + .../armory/network/nodejs/NodeSocketInput.hx | 50 + .../armory/network/nodejs/NodeSocketOutput.hx | 28 + Sources/armory/network/uuid/Uuid.hx | 267 ++ Sources/armory/object/TransformExtension.hx | 72 + Sources/armory/object/Uniforms.hx | 230 + Sources/armory/renderpath/Downsampler.hx | 104 + .../renderpath/DynamicResolutionScale.hx | 51 + Sources/armory/renderpath/HosekWilkie.hx | 123 + Sources/armory/renderpath/HosekWilkieData.hx | 3855 +++++++++++++++++ Sources/armory/renderpath/Inc.hx | 1104 +++++ Sources/armory/renderpath/Nishita.hx | 231 + Sources/armory/renderpath/Postprocess.hx | 351 ++ .../armory/renderpath/RenderPathCreator.hx | 67 + .../armory/renderpath/RenderPathDeferred.hx | 1103 +++++ .../armory/renderpath/RenderPathForward.hx | 657 +++ .../armory/renderpath/RenderPathRaytracer.hx | 150 + Sources/armory/renderpath/RenderToTexture.hx | 21 + Sources/armory/renderpath/Upsampler.hx | 83 + Sources/armory/system/Assert.hx | 131 + Sources/armory/system/Event.hx | 83 + Sources/armory/system/FSM.hx | 74 + Sources/armory/system/InputMap.hx | 226 + Sources/armory/system/Logic.hx | 298 ++ Sources/armory/system/Starter.hx | 128 + Sources/armory/trait/ArcBall.hx | 27 + Sources/armory/trait/Character.hx | 77 + Sources/armory/trait/CustomParticle.hx | 40 + Sources/armory/trait/FirstPersonController.hx | 87 + Sources/armory/trait/FollowCamera.hx | 59 + Sources/armory/trait/NavAgent.hx | 81 + Sources/armory/trait/NavCrowd.hx | 10 + Sources/armory/trait/NavMesh.hx | 83 + Sources/armory/trait/PhysicsBreak.hx | 812 ++++ Sources/armory/trait/PhysicsDrag.hx | 132 + Sources/armory/trait/SimpleMoveObject.hx | 73 + Sources/armory/trait/SimpleRotateObject.hx | 72 + Sources/armory/trait/SimpleScaleObject.hx | 92 + Sources/armory/trait/ThirdPersonController.hx | 108 + Sources/armory/trait/VehicleBody.hx | 265 ++ Sources/armory/trait/VirtualGamepad.hx | 139 + Sources/armory/trait/WalkNavigation.hx | 149 + Sources/armory/trait/internal/Bridge.hx | 19 + .../armory/trait/internal/CameraController.hx | 60 + Sources/armory/trait/internal/CanvasScript.hx | 203 + Sources/armory/trait/internal/DebugConsole.hx | 1016 +++++ Sources/armory/trait/internal/DebugDraw.hx | 215 + Sources/armory/trait/internal/LivePatch.hx | 195 + .../armory/trait/internal/LoadingScreen.hx | 11 + Sources/armory/trait/internal/MovieTexture.hx | 90 + .../armory/trait/internal/TerrainPhysics.hx | 34 + .../armory/trait/internal/UniformsManager.hx | 372 ++ Sources/armory/trait/internal/WasmScript.hx | 91 + Sources/armory/trait/internal/wasm_api.h | 44 + Sources/armory/trait/navigation/Navigation.hx | 26 + .../physics/KinematicCharacterController.hx | 15 + .../armory/trait/physics/PhysicsConstraint.hx | 19 + Sources/armory/trait/physics/PhysicsHook.hx | 19 + Sources/armory/trait/physics/PhysicsWorld.hx | 19 + Sources/armory/trait/physics/RigidBody.hx | 19 + Sources/armory/trait/physics/SoftBody.hx | 19 + .../bullet/KinematicCharacterController.hx | 364 ++ .../trait/physics/bullet/PhysicsConstraint.hx | 476 ++ .../bullet/PhysicsConstraintExportHelper.hx | 60 + .../trait/physics/bullet/PhysicsHook.hx | 189 + .../trait/physics/bullet/PhysicsWorld.hx | 488 +++ .../armory/trait/physics/bullet/RigidBody.hx | 694 +++ .../armory/trait/physics/bullet/SoftBody.hx | 277 ++ Sources/armory/ui/Canvas.hx | 498 +++ Sources/armory/ui/Ext.hx | 331 ++ Sources/armory/ui/Popup.hx | 127 + Sources/armory/ui/Themes.hx | 42 + blender/__init__.py | 0 blender/arm/LICENSE.md | 340 ++ blender/arm/__init__.py | 44 + blender/arm/api.py | 55 + blender/arm/assets.py | 204 + blender/arm/custom_icons/bundle.png | Bin 0 -> 630 bytes blender/arm/custom_icons/haxe.png | Bin 0 -> 673 bytes blender/arm/custom_icons/wasm.png | Bin 0 -> 414 bytes blender/arm/exporter.py | 3252 ++++++++++++++ blender/arm/exporter_opt.py | 420 ++ blender/arm/handlers.py | 297 ++ blender/arm/keymap.py | 54 + blender/arm/lib/__init__.py | 0 blender/arm/lib/armpack.py | 175 + blender/arm/lib/lz4.py | 181 + blender/arm/lib/make_datas.py | 328 ++ blender/arm/lib/server.py | 33 + blender/arm/lightmapper/__init__.py | 1 + .../arm/lightmapper/assets/TLM_Overlay.png | Bin 0 -> 24403 bytes blender/arm/lightmapper/assets/dash.ogg | Bin 0 -> 36333 bytes blender/arm/lightmapper/assets/gentle.ogg | Bin 0 -> 6615 bytes blender/arm/lightmapper/assets/noot.ogg | Bin 0 -> 20436 bytes blender/arm/lightmapper/assets/pingping.ogg | Bin 0 -> 9721 bytes blender/arm/lightmapper/assets/sound.ogg | Bin 0 -> 20436 bytes blender/arm/lightmapper/assets/tlm_data.blend | Bin 0 -> 126639 bytes blender/arm/lightmapper/icons/bake.png | Bin 0 -> 3308 bytes blender/arm/lightmapper/icons/clean.png | Bin 0 -> 3075 bytes blender/arm/lightmapper/icons/explore.png | Bin 0 -> 2967 bytes blender/arm/lightmapper/keymap/__init__.py | 7 + blender/arm/lightmapper/keymap/keymap.py | 22 + blender/arm/lightmapper/network/client.py | 27 + blender/arm/lightmapper/network/server.py | 71 + blender/arm/lightmapper/operators/__init__.py | 50 + .../arm/lightmapper/operators/imagetools.py | 193 + .../lightmapper/operators/installopencv.py | 81 + blender/arm/lightmapper/operators/tlm.py | 1731 ++++++++ blender/arm/lightmapper/panels/__init__.py | 0 blender/arm/lightmapper/panels/image.py | 66 + blender/arm/lightmapper/panels/light.py | 17 + blender/arm/lightmapper/panels/object.py | 126 + blender/arm/lightmapper/panels/scene.py | 756 ++++ blender/arm/lightmapper/panels/world.py | 17 + .../arm/lightmapper/preferences/__init__.py | 16 + .../preferences/addon_preferences.py | 106 + .../arm/lightmapper/properties/__init__.py | 62 + blender/arm/lightmapper/properties/atlas.py | 166 + .../properties/denoiser/integrated.py | 4 + .../lightmapper/properties/denoiser/oidn.py | 40 + .../lightmapper/properties/denoiser/optix.py | 21 + .../arm/lightmapper/properties/filtering.py | 4 + blender/arm/lightmapper/properties/image.py | 26 + blender/arm/lightmapper/properties/object.py | 182 + .../lightmapper/properties/renderer/cycles.py | 115 + .../properties/renderer/luxcorerender.py | 11 + .../properties/renderer/octanerender.py | 10 + .../properties/renderer/radeonrays.py | 0 blender/arm/lightmapper/properties/scene.py | 585 +++ blender/arm/lightmapper/utility/__init__.py | 0 blender/arm/lightmapper/utility/build.py | 1361 ++++++ blender/arm/lightmapper/utility/cycles/ao.py | 0 .../arm/lightmapper/utility/cycles/cache.py | 124 + .../lightmapper/utility/cycles/indirect.py | 0 .../lightmapper/utility/cycles/lightmap.py | 179 + .../arm/lightmapper/utility/cycles/nodes.py | 527 +++ .../arm/lightmapper/utility/cycles/prepare.py | 947 ++++ .../utility/denoiser/integrated.py | 80 + .../arm/lightmapper/utility/denoiser/oidn.py | 207 + .../arm/lightmapper/utility/denoiser/optix.py | 92 + blender/arm/lightmapper/utility/encoding.py | 674 +++ .../lightmapper/utility/filtering/numpy.py | 49 + .../lightmapper/utility/filtering/opencv.py | 178 + .../lightmapper/utility/filtering/shader.py | 160 + .../arm/lightmapper/utility/gui/Viewport.py | 77 + blender/arm/lightmapper/utility/icon.py | 31 + blender/arm/lightmapper/utility/log.py | 21 + .../arm/lightmapper/utility/luxcore/setup.py | 259 ++ .../lightmapper/utility/octane/configure.py | 243 ++ .../lightmapper/utility/octane/lightmap2.py | 71 + blender/arm/lightmapper/utility/pack.py | 354 ++ .../utility/preconfiguration/object.py | 5 + .../lightmapper/utility/rectpack/__init__.py | 23 + .../lightmapper/utility/rectpack/enclose.py | 148 + .../lightmapper/utility/rectpack/geometry.py | 344 ++ .../utility/rectpack/guillotine.py | 368 ++ .../lightmapper/utility/rectpack/maxrects.py | 244 ++ .../lightmapper/utility/rectpack/pack_algo.py | 140 + .../lightmapper/utility/rectpack/packer.py | 580 +++ .../lightmapper/utility/rectpack/skyline.py | 303 ++ .../arm/lightmapper/utility/rectpack/waste.py | 23 + blender/arm/lightmapper/utility/utility.py | 677 +++ blender/arm/live_patch.py | 392 ++ blender/arm/log.py | 92 + blender/arm/logicnode/__init__.py | 99 + blender/arm/logicnode/animation/LN_action.py | 12 + .../logicnode/animation/LN_blend_action.py | 16 + blender/arm/logicnode/animation/LN_bone_fk.py | 16 + blender/arm/logicnode/animation/LN_bone_ik.py | 50 + .../animation/LN_get_action_state.py | 14 + .../animation/LN_get_bone_fk_ik_only.py | 13 + .../animation/LN_get_bone_transform.py | 13 + .../animation/LN_get_tilesheet_state.py | 15 + .../animation/LN_on_action_marker.py | 13 + .../animation/LN_play_action_from.py | 51 + .../logicnode/animation/LN_play_tilesheet.py | 16 + .../animation/LN_remove_parent_bone.py | 16 + .../animation/LN_set_action_paused.py | 14 + .../animation/LN_set_action_speed.py | 14 + .../animation/LN_set_bone_fk_ik_only.py | 16 + .../logicnode/animation/LN_set_parent_bone.py | 16 + .../animation/LN_set_particle_speed.py | 14 + .../animation/LN_set_tilesheet_paused.py | 15 + blender/arm/logicnode/animation/__init__.py | 5 + blender/arm/logicnode/arm_node_group.py | 565 +++ blender/arm/logicnode/arm_nodes.py | 1029 +++++ blender/arm/logicnode/arm_props.py | 282 ++ blender/arm/logicnode/arm_sockets.py | 665 +++ blender/arm/logicnode/array/LN_array.py | 50 + blender/arm/logicnode/array/LN_array_add.py | 45 + .../arm/logicnode/array/LN_array_boolean.py | 50 + blender/arm/logicnode/array/LN_array_color.py | 50 + .../arm/logicnode/array/LN_array_compare.py | 15 + .../arm/logicnode/array/LN_array_concat.py | 15 + .../arm/logicnode/array/LN_array_contains.py | 13 + blender/arm/logicnode/array/LN_array_count.py | 12 + .../arm/logicnode/array/LN_array_display.py | 31 + .../arm/logicnode/array/LN_array_distinct.py | 13 + .../arm/logicnode/array/LN_array_filter.py | 49 + blender/arm/logicnode/array/LN_array_float.py | 50 + blender/arm/logicnode/array/LN_array_get.py | 13 + .../array/LN_array_get_PreviousNext.py | 13 + .../arm/logicnode/array/LN_array_get_next.py | 12 + blender/arm/logicnode/array/LN_array_index.py | 13 + .../arm/logicnode/array/LN_array_integer.py | 50 + .../arm/logicnode/array/LN_array_length.py | 12 + .../arm/logicnode/array/LN_array_loop_node.py | 17 + .../arm/logicnode/array/LN_array_object.py | 50 + blender/arm/logicnode/array/LN_array_pop.py | 14 + .../array/LN_array_remove_by_index.py | 17 + .../array/LN_array_remove_by_value.py | 29 + .../arm/logicnode/array/LN_array_resize.py | 18 + .../arm/logicnode/array/LN_array_reverse.py | 13 + .../arm/logicnode/array/LN_array_sample.py | 15 + blender/arm/logicnode/array/LN_array_set.py | 15 + blender/arm/logicnode/array/LN_array_shift.py | 14 + .../arm/logicnode/array/LN_array_shuffle.py | 13 + blender/arm/logicnode/array/LN_array_slice.py | 16 + blender/arm/logicnode/array/LN_array_sort.py | 14 + .../arm/logicnode/array/LN_array_splice.py | 17 + .../arm/logicnode/array/LN_array_string.py | 50 + .../arm/logicnode/array/LN_array_vector.py | 50 + blender/arm/logicnode/array/__init__.py | 4 + .../logicnode/camera/LN_get_camera_active.py | 12 + .../logicnode/camera/LN_get_camera_aspect.py | 12 + .../arm/logicnode/camera/LN_get_camera_fov.py | 14 + .../logicnode/camera/LN_get_camera_scale.py | 13 + .../camera/LN_get_camera_start_end.py | 15 + .../logicnode/camera/LN_get_camera_type.py | 15 + .../logicnode/camera/LN_set_camera_active.py | 15 + .../logicnode/camera/LN_set_camera_aspect.py | 14 + .../arm/logicnode/camera/LN_set_camera_fov.py | 16 + .../logicnode/camera/LN_set_camera_scale.py | 14 + .../camera/LN_set_camera_start_end.py | 39 + .../logicnode/camera/LN_set_camera_type.py | 33 + blender/arm/logicnode/camera/__init__.py | 0 .../canvas/LN_get_canvas_checkbox.py | 12 + .../canvas/LN_get_canvas_input_text.py | 12 + .../canvas/LN_get_canvas_location.py | 15 + .../canvas/LN_get_canvas_position.py | 12 + .../canvas/LN_get_canvas_progress_bar.py | 15 + .../canvas/LN_get_canvas_rotation.py | 14 + .../logicnode/canvas/LN_get_canvas_scale.py | 15 + .../logicnode/canvas/LN_get_canvas_slider.py | 12 + .../logicnode/canvas/LN_get_canvas_text.py | 12 + .../logicnode/canvas/LN_get_canvas_visible.py | 15 + .../canvas/LN_get_global_canvas_font_size.py | 11 + .../canvas/LN_get_global_canvas_scale.py | 11 + .../logicnode/canvas/LN_on_canvas_element.py | 37 + .../logicnode/canvas/LN_set_canvas_asset.py | 14 + .../canvas/LN_set_canvas_checkbox.py | 14 + .../logicnode/canvas/LN_set_canvas_color.py | 41 + .../canvas/LN_set_canvas_input_text.py | 12 + .../canvas/LN_set_canvas_input_text_focus.py | 12 + .../canvas/LN_set_canvas_location.py | 15 + .../canvas/LN_set_canvas_progress_bar.py | 15 + .../canvas/LN_set_canvas_rotation.py | 14 + .../logicnode/canvas/LN_set_canvas_scale.py | 15 + .../logicnode/canvas/LN_set_canvas_slider.py | 14 + .../logicnode/canvas/LN_set_canvas_text.py | 14 + .../logicnode/canvas/LN_set_canvas_visible.py | 14 + .../canvas/LN_set_global_canvas_font_size.py | 14 + .../canvas/LN_set_global_canvas_scale.py | 14 + blender/arm/logicnode/canvas/__init__.py | 0 .../logicnode/deprecated/LN_get_mouse_lock.py | 27 + .../deprecated/LN_get_mouse_visible.py | 30 + .../logicnode/deprecated/LN_group_nodes.py | 16 + .../logicnode/deprecated/LN_mouse_coords.py | 50 + .../arm/logicnode/deprecated/LN_on_gamepad.py | 62 + .../logicnode/deprecated/LN_on_keyboard.py | 93 + .../arm/logicnode/deprecated/LN_on_mouse.py | 47 + .../arm/logicnode/deprecated/LN_on_surface.py | 26 + .../deprecated/LN_on_virtual_button.py | 27 + .../logicnode/deprecated/LN_pause_action.py | 16 + .../deprecated/LN_pause_tilesheet.py | 17 + .../logicnode/deprecated/LN_pause_trait.py | 16 + .../logicnode/deprecated/LN_play_action.py | 19 + .../arm/logicnode/deprecated/LN_quaternion.py | 75 + .../logicnode/deprecated/LN_resume_action.py | 16 + .../deprecated/LN_resume_tilesheet.py | 16 + .../logicnode/deprecated/LN_resume_trait.py | 16 + .../LN_rotate_object_around_axis.py | 29 + .../logicnode/deprecated/LN_scale_object.py | 18 + .../deprecated/LN_separate_quaternion.py | 45 + .../LN_set_canvas_progress_bar_color.py | 33 + .../deprecated/LN_set_canvas_text_color.py | 49 + .../logicnode/deprecated/LN_set_mouse_lock.py | 27 + .../deprecated/LN_set_mouse_visible.py | 43 + .../deprecated/LN_set_object_material.py | 17 + .../logicnode/deprecated/LN_surface_coords.py | 16 + blender/arm/logicnode/deprecated/__init__.py | 1 + .../draw/LN_draw_Text_Area_string.py | 67 + blender/arm/logicnode/draw/LN_draw_arc.py | 43 + blender/arm/logicnode/draw/LN_draw_camera.py | 68 + .../logicnode/draw/LN_draw_camera_texture.py | 31 + blender/arm/logicnode/draw/LN_draw_circle.py | 37 + blender/arm/logicnode/draw/LN_draw_curve.py | 39 + blender/arm/logicnode/draw/LN_draw_ellipse.py | 41 + blender/arm/logicnode/draw/LN_draw_image.py | 52 + .../logicnode/draw/LN_draw_image_sequence.py | 67 + blender/arm/logicnode/draw/LN_draw_line.py | 31 + blender/arm/logicnode/draw/LN_draw_polygon.py | 72 + blender/arm/logicnode/draw/LN_draw_rect.py | 55 + blender/arm/logicnode/draw/LN_draw_string.py | 35 + .../draw/LN_draw_to_material_image.py | 39 + .../arm/logicnode/draw/LN_draw_triangle.py | 35 + blender/arm/logicnode/draw/__init__.py | 3 + .../event/LN_on_application_state.py | 12 + blender/arm/logicnode/event/LN_on_event.py | 70 + blender/arm/logicnode/event/LN_on_init.py | 10 + blender/arm/logicnode/event/LN_on_render2d.py | 14 + blender/arm/logicnode/event/LN_on_timer.py | 16 + blender/arm/logicnode/event/LN_on_update.py | 24 + .../event/LN_send_event_to_object.py | 21 + .../logicnode/event/LN_send_global_event.py | 19 + blender/arm/logicnode/event/__init__.py | 4 + .../logicnode/input/LN_cursor_in_region.py | 37 + blender/arm/logicnode/input/LN_gamepad.py | 64 + .../arm/logicnode/input/LN_gamepad_coords.py | 22 + .../arm/logicnode/input/LN_gamepad_sticks.py | 52 + .../logicnode/input/LN_get_cursor_location.py | 14 + .../logicnode/input/LN_get_cursor_state.py | 22 + .../logicnode/input/LN_get_gamepad_started.py | 14 + .../logicnode/input/LN_get_input_map_key.py | 14 + .../input/LN_get_keyboard_started.py | 13 + .../logicnode/input/LN_get_mouse_movement.py | 23 + .../logicnode/input/LN_get_mouse_started.py | 13 + .../logicnode/input/LN_get_touch_location.py | 14 + .../logicnode/input/LN_get_touch_movement.py | 17 + blender/arm/logicnode/input/LN_keyboard.py | 89 + blender/arm/logicnode/input/LN_mouse.py | 42 + .../arm/logicnode/input/LN_on_input_map.py | 15 + blender/arm/logicnode/input/LN_on_swipe.py | 98 + .../arm/logicnode/input/LN_on_tap_screen.py | 30 + .../input/LN_remove_input_map_key.py | 14 + .../arm/logicnode/input/LN_sensor_coords.py | 11 + .../logicnode/input/LN_set_cursor_state.py | 33 + .../logicnode/input/LN_set_input_map_key.py | 27 + blender/arm/logicnode/input/LN_touch.py | 26 + .../arm/logicnode/input/LN_touch_in_region.py | 37 + .../arm/logicnode/input/LN_virtual_button.py | 27 + blender/arm/logicnode/input/__init__.py | 8 + .../logicnode/light/LN_set_area_light_size.py | 15 + .../arm/logicnode/light/LN_set_light_color.py | 14 + .../logicnode/light/LN_set_light_strength.py | 14 + .../light/LN_set_spot_light_blend.py | 14 + .../logicnode/light/LN_set_spot_light_size.py | 14 + blender/arm/logicnode/light/__init__.py | 0 .../logicnode/logic/LN_alternate_output.py | 25 + blender/arm/logicnode/logic/LN_branch.py | 16 + .../arm/logicnode/logic/LN_call_function.py | 42 + blender/arm/logicnode/logic/LN_case_index.py | 60 + blender/arm/logicnode/logic/LN_function.py | 42 + .../arm/logicnode/logic/LN_function_output.py | 21 + blender/arm/logicnode/logic/LN_gate.py | 78 + .../arm/logicnode/logic/LN_invert_boolean.py | 12 + .../arm/logicnode/logic/LN_invert_output.py | 14 + blender/arm/logicnode/logic/LN_is_false.py | 17 + blender/arm/logicnode/logic/LN_is_not_null.py | 16 + blender/arm/logicnode/logic/LN_is_null.py | 17 + blender/arm/logicnode/logic/LN_is_true.py | 16 + blender/arm/logicnode/logic/LN_loop.py | 41 + blender/arm/logicnode/logic/LN_loop_break.py | 16 + .../arm/logicnode/logic/LN_loop_continue.py | 15 + blender/arm/logicnode/logic/LN_merge.py | 91 + blender/arm/logicnode/logic/LN_null.py | 10 + .../arm/logicnode/logic/LN_once_per_frame.py | 15 + .../arm/logicnode/logic/LN_output_sequence.py | 34 + .../logicnode/logic/LN_output_to_boolean.py | 13 + blender/arm/logicnode/logic/LN_pulse.py | 19 + blender/arm/logicnode/logic/LN_select.py | 125 + .../arm/logicnode/logic/LN_switch_output.py | 43 + .../arm/logicnode/logic/LN_value_changed.py | 38 + blender/arm/logicnode/logic/LN_while_true.py | 23 + blender/arm/logicnode/logic/__init__.py | 4 + blender/arm/logicnode/map/LN_clear_map.py | 14 + blender/arm/logicnode/map/LN_create_map.py | 38 + blender/arm/logicnode/map/LN_get_map_value.py | 14 + .../arm/logicnode/map/LN_map_key_exists.py | 16 + blender/arm/logicnode/map/LN_map_loop.py | 18 + .../arm/logicnode/map/LN_remove_map_key.py | 15 + blender/arm/logicnode/map/LN_set_map_value.py | 16 + blender/arm/logicnode/map/__init__.py | 1 + .../material/LN_get_object_material.py | 13 + blender/arm/logicnode/material/LN_material.py | 30 + .../material/LN_set_material_image_param.py | 42 + .../material/LN_set_material_rgb_param.py | 42 + .../material/LN_set_material_value_param.py | 43 + .../material/LN_set_object_material_slot.py | 15 + blender/arm/logicnode/material/__init__.py | 4 + blender/arm/logicnode/math/LN_bitwise_math.py | 58 + blender/arm/logicnode/math/LN_clamp.py | 17 + blender/arm/logicnode/math/LN_combine_hsv.py | 20 + blender/arm/logicnode/math/LN_combine_rgb.py | 19 + blender/arm/logicnode/math/LN_compare.py | 67 + blender/arm/logicnode/math/LN_deg_to_rad.py | 13 + .../math/LN_float_delta_interpolate.py | 20 + .../arm/logicnode/math/LN_key_interpolate.py | 18 + blender/arm/logicnode/math/LN_map_range.py | 19 + blender/arm/logicnode/math/LN_math.py | 138 + .../arm/logicnode/math/LN_math_expression.py | 189 + blender/arm/logicnode/math/LN_matrix_math.py | 22 + blender/arm/logicnode/math/LN_mix.py | 43 + blender/arm/logicnode/math/LN_mix_vector.py | 46 + .../arm/logicnode/math/LN_quaternion_math.py | 369 ++ blender/arm/logicnode/math/LN_rad_to_deg.py | 13 + .../arm/logicnode/math/LN_rotation_math.py | 121 + .../math/LN_screen_to_world_space.py | 42 + blender/arm/logicnode/math/LN_separate_hsv.py | 20 + blender/arm/logicnode/math/LN_separate_rgb.py | 25 + blender/arm/logicnode/math/LN_separate_xyz.py | 15 + blender/arm/logicnode/math/LN_tween_float.py | 67 + .../arm/logicnode/math/LN_tween_rotation.py | 67 + .../arm/logicnode/math/LN_tween_transform.py | 67 + blender/arm/logicnode/math/LN_tween_vector.py | 67 + blender/arm/logicnode/math/LN_vector_clamp.py | 41 + blender/arm/logicnode/math/LN_vector_math.py | 152 + .../logicnode/math/LN_vector_move_towards.py | 15 + .../math/LN_world_to_screen_space.py | 13 + blender/arm/logicnode/math/__init__.py | 7 + .../miscellaneous/LN_boolean_to_int.py | 12 + .../miscellaneous/LN_boolean_to_vector.py | 17 + .../logicnode/miscellaneous/LN_call_group.py | 143 + .../miscellaneous/LN_default_if_null.py | 17 + .../miscellaneous/LN_get_application_time.py | 11 + .../LN_get_debug_console_settings.py | 12 + .../LN_get_display_resolution.py | 15 + .../arm/logicnode/miscellaneous/LN_get_fps.py | 10 + .../miscellaneous/LN_get_window_resolution.py | 15 + .../logicnode/miscellaneous/LN_group_input.py | 217 + .../miscellaneous/LN_group_output.py | 217 + .../LN_set_debug_console_settings.py | 24 + .../miscellaneous/LN_set_time_scale.py | 13 + .../arm/logicnode/miscellaneous/LN_sleep.py | 14 + .../arm/logicnode/miscellaneous/LN_timer.py | 50 + .../arm/logicnode/miscellaneous/__init__.py | 5 + .../logicnode/native/LN_call_haxe_static.py | 47 + .../arm/logicnode/native/LN_clear_console.py | 12 + .../native/LN_detect_mobile_browser.py | 10 + blender/arm/logicnode/native/LN_expression.py | 21 + .../arm/logicnode/native/LN_get_date_time.py | 113 + .../logicnode/native/LN_get_haxe_property.py | 16 + .../native/LN_get_system_language.py | 11 + .../logicnode/native/LN_get_system_name.py | 17 + blender/arm/logicnode/native/LN_loadUrl.py | 11 + blender/arm/logicnode/native/LN_print.py | 13 + blender/arm/logicnode/native/LN_read_file.py | 29 + blender/arm/logicnode/native/LN_read_json.py | 29 + .../arm/logicnode/native/LN_read_storage.py | 22 + blender/arm/logicnode/native/LN_script.py | 28 + .../logicnode/native/LN_set_haxe_property.py | 18 + .../arm/logicnode/native/LN_set_vibrate.py | 17 + blender/arm/logicnode/native/LN_shutdown.py | 12 + blender/arm/logicnode/native/LN_write_file.py | 23 + blender/arm/logicnode/native/LN_write_json.py | 25 + .../arm/logicnode/native/LN_write_storage.py | 37 + blender/arm/logicnode/native/__init__.py | 0 .../logicnode/navmesh/LN_get_agent_data.py | 13 + .../logicnode/navmesh/LN_go_to_location.py | 44 + .../navmesh/LN_navigable_location.py | 10 + .../navmesh/LN_pick_navmesh_location.py | 13 + .../arm/logicnode/navmesh/LN_stop_agent.py | 13 + blender/arm/logicnode/navmesh/__init__.py | 0 .../logicnode/network/LN_network_client.py | 16 + .../network/LN_network_close_connection.py | 34 + .../arm/logicnode/network/LN_network_event.py | 92 + .../arm/logicnode/network/LN_network_host.py | 70 + .../network/LN_network_host_close_client.py | 27 + .../network/LN_network_host_get_ip.py | 29 + .../network/LN_network_http_request.py | 78 + .../network/LN_network_message_parser.py | 33 + .../arm/logicnode/network/LN_network_open.py | 28 + .../network/LN_network_send_message.py | 108 + blender/arm/logicnode/network/__init__.py | 1 + .../arm/logicnode/object/LN_get_distance.py | 16 + .../logicnode/object/LN_get_object_by_name.py | 16 + .../logicnode/object/LN_get_object_by_uid.py | 16 + .../logicnode/object/LN_get_object_child.py | 26 + .../object/LN_get_object_children.py | 13 + .../logicnode/object/LN_get_object_mesh.py | 13 + .../logicnode/object/LN_get_object_name.py | 13 + .../object/LN_get_object_offscreen.py | 15 + .../logicnode/object/LN_get_object_parent.py | 15 + .../object/LN_get_object_property.py | 17 + .../arm/logicnode/object/LN_get_object_uid.py | 13 + .../logicnode/object/LN_get_object_visible.py | 18 + blender/arm/logicnode/object/LN_mesh.py | 18 + blender/arm/logicnode/object/LN_object.py | 27 + .../object/LN_raycast_closest_object.py | 21 + .../arm/logicnode/object/LN_raycast_object.py | 21 + .../arm/logicnode/object/LN_remove_object.py | 26 + .../object/LN_remove_object_parent.py | 15 + .../arm/logicnode/object/LN_self_object.py | 10 + .../logicnode/object/LN_set_object_mesh.py | 15 + .../logicnode/object/LN_set_object_name.py | 15 + .../logicnode/object/LN_set_object_parent.py | 32 + .../object/LN_set_object_property.py | 23 + .../object/LN_set_object_shape_key.py | 16 + .../logicnode/object/LN_set_object_visible.py | 29 + .../arm/logicnode/object/LN_spawn_object.py | 17 + .../object/LN_spawn_object_by_name.py | 24 + blender/arm/logicnode/object/__init__.py | 5 + .../logicnode/physics/LN_Add_rigid_body.py | 140 + .../physics/LN_add_physics_constraint.py | 176 + .../arm/logicnode/physics/LN_apply_force.py | 25 + .../physics/LN_apply_force_at_location.py | 30 + .../arm/logicnode/physics/LN_apply_impulse.py | 25 + .../physics/LN_apply_impulse_at_location.py | 30 + .../arm/logicnode/physics/LN_apply_torque.py | 16 + .../physics/LN_apply_torque_impulse.py | 16 + .../arm/logicnode/physics/LN_convex_cast.py | 34 + .../logicnode/physics/LN_convex_cast_on.py | 38 + .../logicnode/physics/LN_get_rb_contacts.py | 17 + .../arm/logicnode/physics/LN_get_rb_data.py | 26 + .../physics/LN_get_rb_first_contact.py | 16 + .../physics/LN_get_rb_point_velocity.py | 13 + .../logicnode/physics/LN_get_rb_velocity.py | 15 + .../logicnode/physics/LN_get_world_gravity.py | 13 + .../arm/logicnode/physics/LN_has_contact.py | 14 + .../logicnode/physics/LN_has_contact_array.py | 14 + .../arm/logicnode/physics/LN_is_rb_active.py | 11 + .../arm/logicnode/physics/LN_on_contact.py | 33 + .../logicnode/physics/LN_on_contact_array.py | 24 + .../logicnode/physics/LN_on_volume_trigger.py | 27 + .../physics/LN_physics_constraint.py | 74 + blender/arm/logicnode/physics/LN_pick_rb.py | 33 + blender/arm/logicnode/physics/LN_ray_cast.py | 32 + .../arm/logicnode/physics/LN_ray_cast_on.py | 36 + blender/arm/logicnode/physics/LN_remove_rb.py | 13 + .../physics/LN_set_rb_activation_state.py | 26 + .../logicnode/physics/LN_set_rb_friction.py | 15 + .../physics/LN_set_rb_gravity_enabled.py | 14 + .../logicnode/physics/LN_set_rb_velocity.py | 17 + .../logicnode/physics/LN_set_world_gravity.py | 16 + .../logicnode/physics/LN_volume_trigger.py | 29 + blender/arm/logicnode/physics/__init__.py | 6 + .../LN_colorgrading_get_global_node.py | 17 + .../LN_colorgrading_get_highlight_node.py | 16 + .../LN_colorgrading_get_midtone_node.py | 15 + .../LN_colorgrading_get_shadow_node.py | 16 + .../LN_colorgrading_set_global_node.py | 75 + .../LN_colorgrading_set_highlight_node.py | 73 + .../LN_colorgrading_set_midtone_node.py | 72 + .../LN_colorgrading_set_shadow_node.py | 73 + .../postprocess/LN_get_bloom_settings.py | 24 + .../postprocess/LN_get_ca_settings.py | 11 + .../postprocess/LN_get_camera_post_process.py | 29 + .../LN_get_lenstexture_settings.py | 14 + .../postprocess/LN_get_letterbox_settings.py | 11 + .../postprocess/LN_get_ssao_settings.py | 12 + .../postprocess/LN_get_ssr_settings.py | 14 + .../postprocess/LN_lenstexture_set.py | 17 + .../postprocess/LN_set_bloom_settings.py | 34 + .../postprocess/LN_set_ca_settings.py | 14 + .../postprocess/LN_set_camera_post_process.py | 39 + .../postprocess/LN_set_letterbox_settings.py | 20 + .../postprocess/LN_set_ssao_settings.py | 15 + .../postprocess/LN_set_ssr_settings.py | 17 + blender/arm/logicnode/postprocess/__init__.py | 4 + .../arm/logicnode/random/LN_random_boolean.py | 11 + .../arm/logicnode/random/LN_random_choice.py | 13 + .../arm/logicnode/random/LN_random_color.py | 11 + .../arm/logicnode/random/LN_random_float.py | 14 + .../arm/logicnode/random/LN_random_integer.py | 13 + .../arm/logicnode/random/LN_random_output.py | 25 + .../arm/logicnode/random/LN_random_string.py | 25 + .../arm/logicnode/random/LN_random_vector.py | 13 + blender/arm/logicnode/random/__init__.py | 0 .../renderpath/LN_create_render_target.py | 40 + .../LN_pause_active_camera_render.py | 20 + .../renderpath/LN_rotate_render_target.py | 31 + .../renderpath/LN_set_msaa_quality.py | 24 + .../renderpath/LN_set_post_process_quality.py | 25 + .../renderpath/LN_set_shader_uniform.py | 46 + .../renderpath/LN_set_shadows_quality.py | 22 + .../renderpath/LN_set_ssaa_quality.py | 23 + blender/arm/logicnode/renderpath/__init__.py | 0 blender/arm/logicnode/replacement.py | 346 ++ .../scene/LN_add_object_to_collection.py | 15 + blender/arm/logicnode/scene/LN_collection.py | 21 + .../logicnode/scene/LN_create_collection.py | 15 + .../arm/logicnode/scene/LN_get_collection.py | 16 + .../scene/LN_get_object_collection.py | 14 + .../logicnode/scene/LN_get_scene_active.py | 10 + .../arm/logicnode/scene/LN_get_scene_root.py | 10 + .../arm/logicnode/scene/LN_global_object.py | 11 + .../logicnode/scene/LN_remove_collection.py | 14 + .../scene/LN_remove_object_from_collection.py | 15 + .../logicnode/scene/LN_remove_scene_active.py | 12 + .../logicnode/scene/LN_set_scene_active.py | 14 + .../logicnode/scene/LN_spawn_collection.py | 58 + blender/arm/logicnode/scene/LN_spawn_scene.py | 15 + blender/arm/logicnode/scene/__init__.py | 5 + .../arm/logicnode/sound/LN_pause_speaker.py | 18 + blender/arm/logicnode/sound/LN_play_sound.py | 97 + .../arm/logicnode/sound/LN_play_speaker.py | 18 + .../arm/logicnode/sound/LN_stop_speaker.py | 18 + blender/arm/logicnode/sound/__init__.py | 0 .../logicnode/string/LN_concatenate_string.py | 34 + .../arm/logicnode/string/LN_parse_float.py | 13 + blender/arm/logicnode/string/LN_parse_int.py | 13 + .../arm/logicnode/string/LN_split_string.py | 13 + blender/arm/logicnode/string/LN_string.py | 16 + .../arm/logicnode/string/LN_string_case.py | 21 + .../logicnode/string/LN_string_contains.py | 23 + .../arm/logicnode/string/LN_string_length.py | 12 + .../arm/logicnode/string/LN_string_replace.py | 15 + blender/arm/logicnode/string/LN_sub_string.py | 14 + blender/arm/logicnode/string/__init__.py | 4 + .../logicnode/trait/LN_add_trait_to_object.py | 14 + .../logicnode/trait/LN_get_object_trait.py | 14 + .../logicnode/trait/LN_get_object_traits.py | 12 + .../arm/logicnode/trait/LN_get_trait_name.py | 13 + .../logicnode/trait/LN_get_trait_paused.py | 12 + .../arm/logicnode/trait/LN_remove_trait.py | 13 + .../trait/LN_remove_trait_from_object.py | 14 + blender/arm/logicnode/trait/LN_self_trait.py | 10 + .../logicnode/trait/LN_set_trait_paused.py | 14 + blender/arm/logicnode/trait/LN_trait.py | 21 + blender/arm/logicnode/trait/__init__.py | 0 .../transform/LN_append_transform.py | 14 + .../transform/LN_get_object_dimension.py | 13 + .../transform/LN_get_object_location.py | 26 + .../transform/LN_get_object_rotation.py | 91 + .../transform/LN_get_object_scale.py | 13 + .../transform/LN_get_object_transform.py | 14 + .../transform/LN_get_world_orientation.py | 23 + blender/arm/logicnode/transform/LN_look_at.py | 47 + .../logicnode/transform/LN_rotate_object.py | 105 + .../transform/LN_separate_rotation.py | 60 + .../transform/LN_separate_transform.py | 45 + .../transform/LN_set_object_location.py | 28 + .../transform/LN_set_object_rotation.py | 76 + .../transform/LN_set_object_scale.py | 15 + .../transform/LN_set_object_transform.py | 14 + .../logicnode/transform/LN_transform_math.py | 13 + .../transform/LN_transform_to_vector.py | 40 + .../transform/LN_translate_object.py | 16 + .../transform/LN_translate_on_local_axis.py | 18 + .../LN_vector_to_object_orientation.py | 19 + .../LN_world_vector_to_local_space.py | 19 + blender/arm/logicnode/transform/__init__.py | 8 + blender/arm/logicnode/tree_variables.py | 557 +++ blender/arm/logicnode/variable/LN_boolean.py | 17 + blender/arm/logicnode/variable/LN_color.py | 16 + blender/arm/logicnode/variable/LN_dynamic.py | 11 + blender/arm/logicnode/variable/LN_float.py | 19 + blender/arm/logicnode/variable/LN_integer.py | 15 + blender/arm/logicnode/variable/LN_mask.py | 19 + .../arm/logicnode/variable/LN_retain_value.py | 30 + blender/arm/logicnode/variable/LN_rotation.py | 70 + blender/arm/logicnode/variable/LN_scene.py | 36 + .../arm/logicnode/variable/LN_set_variable.py | 21 + .../arm/logicnode/variable/LN_transform.py | 56 + blender/arm/logicnode/variable/LN_vector.py | 19 + blender/arm/logicnode/variable/__init__.py | 4 + blender/arm/make.py | 977 +++++ blender/arm/make_logic.py | 371 ++ blender/arm/make_renderpath.py | 454 ++ blender/arm/make_state.py | 20 + blender/arm/make_world.py | 392 ++ blender/arm/material/__init__.py | 1 + blender/arm/material/arm_nodes/__init__.py | 6 + blender/arm/material/arm_nodes/arm_nodes.py | 15 + .../arm_nodes/custom_particle_node.py | 192 + .../material/arm_nodes/shader_data_node.py | 107 + blender/arm/material/cycles.py | 967 +++++ blender/arm/material/cycles_functions.py | 517 +++ blender/arm/material/cycles_nodes/__init__.py | 5 + .../arm/material/cycles_nodes/nodes_color.py | 130 + .../material/cycles_nodes/nodes_converter.py | 397 ++ .../arm/material/cycles_nodes/nodes_input.py | 426 ++ .../arm/material/cycles_nodes/nodes_shader.py | 212 + .../material/cycles_nodes/nodes_texture.py | 584 +++ .../arm/material/cycles_nodes/nodes_vector.py | 205 + blender/arm/material/make.py | 170 + blender/arm/material/make_attrib.py | 100 + blender/arm/material/make_cluster.py | 92 + blender/arm/material/make_decal.py | 83 + blender/arm/material/make_depth.py | 203 + blender/arm/material/make_finalize.py | 147 + blender/arm/material/make_inst.py | 24 + blender/arm/material/make_mesh.py | 755 ++++ blender/arm/material/make_morph_target.py | 28 + blender/arm/material/make_overlay.py | 50 + blender/arm/material/make_particle.py | 99 + blender/arm/material/make_shader.py | 221 + blender/arm/material/make_skin.py | 30 + blender/arm/material/make_tess.py | 32 + blender/arm/material/make_transluc.py | 51 + blender/arm/material/make_voxel.py | 144 + blender/arm/material/mat_batch.py | 139 + blender/arm/material/mat_state.py | 40 + blender/arm/material/mat_utils.py | 107 + blender/arm/material/node_meta.py | 209 + blender/arm/material/parser_state.py | 127 + blender/arm/material/shader.py | 456 ++ blender/arm/node_utils.py | 250 ++ blender/arm/nodes_logic.py | 407 ++ blender/arm/nodes_material.py | 62 + blender/arm/profiler.py | 41 + blender/arm/props.py | 544 +++ blender/arm/props_bake.py | 401 ++ blender/arm/props_collision_filter_mask.py | 38 + blender/arm/props_exporter.py | 488 +++ blender/arm/props_lod.py | 157 + blender/arm/props_properties.py | 158 + blender/arm/props_renderpath.py | 757 ++++ blender/arm/props_tilesheet.py | 282 ++ blender/arm/props_traits.py | 1012 +++++ blender/arm/props_traits_props.py | 164 + blender/arm/props_ui.py | 2880 ++++++++++++ blender/arm/ui_icons.py | 55 + blender/arm/utils.py | 1212 ++++++ blender/arm/utils_vs.py | 327 ++ blender/arm/write_data.py | 838 ++++ blender/arm/write_probes.py | 478 ++ blender/data/arm_data.blend | Bin 0 -> 96980 bytes blender/data/haxelogic.py | 104 + blender/data/skydome.blend | Bin 0 -> 107524 bytes blender/start.py | 98 + changes.md | 6 + checkstyle.json | 253 ++ 1367 files changed, 104591 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/krom.yml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Assets/blue_noise64.png create mode 100644 Assets/brdf.png create mode 100644 Assets/clouds_base.raw create mode 100644 Assets/clouds_detail.raw create mode 100644 Assets/clouds_map.png create mode 100644 Assets/font_default.ttf create mode 100644 Assets/font_default_full.ttf create mode 100644 Assets/font_license_roboto.txt create mode 100644 Assets/font_mono.ttf create mode 100644 Assets/font_mono_full.ttf create mode 100644 Assets/hosek/hosek_radiance.hdr create mode 100644 Assets/hosek/hosek_radiance_0.hdr create mode 100644 Assets/hosek/hosek_radiance_1.hdr create mode 100644 Assets/hosek/hosek_radiance_2.hdr create mode 100644 Assets/hosek/hosek_radiance_3.hdr create mode 100644 Assets/hosek/hosek_radiance_4.hdr create mode 100644 Assets/hosek/hosek_radiance_5.hdr create mode 100644 Assets/hosek/hosek_radiance_6.hdr create mode 100644 Assets/hosek/hosek_radiance_7.hdr create mode 100644 Assets/ies/JellyFish.ies create mode 100644 Assets/ies/load_ies.py create mode 100644 Assets/noise256.png create mode 100644 Assets/noise64.png create mode 100644 Assets/noise8.png create mode 100644 Assets/smaa_area.png create mode 100644 Assets/smaa_search.png create mode 100644 Assets/water_base.png create mode 100644 Assets/water_detail.png create mode 100644 Assets/water_foam.png create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Shaders/blend_pass/blend_pass.json create mode 100644 Shaders/bloom_pass/bloom_pass.json create mode 100644 Shaders/bloom_pass/downsample_pass.frag.glsl create mode 100644 Shaders/bloom_pass/upsample_pass.frag.glsl create mode 100644 Shaders/blur_adaptive_pass/blur_adaptive_pass.frag.glsl create mode 100644 Shaders/blur_adaptive_pass/blur_adaptive_pass.json create mode 100644 Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.frag.glsl create mode 100644 Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.json create mode 100644 Shaders/blur_bilat_pass/blur_bilat_pass.frag.glsl create mode 100644 Shaders/blur_bilat_pass/blur_bilat_pass.json create mode 100644 Shaders/blur_edge_pass/blur_edge_pass.frag.glsl create mode 100644 Shaders/blur_edge_pass/blur_edge_pass.json create mode 100644 Shaders/blur_pass/blur_pass.frag.glsl create mode 100644 Shaders/blur_pass/blur_pass.json create mode 100644 Shaders/chromatic_aberration_pass/chromatic_aberration_pass.frag.glsl create mode 100644 Shaders/chromatic_aberration_pass/chromatic_aberration_pass.json create mode 100644 Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl create mode 100644 Shaders/clear_color_depth_pass/clear_color_depth_pass.json create mode 100644 Shaders/clear_color_pass/clear_color_pass.frag.glsl create mode 100644 Shaders/clear_color_pass/clear_color_pass.json create mode 100644 Shaders/clear_depth_pass/clear_depth_pass.frag.glsl create mode 100644 Shaders/clear_depth_pass/clear_depth_pass.json create mode 100644 Shaders/compositor_pass/compositor_pass.frag.glsl create mode 100644 Shaders/compositor_pass/compositor_pass.json create mode 100644 Shaders/compositor_pass/compositor_pass.vert.glsl create mode 100644 Shaders/copy_mrt2_pass/copy_mrt2_pass.frag.glsl create mode 100644 Shaders/copy_mrt2_pass/copy_mrt2_pass.json create mode 100644 Shaders/copy_mrt3_pass/copy_mrt3_pass.frag.glsl create mode 100644 Shaders/copy_mrt3_pass/copy_mrt3_pass.json create mode 100644 Shaders/copy_pass/copy_pass.json create mode 100644 Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl create mode 100644 Shaders/custom_mat_presets/custom_mat_deferred.vert.glsl create mode 100644 Shaders/custom_mat_presets/custom_mat_forward.frag.glsl create mode 100644 Shaders/custom_mat_presets/custom_mat_forward.vert.glsl create mode 100644 Shaders/debug_draw/line.frag.glsl create mode 100644 Shaders/debug_draw/line.vert.glsl create mode 100644 Shaders/debug_draw/line_deferred.frag.glsl create mode 100644 Shaders/deferred_light/deferred_light.frag.glsl create mode 100644 Shaders/deferred_light/deferred_light.json create mode 100644 Shaders/deferred_light_mobile/deferred_light.frag.glsl create mode 100644 Shaders/deferred_light_mobile/deferred_light_mobile.json create mode 100644 Shaders/deferred_light_solid/deferred_light.frag.glsl create mode 100644 Shaders/deferred_light_solid/deferred_light_solid.json create mode 100644 Shaders/downsample_depth/downsample_depth.frag.glsl create mode 100644 Shaders/downsample_depth/downsample_depth.json create mode 100644 Shaders/fxaa_pass/fxaa_pass.frag.glsl create mode 100644 Shaders/fxaa_pass/fxaa_pass.json create mode 100644 Shaders/histogram_pass/histogram_pass.frag.glsl create mode 100644 Shaders/histogram_pass/histogram_pass.json create mode 100644 Shaders/include/pass.vert.glsl create mode 100644 Shaders/include/pass_copy.frag.glsl create mode 100644 Shaders/include/pass_viewray.vert.glsl create mode 100644 Shaders/include/pass_viewray2.vert.glsl create mode 100644 Shaders/include/pass_volume.vert.glsl create mode 100644 Shaders/motion_blur_pass/motion_blur_pass.frag.glsl create mode 100644 Shaders/motion_blur_pass/motion_blur_pass.json create mode 100644 Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl create mode 100644 Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.json create mode 100644 Shaders/probe_cubemap/probe_cubemap.frag.glsl create mode 100644 Shaders/probe_cubemap/probe_cubemap.json create mode 100644 Shaders/probe_planar/probe_planar.frag.glsl create mode 100644 Shaders/probe_planar/probe_planar.json create mode 100644 Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl create mode 100644 Shaders/smaa_blend_weight/smaa_blend_weight.json create mode 100644 Shaders/smaa_blend_weight/smaa_blend_weight.vert.glsl create mode 100644 Shaders/smaa_edge_detect/smaa_edge_detect.frag.glsl create mode 100644 Shaders/smaa_edge_detect/smaa_edge_detect.json create mode 100644 Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl create mode 100644 Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl create mode 100644 Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.json create mode 100644 Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl create mode 100644 Shaders/ssao_pass/ssao_pass.frag.glsl create mode 100644 Shaders/ssao_pass/ssao_pass.json create mode 100644 Shaders/ssao_pass/ssgi_pass_.frag.glsl create mode 100644 Shaders/ssao_pass/ssgi_pass_.json create mode 100644 Shaders/ssgi_blur_pass/ssgi_blur_pass.frag.glsl create mode 100644 Shaders/ssgi_blur_pass/ssgi_blur_pass.json create mode 100644 Shaders/ssgi_pass/ssgi_pass.frag.glsl create mode 100644 Shaders/ssgi_pass/ssgi_pass.json create mode 100644 Shaders/ssr_pass/ssr_pass.frag.glsl create mode 100644 Shaders/ssr_pass/ssr_pass.json create mode 100644 Shaders/ssrefr_pass/ssrefr_pass.frag.glsl create mode 100755 Shaders/ssrefr_pass/ssrefr_pass.json create mode 100644 Shaders/sss_pass/sss_pass.frag.glsl create mode 100644 Shaders/sss_pass/sss_pass.json create mode 100644 Shaders/std/brdf.glsl create mode 100644 Shaders/std/clusters.glsl create mode 100644 Shaders/std/colorgrading.glsl create mode 100644 Shaders/std/conetrace.glsl create mode 100644 Shaders/std/denoise.glsl create mode 100644 Shaders/std/dof.glsl create mode 100644 Shaders/std/filters.glsl create mode 100644 Shaders/std/gbuffer.glsl create mode 100644 Shaders/std/ies.glsl create mode 100644 Shaders/std/light.glsl create mode 100644 Shaders/std/light_common.glsl create mode 100644 Shaders/std/light_mobile.glsl create mode 100644 Shaders/std/ltc.glsl create mode 100644 Shaders/std/mapping.glsl create mode 100644 Shaders/std/math.glsl create mode 100644 Shaders/std/morph_target.glsl create mode 100644 Shaders/std/normals.glsl create mode 100644 Shaders/std/resample.glsl create mode 100644 Shaders/std/shadows.glsl create mode 100644 Shaders/std/shirr.glsl create mode 100644 Shaders/std/skinning.glsl create mode 100644 Shaders/std/sky.glsl create mode 100644 Shaders/std/ssrs.glsl create mode 100644 Shaders/std/sss.glsl create mode 100644 Shaders/std/tonemap.glsl create mode 100644 Shaders/std/vr.glsl create mode 100644 Shaders/supersample_resolve/supersample_resolve.frag.glsl create mode 100644 Shaders/supersample_resolve/supersample_resolve.json create mode 100644 Shaders/taa_pass/taa_pass.frag.glsl create mode 100644 Shaders/taa_pass/taa_pass.json create mode 100644 Shaders/translucent_resolve/translucent_resolve.frag.glsl create mode 100644 Shaders/translucent_resolve/translucent_resolve.json create mode 100644 Shaders/volumetric_light/volumetric_light.frag.glsl create mode 100644 Shaders/volumetric_light/volumetric_light.json create mode 100644 Shaders/water_pass/water_pass.frag.glsl create mode 100644 Shaders/water_pass/water_pass.json create mode 100644 Sources/armory/data/Config.hx create mode 100644 Sources/armory/data/ConstData.hx create mode 100644 Sources/armory/import.hx create mode 100644 Sources/armory/logicnode/ActiveCameraNode.hx create mode 100644 Sources/armory/logicnode/ActiveSceneNode.hx create mode 100644 Sources/armory/logicnode/AddGroupNode.hx create mode 100644 Sources/armory/logicnode/AddObjectToGroupNode.hx create mode 100644 Sources/armory/logicnode/AddPhysicsConstraintNode.hx create mode 100644 Sources/armory/logicnode/AddRigidBodyNode.hx create mode 100644 Sources/armory/logicnode/AddTraitNode.hx create mode 100644 Sources/armory/logicnode/AlternateNode.hx create mode 100644 Sources/armory/logicnode/AnimActionNode.hx create mode 100644 Sources/armory/logicnode/AnimationStateNode.hx create mode 100644 Sources/armory/logicnode/AppendTransformNode.hx create mode 100644 Sources/armory/logicnode/ApplyForceAtLocationNode.hx create mode 100644 Sources/armory/logicnode/ApplyForceNode.hx create mode 100644 Sources/armory/logicnode/ApplyImpulseAtLocationNode.hx create mode 100644 Sources/armory/logicnode/ApplyImpulseNode.hx create mode 100644 Sources/armory/logicnode/ApplyTorqueImpulseNode.hx create mode 100644 Sources/armory/logicnode/ApplyTorqueNode.hx create mode 100644 Sources/armory/logicnode/ArrayAddNode.hx create mode 100644 Sources/armory/logicnode/ArrayBooleanNode.hx create mode 100644 Sources/armory/logicnode/ArrayColorNode.hx create mode 100644 Sources/armory/logicnode/ArrayCompareNode.hx create mode 100644 Sources/armory/logicnode/ArrayConcatNode.hx create mode 100644 Sources/armory/logicnode/ArrayCountNode.hx create mode 100644 Sources/armory/logicnode/ArrayDisplayNode.hx create mode 100644 Sources/armory/logicnode/ArrayDistinctNode.hx create mode 100644 Sources/armory/logicnode/ArrayFilterNode.hx create mode 100644 Sources/armory/logicnode/ArrayFloatNode.hx create mode 100644 Sources/armory/logicnode/ArrayGetNextNode.hx create mode 100644 Sources/armory/logicnode/ArrayGetNode.hx create mode 100644 Sources/armory/logicnode/ArrayGetPreviousNextNode.hx create mode 100644 Sources/armory/logicnode/ArrayInArrayNode.hx create mode 100644 Sources/armory/logicnode/ArrayIndexNode.hx create mode 100644 Sources/armory/logicnode/ArrayIntegerNode.hx create mode 100644 Sources/armory/logicnode/ArrayLengthNode.hx create mode 100644 Sources/armory/logicnode/ArrayLoopNode.hx create mode 100644 Sources/armory/logicnode/ArrayNode.hx create mode 100644 Sources/armory/logicnode/ArrayObjectNode.hx create mode 100644 Sources/armory/logicnode/ArrayPopNode.hx create mode 100644 Sources/armory/logicnode/ArrayRemoveNode.hx create mode 100644 Sources/armory/logicnode/ArrayRemoveValueNode.hx create mode 100644 Sources/armory/logicnode/ArrayResizeNode.hx create mode 100644 Sources/armory/logicnode/ArrayReverseNode.hx create mode 100644 Sources/armory/logicnode/ArraySampleNode.hx create mode 100644 Sources/armory/logicnode/ArraySetNode.hx create mode 100644 Sources/armory/logicnode/ArrayShiftNode.hx create mode 100644 Sources/armory/logicnode/ArrayShuffleNode.hx create mode 100644 Sources/armory/logicnode/ArraySliceNode.hx create mode 100644 Sources/armory/logicnode/ArraySortNode.hx create mode 100644 Sources/armory/logicnode/ArraySpliceNode.hx create mode 100644 Sources/armory/logicnode/ArrayStringNode.hx create mode 100644 Sources/armory/logicnode/ArrayVectorNode.hx create mode 100644 Sources/armory/logicnode/BitwiseMathNode.hx create mode 100644 Sources/armory/logicnode/BlendActionNode.hx create mode 100644 Sources/armory/logicnode/BloomGetNode.hx create mode 100644 Sources/armory/logicnode/BloomSetNode.hx create mode 100644 Sources/armory/logicnode/BoneFKNode.hx create mode 100644 Sources/armory/logicnode/BoneIKNode.hx create mode 100644 Sources/armory/logicnode/BooleanNode.hx create mode 100644 Sources/armory/logicnode/BranchNode.hx create mode 100644 Sources/armory/logicnode/CallFunctionNode.hx create mode 100644 Sources/armory/logicnode/CallGroupNode.hx create mode 100644 Sources/armory/logicnode/CallHaxeStaticNode.hx create mode 100644 Sources/armory/logicnode/CameraGetNode.hx create mode 100644 Sources/armory/logicnode/CameraSetNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetCheckboxNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetInputTextNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetLocationNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetPBNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetPositionNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetRotationNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetScaleNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetSliderNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetTextNode.hx create mode 100644 Sources/armory/logicnode/CanvasGetVisibleNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetAssetNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetCheckBoxNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetColorNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetInputTextFocusNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetInputTextNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetLocationNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetPBNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetProgressBarColorNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetRotationNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetScaleNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetSliderNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetTextColorNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetTextNode.hx create mode 100644 Sources/armory/logicnode/CanvasSetVisibleNode.hx create mode 100644 Sources/armory/logicnode/CaseIndexNode.hx create mode 100644 Sources/armory/logicnode/CaseStringNode.hx create mode 100644 Sources/armory/logicnode/CastPhysicsRayNode.hx create mode 100644 Sources/armory/logicnode/CastPhysicsRayOnNode.hx create mode 100644 Sources/armory/logicnode/ChromaticAberrationGetNode.hx create mode 100644 Sources/armory/logicnode/ChromaticAberrationSetNode.hx create mode 100644 Sources/armory/logicnode/ClampNode.hx create mode 100644 Sources/armory/logicnode/ClearConsoleNode.hx create mode 100644 Sources/armory/logicnode/ClearMapNode.hx create mode 100644 Sources/armory/logicnode/ClearParentNode.hx create mode 100644 Sources/armory/logicnode/ColorNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingGetGlobalNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingGetHighlightNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingGetMidtoneNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingGetShadowNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingSetGlobalNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingSetHighlightNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingSetMidtoneNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingSetShadowNode.hx create mode 100644 Sources/armory/logicnode/ColorgradingShadowNode.hx create mode 100644 Sources/armory/logicnode/CombineColorHSVNode.hx create mode 100644 Sources/armory/logicnode/CombineColorNode.hx create mode 100644 Sources/armory/logicnode/CompareNode.hx create mode 100644 Sources/armory/logicnode/ConcatenateStringNode.hx create mode 100644 Sources/armory/logicnode/ContainsStringNode.hx create mode 100644 Sources/armory/logicnode/CreateMapNode.hx create mode 100644 Sources/armory/logicnode/CreateRenderTargetNode.hx create mode 100644 Sources/armory/logicnode/CursorInRegionNode.hx create mode 100644 Sources/armory/logicnode/DefaultIfNullNode.hx create mode 100644 Sources/armory/logicnode/DegToRadNode.hx create mode 100644 Sources/armory/logicnode/DetectMobileBrowserNode.hx create mode 100644 Sources/armory/logicnode/DisplayInfoNode.hx create mode 100644 Sources/armory/logicnode/DrawArcNode.hx create mode 100644 Sources/armory/logicnode/DrawCameraNode.hx create mode 100644 Sources/armory/logicnode/DrawCameraTextureNode.hx create mode 100644 Sources/armory/logicnode/DrawCircleNode.hx create mode 100644 Sources/armory/logicnode/DrawCurveNode.hx create mode 100644 Sources/armory/logicnode/DrawEllipseNode.hx create mode 100644 Sources/armory/logicnode/DrawImageNode.hx create mode 100644 Sources/armory/logicnode/DrawImageSequenceNode.hx create mode 100644 Sources/armory/logicnode/DrawLineNode.hx create mode 100644 Sources/armory/logicnode/DrawPolygonNode.hx create mode 100644 Sources/armory/logicnode/DrawRectNode.hx create mode 100644 Sources/armory/logicnode/DrawStringNode.hx create mode 100644 Sources/armory/logicnode/DrawTextAreaStringNode.hx create mode 100644 Sources/armory/logicnode/DrawToMaterialImageNode.hx create mode 100644 Sources/armory/logicnode/DrawTriangleNode.hx create mode 100644 Sources/armory/logicnode/DynamicNode.hx create mode 100644 Sources/armory/logicnode/ExpressionNode.hx create mode 100644 Sources/armory/logicnode/FloatDeltaInterpolateNode.hx create mode 100644 Sources/armory/logicnode/FloatNode.hx create mode 100644 Sources/armory/logicnode/FunctionNode.hx create mode 100644 Sources/armory/logicnode/FunctionOutputNode.hx create mode 100644 Sources/armory/logicnode/GamepadCoordsNode.hx create mode 100644 Sources/armory/logicnode/GamepadSticksNode.hx create mode 100644 Sources/armory/logicnode/GateNode.hx create mode 100644 Sources/armory/logicnode/GetAgentDataNode.hx create mode 100644 Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx create mode 100644 Sources/armory/logicnode/GetBoneTransformNode.hx create mode 100644 Sources/armory/logicnode/GetCameraAspectNode.hx create mode 100644 Sources/armory/logicnode/GetCameraFovNode.hx create mode 100644 Sources/armory/logicnode/GetCameraScaleNode.hx create mode 100644 Sources/armory/logicnode/GetCameraStartEndNode.hx create mode 100644 Sources/armory/logicnode/GetCameraTypeNode.hx create mode 100644 Sources/armory/logicnode/GetChildNode.hx create mode 100644 Sources/armory/logicnode/GetChildrenNode.hx create mode 100644 Sources/armory/logicnode/GetContactsNode.hx create mode 100644 Sources/armory/logicnode/GetCursorLocationNode.hx create mode 100644 Sources/armory/logicnode/GetCursorStateNode.hx create mode 100644 Sources/armory/logicnode/GetDateTimeNode.hx create mode 100644 Sources/armory/logicnode/GetDebugConsoleSettings.hx create mode 100644 Sources/armory/logicnode/GetDimensionNode.hx create mode 100644 Sources/armory/logicnode/GetDistanceNode.hx create mode 100644 Sources/armory/logicnode/GetFPSNode.hx create mode 100644 Sources/armory/logicnode/GetFirstContactNode.hx create mode 100644 Sources/armory/logicnode/GetGamepadStartedNode.hx create mode 100644 Sources/armory/logicnode/GetGlobalCanvasFontSizeNode.hx create mode 100644 Sources/armory/logicnode/GetGlobalCanvasScaleNode.hx create mode 100644 Sources/armory/logicnode/GetGravityNode.hx create mode 100644 Sources/armory/logicnode/GetGroupNode.hx create mode 100644 Sources/armory/logicnode/GetHaxePropertyNode.hx create mode 100644 Sources/armory/logicnode/GetInputMapKeyNode.hx create mode 100644 Sources/armory/logicnode/GetKeyboardStartedNode.hx create mode 100644 Sources/armory/logicnode/GetLocationNode.hx create mode 100644 Sources/armory/logicnode/GetMapValueNode.hx create mode 100644 Sources/armory/logicnode/GetMaterialNode.hx create mode 100644 Sources/armory/logicnode/GetMeshNode.hx create mode 100644 Sources/armory/logicnode/GetMouseLockNode.hx create mode 100644 Sources/armory/logicnode/GetMouseMovementNode.hx create mode 100644 Sources/armory/logicnode/GetMouseStartedNode.hx create mode 100644 Sources/armory/logicnode/GetMouseVisibleNode.hx create mode 100644 Sources/armory/logicnode/GetNameNode.hx create mode 100644 Sources/armory/logicnode/GetObjectByUidNode.hx create mode 100644 Sources/armory/logicnode/GetObjectGroupNode.hx create mode 100644 Sources/armory/logicnode/GetObjectNode.hx create mode 100644 Sources/armory/logicnode/GetObjectOffscreenNode.hx create mode 100644 Sources/armory/logicnode/GetObjectTraitsNode.hx create mode 100644 Sources/armory/logicnode/GetParentNode.hx create mode 100644 Sources/armory/logicnode/GetPointVelocityNode.hx create mode 100644 Sources/armory/logicnode/GetPropertyNode.hx create mode 100644 Sources/armory/logicnode/GetRigidBodyDataNode.hx create mode 100644 Sources/armory/logicnode/GetRotationNode.hx create mode 100644 Sources/armory/logicnode/GetScaleNode.hx create mode 100644 Sources/armory/logicnode/GetSystemLanguage.hx create mode 100644 Sources/armory/logicnode/GetSystemName.hx create mode 100644 Sources/armory/logicnode/GetTilesheetStateNode.hx create mode 100644 Sources/armory/logicnode/GetTouchLocationNode.hx create mode 100644 Sources/armory/logicnode/GetTouchMovementNode.hx create mode 100644 Sources/armory/logicnode/GetTraitNameNode.hx create mode 100644 Sources/armory/logicnode/GetTraitNode.hx create mode 100644 Sources/armory/logicnode/GetTraitPausedNode.hx create mode 100644 Sources/armory/logicnode/GetTransformNode.hx create mode 100644 Sources/armory/logicnode/GetUidNode.hx create mode 100644 Sources/armory/logicnode/GetVelocityNode.hx create mode 100644 Sources/armory/logicnode/GetVisibleNode.hx create mode 100644 Sources/armory/logicnode/GetWorldNode.hx create mode 100644 Sources/armory/logicnode/GlobalObjectNode.hx create mode 100644 Sources/armory/logicnode/GoToLocationNode.hx create mode 100644 Sources/armory/logicnode/GroupInputsNode.hx create mode 100644 Sources/armory/logicnode/GroupNode.hx create mode 100644 Sources/armory/logicnode/GroupOutputsNode.hx create mode 100644 Sources/armory/logicnode/HasContactArrayNode.hx create mode 100644 Sources/armory/logicnode/HasContactNode.hx create mode 100644 Sources/armory/logicnode/IntFromBooleanNode.hx create mode 100644 Sources/armory/logicnode/IntegerNode.hx create mode 100644 Sources/armory/logicnode/InverseNode.hx create mode 100644 Sources/armory/logicnode/IsFalseNode.hx create mode 100644 Sources/armory/logicnode/IsNoneNode.hx create mode 100644 Sources/armory/logicnode/IsNotNoneNode.hx create mode 100644 Sources/armory/logicnode/IsRigidBodyActiveNode.hx create mode 100644 Sources/armory/logicnode/IsTrueNode.hx create mode 100644 Sources/armory/logicnode/KeyInterpolateNode.hx create mode 100644 Sources/armory/logicnode/LengthStringNode.hx create mode 100644 Sources/armory/logicnode/LenstextureGetNode.hx create mode 100644 Sources/armory/logicnode/LenstextureSetNode.hx create mode 100644 Sources/armory/logicnode/LetterboxGetNode.hx create mode 100644 Sources/armory/logicnode/LetterboxSetNode.hx create mode 100644 Sources/armory/logicnode/LoadUrlNode.hx create mode 100644 Sources/armory/logicnode/LogicNode.hx create mode 100644 Sources/armory/logicnode/LogicTree.hx create mode 100644 Sources/armory/logicnode/LookAtNode.hx create mode 100644 Sources/armory/logicnode/LoopBreakNode.hx create mode 100644 Sources/armory/logicnode/LoopContinueNode.hx create mode 100644 Sources/armory/logicnode/LoopNode.hx create mode 100644 Sources/armory/logicnode/MapKeyExistsNode.hx create mode 100644 Sources/armory/logicnode/MapLoopNode.hx create mode 100644 Sources/armory/logicnode/MapRangeNode.hx create mode 100644 Sources/armory/logicnode/MaskNode.hx create mode 100644 Sources/armory/logicnode/MaterialNode.hx create mode 100644 Sources/armory/logicnode/MathExpressionNode.hx create mode 100644 Sources/armory/logicnode/MathNode.hx create mode 100644 Sources/armory/logicnode/MatrixMathNode.hx create mode 100644 Sources/armory/logicnode/MergeNode.hx create mode 100644 Sources/armory/logicnode/MergedGamepadNode.hx create mode 100644 Sources/armory/logicnode/MergedKeyboardNode.hx create mode 100644 Sources/armory/logicnode/MergedMouseNode.hx create mode 100644 Sources/armory/logicnode/MergedSurfaceNode.hx create mode 100644 Sources/armory/logicnode/MergedVirtualButtonNode.hx create mode 100644 Sources/armory/logicnode/MeshNode.hx create mode 100644 Sources/armory/logicnode/MixNode.hx create mode 100644 Sources/armory/logicnode/MouseCoordsNode.hx create mode 100644 Sources/armory/logicnode/NavigableLocationNode.hx create mode 100644 Sources/armory/logicnode/NetworkClientNode.hx create mode 100644 Sources/armory/logicnode/NetworkCloseConnectionNode.hx create mode 100644 Sources/armory/logicnode/NetworkEventNode.hx create mode 100644 Sources/armory/logicnode/NetworkHostCloseClientNode.hx create mode 100644 Sources/armory/logicnode/NetworkHostGetIpNode.hx create mode 100644 Sources/armory/logicnode/NetworkHostNode.hx create mode 100644 Sources/armory/logicnode/NetworkHttpRequestNode.hx create mode 100644 Sources/armory/logicnode/NetworkMessageParserNode.hx create mode 100644 Sources/armory/logicnode/NetworkOpenConnectionNode.hx create mode 100644 Sources/armory/logicnode/NetworkSendMessageNode.hx create mode 100644 Sources/armory/logicnode/NoneNode.hx create mode 100644 Sources/armory/logicnode/NotNode.hx create mode 100644 Sources/armory/logicnode/NullNode.hx create mode 100644 Sources/armory/logicnode/ObjectNode.hx create mode 100644 Sources/armory/logicnode/OnActionMarkerNode.hx create mode 100644 Sources/armory/logicnode/OnApplicationStateNode.hx create mode 100644 Sources/armory/logicnode/OnCanvasElementNode.hx create mode 100644 Sources/armory/logicnode/OnContactArrayNode.hx create mode 100644 Sources/armory/logicnode/OnContactNode.hx create mode 100644 Sources/armory/logicnode/OnEventNode.hx create mode 100644 Sources/armory/logicnode/OnGamepadNode.hx create mode 100644 Sources/armory/logicnode/OnInitNode.hx create mode 100644 Sources/armory/logicnode/OnInputMapNode.hx create mode 100644 Sources/armory/logicnode/OnKeyboardNode.hx create mode 100644 Sources/armory/logicnode/OnMouseNode.hx create mode 100644 Sources/armory/logicnode/OnRender2DNode.hx create mode 100644 Sources/armory/logicnode/OnSurfaceNode.hx create mode 100644 Sources/armory/logicnode/OnSwipeNode.hx create mode 100644 Sources/armory/logicnode/OnTapScreen.hx create mode 100644 Sources/armory/logicnode/OnTimerNode.hx create mode 100644 Sources/armory/logicnode/OnUpdateNode.hx create mode 100644 Sources/armory/logicnode/OnVirtualButtonNode.hx create mode 100644 Sources/armory/logicnode/OnVolumeTriggerNode.hx create mode 100644 Sources/armory/logicnode/OncePerFrameNode.hx create mode 100644 Sources/armory/logicnode/ParseFloatNode.hx create mode 100644 Sources/armory/logicnode/ParseIntNode.hx create mode 100644 Sources/armory/logicnode/PauseActionNode.hx create mode 100644 Sources/armory/logicnode/PauseActiveCameraRenderNode.hx create mode 100644 Sources/armory/logicnode/PauseSoundNode.hx create mode 100644 Sources/armory/logicnode/PauseTilesheetNode.hx create mode 100644 Sources/armory/logicnode/PauseTraitNode.hx create mode 100644 Sources/armory/logicnode/PhysicsConstraintNode.hx create mode 100644 Sources/armory/logicnode/PhysicsConvexCastNode.hx create mode 100644 Sources/armory/logicnode/PhysicsConvexCastOnNode.hx create mode 100644 Sources/armory/logicnode/PickLocationNode.hx create mode 100644 Sources/armory/logicnode/PickObjectNode.hx create mode 100644 Sources/armory/logicnode/PlayActionFromNode.hx create mode 100644 Sources/armory/logicnode/PlayActionNode.hx create mode 100644 Sources/armory/logicnode/PlaySoundNode.hx create mode 100644 Sources/armory/logicnode/PlaySoundRawNode.hx create mode 100644 Sources/armory/logicnode/PlayTilesheetNode.hx create mode 100644 Sources/armory/logicnode/PrintNode.hx create mode 100644 Sources/armory/logicnode/PulseNode.hx create mode 100644 Sources/armory/logicnode/QuaternionMathNode.hx create mode 100644 Sources/armory/logicnode/QuaternionNode.hx create mode 100644 Sources/armory/logicnode/RadToDegNode.hx create mode 100644 Sources/armory/logicnode/RandomBooleanNode.hx create mode 100644 Sources/armory/logicnode/RandomChoiceNode.hx create mode 100644 Sources/armory/logicnode/RandomColorNode.hx create mode 100644 Sources/armory/logicnode/RandomFloatNode.hx create mode 100644 Sources/armory/logicnode/RandomIntegerNode.hx create mode 100644 Sources/armory/logicnode/RandomOutputNode.hx create mode 100644 Sources/armory/logicnode/RandomStringNode.hx create mode 100644 Sources/armory/logicnode/RandomVectorNode.hx create mode 100644 Sources/armory/logicnode/RaycastClosestObjectNode.hx create mode 100644 Sources/armory/logicnode/RaycastObjectNode.hx create mode 100644 Sources/armory/logicnode/ReadFileNode.hx create mode 100644 Sources/armory/logicnode/ReadJsonNode.hx create mode 100644 Sources/armory/logicnode/ReadStorageNode.hx create mode 100644 Sources/armory/logicnode/RemoveActiveSceneNode.hx create mode 100644 Sources/armory/logicnode/RemoveGroupNode.hx create mode 100644 Sources/armory/logicnode/RemoveInputMapKeyNode.hx create mode 100644 Sources/armory/logicnode/RemoveMapKeyNode.hx create mode 100644 Sources/armory/logicnode/RemoveObjectFromGroupNode.hx create mode 100644 Sources/armory/logicnode/RemoveObjectNode.hx create mode 100644 Sources/armory/logicnode/RemoveParentBoneNode.hx create mode 100644 Sources/armory/logicnode/RemovePhysicsNode.hx create mode 100644 Sources/armory/logicnode/RemoveTraitNode.hx create mode 100644 Sources/armory/logicnode/RemoveTraitObjectNode.hx create mode 100644 Sources/armory/logicnode/ResumeActionNode.hx create mode 100644 Sources/armory/logicnode/ResumeTilesheetNode.hx create mode 100644 Sources/armory/logicnode/ResumeTraitNode.hx create mode 100644 Sources/armory/logicnode/RetainValueNode.hx create mode 100644 Sources/armory/logicnode/RotateObjectAroundAxisNode.hx create mode 100644 Sources/armory/logicnode/RotateObjectNode.hx create mode 100644 Sources/armory/logicnode/RotateRenderTargetNode.hx create mode 100644 Sources/armory/logicnode/RotationMathNode.hx create mode 100644 Sources/armory/logicnode/RotationNode.hx create mode 100644 Sources/armory/logicnode/RpConfigNode.hx create mode 100644 Sources/armory/logicnode/RpMSAANode.hx create mode 100644 Sources/armory/logicnode/RpShadowQualityNode.hx create mode 100644 Sources/armory/logicnode/RpSuperSampleNode.hx create mode 100644 Sources/armory/logicnode/SSAOGetNode.hx create mode 100644 Sources/armory/logicnode/SSAOSetNode.hx create mode 100644 Sources/armory/logicnode/SSRGetNode.hx create mode 100644 Sources/armory/logicnode/SSRSetNode.hx create mode 100644 Sources/armory/logicnode/ScaleObjectNode.hx create mode 100644 Sources/armory/logicnode/SceneNode.hx create mode 100644 Sources/armory/logicnode/SceneRootNode.hx create mode 100644 Sources/armory/logicnode/ScreenToWorldSpaceNode.hx create mode 100644 Sources/armory/logicnode/ScriptNode.hx create mode 100644 Sources/armory/logicnode/SelectNode.hx create mode 100644 Sources/armory/logicnode/SelfNode.hx create mode 100644 Sources/armory/logicnode/SelfTraitNode.hx create mode 100644 Sources/armory/logicnode/SendEventNode.hx create mode 100644 Sources/armory/logicnode/SendGlobalEventNode.hx create mode 100644 Sources/armory/logicnode/SensorCoordsNode.hx create mode 100644 Sources/armory/logicnode/SeparateColorHSVNode.hx create mode 100644 Sources/armory/logicnode/SeparateColorNode.hx create mode 100644 Sources/armory/logicnode/SeparateQuaternionNode.hx create mode 100644 Sources/armory/logicnode/SeparateRotationNode.hx create mode 100644 Sources/armory/logicnode/SeparateTransformNode.hx create mode 100644 Sources/armory/logicnode/SeparateVectorNode.hx create mode 100644 Sources/armory/logicnode/SequenceNode.hx create mode 100644 Sources/armory/logicnode/SetActionPausedNode.hx create mode 100644 Sources/armory/logicnode/SetActionSpeedNode.hx create mode 100644 Sources/armory/logicnode/SetActivationStateNode.hx create mode 100644 Sources/armory/logicnode/SetAreaLightSizeNode.hx create mode 100644 Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx create mode 100644 Sources/armory/logicnode/SetCameraAspectNode.hx create mode 100644 Sources/armory/logicnode/SetCameraFovNode.hx create mode 100644 Sources/armory/logicnode/SetCameraNode.hx create mode 100644 Sources/armory/logicnode/SetCameraScaleNode.hx create mode 100644 Sources/armory/logicnode/SetCameraStartEndNode.hx create mode 100644 Sources/armory/logicnode/SetCameraTypeNode.hx create mode 100644 Sources/armory/logicnode/SetCursorStateNode.hx create mode 100644 Sources/armory/logicnode/SetDebugConsoleSettings.hx create mode 100644 Sources/armory/logicnode/SetFrictionNode.hx create mode 100644 Sources/armory/logicnode/SetGlobalCanvasFontSizeNode.hx create mode 100644 Sources/armory/logicnode/SetGlobalCanvasScaleNode.hx create mode 100644 Sources/armory/logicnode/SetGravityEnabledNode.hx create mode 100644 Sources/armory/logicnode/SetGravityNode.hx create mode 100644 Sources/armory/logicnode/SetHaxePropertyNode.hx create mode 100644 Sources/armory/logicnode/SetInputMapKeyNode.hx create mode 100644 Sources/armory/logicnode/SetLightColorNode.hx create mode 100644 Sources/armory/logicnode/SetLightStrengthNode.hx create mode 100644 Sources/armory/logicnode/SetLocationNode.hx create mode 100644 Sources/armory/logicnode/SetMapValueNode.hx create mode 100644 Sources/armory/logicnode/SetMaterialImageParamNode.hx create mode 100644 Sources/armory/logicnode/SetMaterialNode.hx create mode 100644 Sources/armory/logicnode/SetMaterialRgbParamNode.hx create mode 100644 Sources/armory/logicnode/SetMaterialSlotNode.hx create mode 100644 Sources/armory/logicnode/SetMaterialValueParamNode.hx create mode 100644 Sources/armory/logicnode/SetMeshNode.hx create mode 100644 Sources/armory/logicnode/SetMouseLockNode.hx create mode 100644 Sources/armory/logicnode/SetNameNode.hx create mode 100644 Sources/armory/logicnode/SetObjectShapeKeyNode.hx create mode 100644 Sources/armory/logicnode/SetParentBoneNode.hx create mode 100644 Sources/armory/logicnode/SetParentNode.hx create mode 100644 Sources/armory/logicnode/SetParticleSpeedNode.hx create mode 100644 Sources/armory/logicnode/SetPropertyNode.hx create mode 100644 Sources/armory/logicnode/SetRotationNode.hx create mode 100644 Sources/armory/logicnode/SetScaleNode.hx create mode 100644 Sources/armory/logicnode/SetSceneNode.hx create mode 100644 Sources/armory/logicnode/SetShaderUniformNode.hx create mode 100644 Sources/armory/logicnode/SetSpotLightBlendNode.hx create mode 100644 Sources/armory/logicnode/SetSpotLightSizeNode.hx create mode 100644 Sources/armory/logicnode/SetTilesheetPausedNode.hx create mode 100644 Sources/armory/logicnode/SetTimeScaleNode.hx create mode 100644 Sources/armory/logicnode/SetTraitPausedNode.hx create mode 100644 Sources/armory/logicnode/SetTransformNode.hx create mode 100644 Sources/armory/logicnode/SetVariableNode.hx create mode 100644 Sources/armory/logicnode/SetVelocityNode.hx create mode 100644 Sources/armory/logicnode/SetVibrateNode.hx create mode 100644 Sources/armory/logicnode/SetVisibleNode.hx create mode 100644 Sources/armory/logicnode/ShowMouseNode.hx create mode 100644 Sources/armory/logicnode/ShutdownNode.hx create mode 100644 Sources/armory/logicnode/SleepNode.hx create mode 100644 Sources/armory/logicnode/SpawnCollectionNode.hx create mode 100644 Sources/armory/logicnode/SpawnObjectByNameNode.hx create mode 100644 Sources/armory/logicnode/SpawnObjectNode.hx create mode 100644 Sources/armory/logicnode/SpawnSceneNode.hx create mode 100644 Sources/armory/logicnode/SplitStringNode.hx create mode 100644 Sources/armory/logicnode/StopAgentNode.hx create mode 100644 Sources/armory/logicnode/StopSoundNode.hx create mode 100644 Sources/armory/logicnode/StringNode.hx create mode 100644 Sources/armory/logicnode/StringReplaceNode.hx create mode 100644 Sources/armory/logicnode/SubStringNode.hx create mode 100644 Sources/armory/logicnode/SurfaceCoordsNode.hx create mode 100644 Sources/armory/logicnode/SwitchNode.hx create mode 100644 Sources/armory/logicnode/TimeNode.hx create mode 100644 Sources/armory/logicnode/TimerNode.hx create mode 100644 Sources/armory/logicnode/ToBoolNode.hx create mode 100644 Sources/armory/logicnode/TouchInRegionNode.hx create mode 100644 Sources/armory/logicnode/TraitNode.hx create mode 100644 Sources/armory/logicnode/TransformMathNode.hx create mode 100644 Sources/armory/logicnode/TransformNode.hx create mode 100644 Sources/armory/logicnode/TranslateObjectNode.hx create mode 100644 Sources/armory/logicnode/TranslateOnLocalAxisNode.hx create mode 100644 Sources/armory/logicnode/TweenFloatNode.hx create mode 100644 Sources/armory/logicnode/TweenRotationNode.hx create mode 100644 Sources/armory/logicnode/TweenTransformNode.hx create mode 100644 Sources/armory/logicnode/TweenVectorNode.hx create mode 100644 Sources/armory/logicnode/ValueChangedNode.hx create mode 100644 Sources/armory/logicnode/VectorClampToSizeNode.hx create mode 100644 Sources/armory/logicnode/VectorFromBooleanNode.hx create mode 100644 Sources/armory/logicnode/VectorFromTransformNode.hx create mode 100644 Sources/armory/logicnode/VectorMathNode.hx create mode 100644 Sources/armory/logicnode/VectorMixNode.hx create mode 100644 Sources/armory/logicnode/VectorMoveTowardsNode.hx create mode 100644 Sources/armory/logicnode/VectorNode.hx create mode 100644 Sources/armory/logicnode/VectorToObjectOrientationNode.hx create mode 100644 Sources/armory/logicnode/VolumeTriggerNode.hx create mode 100644 Sources/armory/logicnode/WhileNode.hx create mode 100644 Sources/armory/logicnode/WindowInfoNode.hx create mode 100644 Sources/armory/logicnode/WorldToScreenSpaceNode.hx create mode 100644 Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx create mode 100644 Sources/armory/logicnode/WriteFileNode.hx create mode 100644 Sources/armory/logicnode/WriteJsonNode.hx create mode 100644 Sources/armory/logicnode/WriteStorageNode.hx create mode 100644 Sources/armory/math/Helper.hx create mode 100644 Sources/armory/math/Rotator.hx create mode 100644 Sources/armory/network/Buffer.hx create mode 100644 Sources/armory/network/Connect.hx create mode 100644 Sources/armory/network/Handler.hx create mode 100644 Sources/armory/network/HttpHeader.hx create mode 100644 Sources/armory/network/HttpRequest.hx create mode 100644 Sources/armory/network/HttpResponse.hx create mode 100644 Sources/armory/network/LICENSE.md create mode 100644 Sources/armory/network/Log.hx create mode 100644 Sources/armory/network/OpCode.hx create mode 100644 Sources/armory/network/SecureSocketImpl.hx create mode 100644 Sources/armory/network/SocketImpl.hx create mode 100644 Sources/armory/network/State.hx create mode 100644 Sources/armory/network/Types.hx create mode 100644 Sources/armory/network/Utf8Encoder.hx create mode 100644 Sources/armory/network/Util.hx create mode 100644 Sources/armory/network/WebSocket.hx create mode 100644 Sources/armory/network/WebSocketCommon.hx create mode 100644 Sources/armory/network/WebSocketHandler.hx create mode 100644 Sources/armory/network/WebSocketSecureServer.hx create mode 100644 Sources/armory/network/WebSocketServer.hx create mode 100644 Sources/armory/network/nodejs/NodeSocket.hx create mode 100644 Sources/armory/network/nodejs/NodeSocketInput.hx create mode 100644 Sources/armory/network/nodejs/NodeSocketOutput.hx create mode 100644 Sources/armory/network/uuid/Uuid.hx create mode 100644 Sources/armory/object/TransformExtension.hx create mode 100644 Sources/armory/object/Uniforms.hx create mode 100644 Sources/armory/renderpath/Downsampler.hx create mode 100644 Sources/armory/renderpath/DynamicResolutionScale.hx create mode 100644 Sources/armory/renderpath/HosekWilkie.hx create mode 100644 Sources/armory/renderpath/HosekWilkieData.hx create mode 100644 Sources/armory/renderpath/Inc.hx create mode 100644 Sources/armory/renderpath/Nishita.hx create mode 100644 Sources/armory/renderpath/Postprocess.hx create mode 100644 Sources/armory/renderpath/RenderPathCreator.hx create mode 100644 Sources/armory/renderpath/RenderPathDeferred.hx create mode 100644 Sources/armory/renderpath/RenderPathForward.hx create mode 100644 Sources/armory/renderpath/RenderPathRaytracer.hx create mode 100644 Sources/armory/renderpath/RenderToTexture.hx create mode 100644 Sources/armory/renderpath/Upsampler.hx create mode 100644 Sources/armory/system/Assert.hx create mode 100644 Sources/armory/system/Event.hx create mode 100644 Sources/armory/system/FSM.hx create mode 100644 Sources/armory/system/InputMap.hx create mode 100644 Sources/armory/system/Logic.hx create mode 100644 Sources/armory/system/Starter.hx create mode 100644 Sources/armory/trait/ArcBall.hx create mode 100644 Sources/armory/trait/Character.hx create mode 100644 Sources/armory/trait/CustomParticle.hx create mode 100644 Sources/armory/trait/FirstPersonController.hx create mode 100644 Sources/armory/trait/FollowCamera.hx create mode 100644 Sources/armory/trait/NavAgent.hx create mode 100644 Sources/armory/trait/NavCrowd.hx create mode 100644 Sources/armory/trait/NavMesh.hx create mode 100644 Sources/armory/trait/PhysicsBreak.hx create mode 100644 Sources/armory/trait/PhysicsDrag.hx create mode 100644 Sources/armory/trait/SimpleMoveObject.hx create mode 100644 Sources/armory/trait/SimpleRotateObject.hx create mode 100644 Sources/armory/trait/SimpleScaleObject.hx create mode 100644 Sources/armory/trait/ThirdPersonController.hx create mode 100644 Sources/armory/trait/VehicleBody.hx create mode 100644 Sources/armory/trait/VirtualGamepad.hx create mode 100644 Sources/armory/trait/WalkNavigation.hx create mode 100644 Sources/armory/trait/internal/Bridge.hx create mode 100644 Sources/armory/trait/internal/CameraController.hx create mode 100644 Sources/armory/trait/internal/CanvasScript.hx create mode 100644 Sources/armory/trait/internal/DebugConsole.hx create mode 100644 Sources/armory/trait/internal/DebugDraw.hx create mode 100644 Sources/armory/trait/internal/LivePatch.hx create mode 100644 Sources/armory/trait/internal/LoadingScreen.hx create mode 100644 Sources/armory/trait/internal/MovieTexture.hx create mode 100644 Sources/armory/trait/internal/TerrainPhysics.hx create mode 100644 Sources/armory/trait/internal/UniformsManager.hx create mode 100644 Sources/armory/trait/internal/WasmScript.hx create mode 100644 Sources/armory/trait/internal/wasm_api.h create mode 100644 Sources/armory/trait/navigation/Navigation.hx create mode 100644 Sources/armory/trait/physics/KinematicCharacterController.hx create mode 100644 Sources/armory/trait/physics/PhysicsConstraint.hx create mode 100644 Sources/armory/trait/physics/PhysicsHook.hx create mode 100644 Sources/armory/trait/physics/PhysicsWorld.hx create mode 100644 Sources/armory/trait/physics/RigidBody.hx create mode 100644 Sources/armory/trait/physics/SoftBody.hx create mode 100644 Sources/armory/trait/physics/bullet/KinematicCharacterController.hx create mode 100644 Sources/armory/trait/physics/bullet/PhysicsConstraint.hx create mode 100644 Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx create mode 100644 Sources/armory/trait/physics/bullet/PhysicsHook.hx create mode 100644 Sources/armory/trait/physics/bullet/PhysicsWorld.hx create mode 100644 Sources/armory/trait/physics/bullet/RigidBody.hx create mode 100644 Sources/armory/trait/physics/bullet/SoftBody.hx create mode 100644 Sources/armory/ui/Canvas.hx create mode 100644 Sources/armory/ui/Ext.hx create mode 100644 Sources/armory/ui/Popup.hx create mode 100644 Sources/armory/ui/Themes.hx create mode 100644 blender/__init__.py create mode 100644 blender/arm/LICENSE.md create mode 100644 blender/arm/__init__.py create mode 100644 blender/arm/api.py create mode 100644 blender/arm/assets.py create mode 100644 blender/arm/custom_icons/bundle.png create mode 100644 blender/arm/custom_icons/haxe.png create mode 100644 blender/arm/custom_icons/wasm.png create mode 100644 blender/arm/exporter.py create mode 100644 blender/arm/exporter_opt.py create mode 100644 blender/arm/handlers.py create mode 100644 blender/arm/keymap.py create mode 100644 blender/arm/lib/__init__.py create mode 100644 blender/arm/lib/armpack.py create mode 100644 blender/arm/lib/lz4.py create mode 100644 blender/arm/lib/make_datas.py create mode 100644 blender/arm/lib/server.py create mode 100644 blender/arm/lightmapper/__init__.py create mode 100644 blender/arm/lightmapper/assets/TLM_Overlay.png create mode 100644 blender/arm/lightmapper/assets/dash.ogg create mode 100644 blender/arm/lightmapper/assets/gentle.ogg create mode 100644 blender/arm/lightmapper/assets/noot.ogg create mode 100644 blender/arm/lightmapper/assets/pingping.ogg create mode 100644 blender/arm/lightmapper/assets/sound.ogg create mode 100644 blender/arm/lightmapper/assets/tlm_data.blend create mode 100644 blender/arm/lightmapper/icons/bake.png create mode 100644 blender/arm/lightmapper/icons/clean.png create mode 100644 blender/arm/lightmapper/icons/explore.png create mode 100644 blender/arm/lightmapper/keymap/__init__.py create mode 100644 blender/arm/lightmapper/keymap/keymap.py create mode 100644 blender/arm/lightmapper/network/client.py create mode 100644 blender/arm/lightmapper/network/server.py create mode 100644 blender/arm/lightmapper/operators/__init__.py create mode 100644 blender/arm/lightmapper/operators/imagetools.py create mode 100644 blender/arm/lightmapper/operators/installopencv.py create mode 100644 blender/arm/lightmapper/operators/tlm.py create mode 100644 blender/arm/lightmapper/panels/__init__.py create mode 100644 blender/arm/lightmapper/panels/image.py create mode 100644 blender/arm/lightmapper/panels/light.py create mode 100644 blender/arm/lightmapper/panels/object.py create mode 100644 blender/arm/lightmapper/panels/scene.py create mode 100644 blender/arm/lightmapper/panels/world.py create mode 100644 blender/arm/lightmapper/preferences/__init__.py create mode 100644 blender/arm/lightmapper/preferences/addon_preferences.py create mode 100644 blender/arm/lightmapper/properties/__init__.py create mode 100644 blender/arm/lightmapper/properties/atlas.py create mode 100644 blender/arm/lightmapper/properties/denoiser/integrated.py create mode 100644 blender/arm/lightmapper/properties/denoiser/oidn.py create mode 100644 blender/arm/lightmapper/properties/denoiser/optix.py create mode 100644 blender/arm/lightmapper/properties/filtering.py create mode 100644 blender/arm/lightmapper/properties/image.py create mode 100644 blender/arm/lightmapper/properties/object.py create mode 100644 blender/arm/lightmapper/properties/renderer/cycles.py create mode 100644 blender/arm/lightmapper/properties/renderer/luxcorerender.py create mode 100644 blender/arm/lightmapper/properties/renderer/octanerender.py create mode 100644 blender/arm/lightmapper/properties/renderer/radeonrays.py create mode 100644 blender/arm/lightmapper/properties/scene.py create mode 100644 blender/arm/lightmapper/utility/__init__.py create mode 100644 blender/arm/lightmapper/utility/build.py create mode 100644 blender/arm/lightmapper/utility/cycles/ao.py create mode 100644 blender/arm/lightmapper/utility/cycles/cache.py create mode 100644 blender/arm/lightmapper/utility/cycles/indirect.py create mode 100644 blender/arm/lightmapper/utility/cycles/lightmap.py create mode 100644 blender/arm/lightmapper/utility/cycles/nodes.py create mode 100644 blender/arm/lightmapper/utility/cycles/prepare.py create mode 100644 blender/arm/lightmapper/utility/denoiser/integrated.py create mode 100644 blender/arm/lightmapper/utility/denoiser/oidn.py create mode 100644 blender/arm/lightmapper/utility/denoiser/optix.py create mode 100644 blender/arm/lightmapper/utility/encoding.py create mode 100644 blender/arm/lightmapper/utility/filtering/numpy.py create mode 100644 blender/arm/lightmapper/utility/filtering/opencv.py create mode 100644 blender/arm/lightmapper/utility/filtering/shader.py create mode 100644 blender/arm/lightmapper/utility/gui/Viewport.py create mode 100644 blender/arm/lightmapper/utility/icon.py create mode 100644 blender/arm/lightmapper/utility/log.py create mode 100644 blender/arm/lightmapper/utility/luxcore/setup.py create mode 100644 blender/arm/lightmapper/utility/octane/configure.py create mode 100644 blender/arm/lightmapper/utility/octane/lightmap2.py create mode 100644 blender/arm/lightmapper/utility/pack.py create mode 100644 blender/arm/lightmapper/utility/preconfiguration/object.py create mode 100644 blender/arm/lightmapper/utility/rectpack/__init__.py create mode 100644 blender/arm/lightmapper/utility/rectpack/enclose.py create mode 100644 blender/arm/lightmapper/utility/rectpack/geometry.py create mode 100644 blender/arm/lightmapper/utility/rectpack/guillotine.py create mode 100644 blender/arm/lightmapper/utility/rectpack/maxrects.py create mode 100644 blender/arm/lightmapper/utility/rectpack/pack_algo.py create mode 100644 blender/arm/lightmapper/utility/rectpack/packer.py create mode 100644 blender/arm/lightmapper/utility/rectpack/skyline.py create mode 100644 blender/arm/lightmapper/utility/rectpack/waste.py create mode 100644 blender/arm/lightmapper/utility/utility.py create mode 100644 blender/arm/live_patch.py create mode 100644 blender/arm/log.py create mode 100644 blender/arm/logicnode/__init__.py create mode 100644 blender/arm/logicnode/animation/LN_action.py create mode 100644 blender/arm/logicnode/animation/LN_blend_action.py create mode 100644 blender/arm/logicnode/animation/LN_bone_fk.py create mode 100644 blender/arm/logicnode/animation/LN_bone_ik.py create mode 100644 blender/arm/logicnode/animation/LN_get_action_state.py create mode 100644 blender/arm/logicnode/animation/LN_get_bone_fk_ik_only.py create mode 100644 blender/arm/logicnode/animation/LN_get_bone_transform.py create mode 100644 blender/arm/logicnode/animation/LN_get_tilesheet_state.py create mode 100644 blender/arm/logicnode/animation/LN_on_action_marker.py create mode 100644 blender/arm/logicnode/animation/LN_play_action_from.py create mode 100644 blender/arm/logicnode/animation/LN_play_tilesheet.py create mode 100644 blender/arm/logicnode/animation/LN_remove_parent_bone.py create mode 100644 blender/arm/logicnode/animation/LN_set_action_paused.py create mode 100644 blender/arm/logicnode/animation/LN_set_action_speed.py create mode 100644 blender/arm/logicnode/animation/LN_set_bone_fk_ik_only.py create mode 100644 blender/arm/logicnode/animation/LN_set_parent_bone.py create mode 100644 blender/arm/logicnode/animation/LN_set_particle_speed.py create mode 100644 blender/arm/logicnode/animation/LN_set_tilesheet_paused.py create mode 100644 blender/arm/logicnode/animation/__init__.py create mode 100644 blender/arm/logicnode/arm_node_group.py create mode 100644 blender/arm/logicnode/arm_nodes.py create mode 100644 blender/arm/logicnode/arm_props.py create mode 100644 blender/arm/logicnode/arm_sockets.py create mode 100644 blender/arm/logicnode/array/LN_array.py create mode 100644 blender/arm/logicnode/array/LN_array_add.py create mode 100644 blender/arm/logicnode/array/LN_array_boolean.py create mode 100644 blender/arm/logicnode/array/LN_array_color.py create mode 100644 blender/arm/logicnode/array/LN_array_compare.py create mode 100644 blender/arm/logicnode/array/LN_array_concat.py create mode 100644 blender/arm/logicnode/array/LN_array_contains.py create mode 100644 blender/arm/logicnode/array/LN_array_count.py create mode 100644 blender/arm/logicnode/array/LN_array_display.py create mode 100644 blender/arm/logicnode/array/LN_array_distinct.py create mode 100644 blender/arm/logicnode/array/LN_array_filter.py create mode 100644 blender/arm/logicnode/array/LN_array_float.py create mode 100644 blender/arm/logicnode/array/LN_array_get.py create mode 100644 blender/arm/logicnode/array/LN_array_get_PreviousNext.py create mode 100644 blender/arm/logicnode/array/LN_array_get_next.py create mode 100644 blender/arm/logicnode/array/LN_array_index.py create mode 100644 blender/arm/logicnode/array/LN_array_integer.py create mode 100644 blender/arm/logicnode/array/LN_array_length.py create mode 100644 blender/arm/logicnode/array/LN_array_loop_node.py create mode 100644 blender/arm/logicnode/array/LN_array_object.py create mode 100644 blender/arm/logicnode/array/LN_array_pop.py create mode 100644 blender/arm/logicnode/array/LN_array_remove_by_index.py create mode 100644 blender/arm/logicnode/array/LN_array_remove_by_value.py create mode 100644 blender/arm/logicnode/array/LN_array_resize.py create mode 100644 blender/arm/logicnode/array/LN_array_reverse.py create mode 100644 blender/arm/logicnode/array/LN_array_sample.py create mode 100644 blender/arm/logicnode/array/LN_array_set.py create mode 100644 blender/arm/logicnode/array/LN_array_shift.py create mode 100644 blender/arm/logicnode/array/LN_array_shuffle.py create mode 100644 blender/arm/logicnode/array/LN_array_slice.py create mode 100644 blender/arm/logicnode/array/LN_array_sort.py create mode 100644 blender/arm/logicnode/array/LN_array_splice.py create mode 100644 blender/arm/logicnode/array/LN_array_string.py create mode 100644 blender/arm/logicnode/array/LN_array_vector.py create mode 100644 blender/arm/logicnode/array/__init__.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_active.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_aspect.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_fov.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_scale.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_start_end.py create mode 100644 blender/arm/logicnode/camera/LN_get_camera_type.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_active.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_aspect.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_fov.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_scale.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_start_end.py create mode 100644 blender/arm/logicnode/camera/LN_set_camera_type.py create mode 100644 blender/arm/logicnode/camera/__init__.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_input_text.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_location.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_position.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_rotation.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_scale.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_slider.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_text.py create mode 100644 blender/arm/logicnode/canvas/LN_get_canvas_visible.py create mode 100644 blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py create mode 100644 blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py create mode 100644 blender/arm/logicnode/canvas/LN_on_canvas_element.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_asset.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_color.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_input_text.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_location.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_rotation.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_scale.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_slider.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_text.py create mode 100644 blender/arm/logicnode/canvas/LN_set_canvas_visible.py create mode 100644 blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py create mode 100644 blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py create mode 100644 blender/arm/logicnode/canvas/__init__.py create mode 100644 blender/arm/logicnode/deprecated/LN_get_mouse_lock.py create mode 100644 blender/arm/logicnode/deprecated/LN_get_mouse_visible.py create mode 100644 blender/arm/logicnode/deprecated/LN_group_nodes.py create mode 100644 blender/arm/logicnode/deprecated/LN_mouse_coords.py create mode 100644 blender/arm/logicnode/deprecated/LN_on_gamepad.py create mode 100644 blender/arm/logicnode/deprecated/LN_on_keyboard.py create mode 100644 blender/arm/logicnode/deprecated/LN_on_mouse.py create mode 100644 blender/arm/logicnode/deprecated/LN_on_surface.py create mode 100644 blender/arm/logicnode/deprecated/LN_on_virtual_button.py create mode 100644 blender/arm/logicnode/deprecated/LN_pause_action.py create mode 100644 blender/arm/logicnode/deprecated/LN_pause_tilesheet.py create mode 100644 blender/arm/logicnode/deprecated/LN_pause_trait.py create mode 100644 blender/arm/logicnode/deprecated/LN_play_action.py create mode 100644 blender/arm/logicnode/deprecated/LN_quaternion.py create mode 100644 blender/arm/logicnode/deprecated/LN_resume_action.py create mode 100644 blender/arm/logicnode/deprecated/LN_resume_tilesheet.py create mode 100644 blender/arm/logicnode/deprecated/LN_resume_trait.py create mode 100644 blender/arm/logicnode/deprecated/LN_rotate_object_around_axis.py create mode 100644 blender/arm/logicnode/deprecated/LN_scale_object.py create mode 100644 blender/arm/logicnode/deprecated/LN_separate_quaternion.py create mode 100644 blender/arm/logicnode/deprecated/LN_set_canvas_progress_bar_color.py create mode 100644 blender/arm/logicnode/deprecated/LN_set_canvas_text_color.py create mode 100644 blender/arm/logicnode/deprecated/LN_set_mouse_lock.py create mode 100644 blender/arm/logicnode/deprecated/LN_set_mouse_visible.py create mode 100644 blender/arm/logicnode/deprecated/LN_set_object_material.py create mode 100644 blender/arm/logicnode/deprecated/LN_surface_coords.py create mode 100644 blender/arm/logicnode/deprecated/__init__.py create mode 100644 blender/arm/logicnode/draw/LN_draw_Text_Area_string.py create mode 100644 blender/arm/logicnode/draw/LN_draw_arc.py create mode 100644 blender/arm/logicnode/draw/LN_draw_camera.py create mode 100644 blender/arm/logicnode/draw/LN_draw_camera_texture.py create mode 100644 blender/arm/logicnode/draw/LN_draw_circle.py create mode 100644 blender/arm/logicnode/draw/LN_draw_curve.py create mode 100644 blender/arm/logicnode/draw/LN_draw_ellipse.py create mode 100644 blender/arm/logicnode/draw/LN_draw_image.py create mode 100644 blender/arm/logicnode/draw/LN_draw_image_sequence.py create mode 100644 blender/arm/logicnode/draw/LN_draw_line.py create mode 100644 blender/arm/logicnode/draw/LN_draw_polygon.py create mode 100644 blender/arm/logicnode/draw/LN_draw_rect.py create mode 100644 blender/arm/logicnode/draw/LN_draw_string.py create mode 100644 blender/arm/logicnode/draw/LN_draw_to_material_image.py create mode 100644 blender/arm/logicnode/draw/LN_draw_triangle.py create mode 100644 blender/arm/logicnode/draw/__init__.py create mode 100644 blender/arm/logicnode/event/LN_on_application_state.py create mode 100644 blender/arm/logicnode/event/LN_on_event.py create mode 100644 blender/arm/logicnode/event/LN_on_init.py create mode 100644 blender/arm/logicnode/event/LN_on_render2d.py create mode 100644 blender/arm/logicnode/event/LN_on_timer.py create mode 100644 blender/arm/logicnode/event/LN_on_update.py create mode 100644 blender/arm/logicnode/event/LN_send_event_to_object.py create mode 100644 blender/arm/logicnode/event/LN_send_global_event.py create mode 100644 blender/arm/logicnode/event/__init__.py create mode 100644 blender/arm/logicnode/input/LN_cursor_in_region.py create mode 100644 blender/arm/logicnode/input/LN_gamepad.py create mode 100644 blender/arm/logicnode/input/LN_gamepad_coords.py create mode 100644 blender/arm/logicnode/input/LN_gamepad_sticks.py create mode 100644 blender/arm/logicnode/input/LN_get_cursor_location.py create mode 100644 blender/arm/logicnode/input/LN_get_cursor_state.py create mode 100644 blender/arm/logicnode/input/LN_get_gamepad_started.py create mode 100644 blender/arm/logicnode/input/LN_get_input_map_key.py create mode 100644 blender/arm/logicnode/input/LN_get_keyboard_started.py create mode 100644 blender/arm/logicnode/input/LN_get_mouse_movement.py create mode 100644 blender/arm/logicnode/input/LN_get_mouse_started.py create mode 100644 blender/arm/logicnode/input/LN_get_touch_location.py create mode 100644 blender/arm/logicnode/input/LN_get_touch_movement.py create mode 100644 blender/arm/logicnode/input/LN_keyboard.py create mode 100644 blender/arm/logicnode/input/LN_mouse.py create mode 100644 blender/arm/logicnode/input/LN_on_input_map.py create mode 100644 blender/arm/logicnode/input/LN_on_swipe.py create mode 100644 blender/arm/logicnode/input/LN_on_tap_screen.py create mode 100644 blender/arm/logicnode/input/LN_remove_input_map_key.py create mode 100644 blender/arm/logicnode/input/LN_sensor_coords.py create mode 100644 blender/arm/logicnode/input/LN_set_cursor_state.py create mode 100644 blender/arm/logicnode/input/LN_set_input_map_key.py create mode 100644 blender/arm/logicnode/input/LN_touch.py create mode 100644 blender/arm/logicnode/input/LN_touch_in_region.py create mode 100644 blender/arm/logicnode/input/LN_virtual_button.py create mode 100644 blender/arm/logicnode/input/__init__.py create mode 100644 blender/arm/logicnode/light/LN_set_area_light_size.py create mode 100644 blender/arm/logicnode/light/LN_set_light_color.py create mode 100644 blender/arm/logicnode/light/LN_set_light_strength.py create mode 100644 blender/arm/logicnode/light/LN_set_spot_light_blend.py create mode 100644 blender/arm/logicnode/light/LN_set_spot_light_size.py create mode 100644 blender/arm/logicnode/light/__init__.py create mode 100644 blender/arm/logicnode/logic/LN_alternate_output.py create mode 100644 blender/arm/logicnode/logic/LN_branch.py create mode 100644 blender/arm/logicnode/logic/LN_call_function.py create mode 100644 blender/arm/logicnode/logic/LN_case_index.py create mode 100644 blender/arm/logicnode/logic/LN_function.py create mode 100644 blender/arm/logicnode/logic/LN_function_output.py create mode 100644 blender/arm/logicnode/logic/LN_gate.py create mode 100644 blender/arm/logicnode/logic/LN_invert_boolean.py create mode 100644 blender/arm/logicnode/logic/LN_invert_output.py create mode 100644 blender/arm/logicnode/logic/LN_is_false.py create mode 100644 blender/arm/logicnode/logic/LN_is_not_null.py create mode 100644 blender/arm/logicnode/logic/LN_is_null.py create mode 100644 blender/arm/logicnode/logic/LN_is_true.py create mode 100644 blender/arm/logicnode/logic/LN_loop.py create mode 100644 blender/arm/logicnode/logic/LN_loop_break.py create mode 100644 blender/arm/logicnode/logic/LN_loop_continue.py create mode 100644 blender/arm/logicnode/logic/LN_merge.py create mode 100644 blender/arm/logicnode/logic/LN_null.py create mode 100644 blender/arm/logicnode/logic/LN_once_per_frame.py create mode 100644 blender/arm/logicnode/logic/LN_output_sequence.py create mode 100644 blender/arm/logicnode/logic/LN_output_to_boolean.py create mode 100644 blender/arm/logicnode/logic/LN_pulse.py create mode 100644 blender/arm/logicnode/logic/LN_select.py create mode 100644 blender/arm/logicnode/logic/LN_switch_output.py create mode 100644 blender/arm/logicnode/logic/LN_value_changed.py create mode 100644 blender/arm/logicnode/logic/LN_while_true.py create mode 100644 blender/arm/logicnode/logic/__init__.py create mode 100644 blender/arm/logicnode/map/LN_clear_map.py create mode 100644 blender/arm/logicnode/map/LN_create_map.py create mode 100644 blender/arm/logicnode/map/LN_get_map_value.py create mode 100644 blender/arm/logicnode/map/LN_map_key_exists.py create mode 100644 blender/arm/logicnode/map/LN_map_loop.py create mode 100644 blender/arm/logicnode/map/LN_remove_map_key.py create mode 100644 blender/arm/logicnode/map/LN_set_map_value.py create mode 100644 blender/arm/logicnode/map/__init__.py create mode 100644 blender/arm/logicnode/material/LN_get_object_material.py create mode 100644 blender/arm/logicnode/material/LN_material.py create mode 100644 blender/arm/logicnode/material/LN_set_material_image_param.py create mode 100644 blender/arm/logicnode/material/LN_set_material_rgb_param.py create mode 100644 blender/arm/logicnode/material/LN_set_material_value_param.py create mode 100644 blender/arm/logicnode/material/LN_set_object_material_slot.py create mode 100644 blender/arm/logicnode/material/__init__.py create mode 100644 blender/arm/logicnode/math/LN_bitwise_math.py create mode 100644 blender/arm/logicnode/math/LN_clamp.py create mode 100644 blender/arm/logicnode/math/LN_combine_hsv.py create mode 100644 blender/arm/logicnode/math/LN_combine_rgb.py create mode 100644 blender/arm/logicnode/math/LN_compare.py create mode 100644 blender/arm/logicnode/math/LN_deg_to_rad.py create mode 100644 blender/arm/logicnode/math/LN_float_delta_interpolate.py create mode 100644 blender/arm/logicnode/math/LN_key_interpolate.py create mode 100644 blender/arm/logicnode/math/LN_map_range.py create mode 100644 blender/arm/logicnode/math/LN_math.py create mode 100644 blender/arm/logicnode/math/LN_math_expression.py create mode 100644 blender/arm/logicnode/math/LN_matrix_math.py create mode 100644 blender/arm/logicnode/math/LN_mix.py create mode 100644 blender/arm/logicnode/math/LN_mix_vector.py create mode 100644 blender/arm/logicnode/math/LN_quaternion_math.py create mode 100644 blender/arm/logicnode/math/LN_rad_to_deg.py create mode 100644 blender/arm/logicnode/math/LN_rotation_math.py create mode 100644 blender/arm/logicnode/math/LN_screen_to_world_space.py create mode 100644 blender/arm/logicnode/math/LN_separate_hsv.py create mode 100644 blender/arm/logicnode/math/LN_separate_rgb.py create mode 100644 blender/arm/logicnode/math/LN_separate_xyz.py create mode 100644 blender/arm/logicnode/math/LN_tween_float.py create mode 100644 blender/arm/logicnode/math/LN_tween_rotation.py create mode 100644 blender/arm/logicnode/math/LN_tween_transform.py create mode 100644 blender/arm/logicnode/math/LN_tween_vector.py create mode 100644 blender/arm/logicnode/math/LN_vector_clamp.py create mode 100644 blender/arm/logicnode/math/LN_vector_math.py create mode 100644 blender/arm/logicnode/math/LN_vector_move_towards.py create mode 100644 blender/arm/logicnode/math/LN_world_to_screen_space.py create mode 100644 blender/arm/logicnode/math/__init__.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_boolean_to_int.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_boolean_to_vector.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_call_group.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_default_if_null.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_get_application_time.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_get_display_resolution.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_get_fps.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_get_window_resolution.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_group_input.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_group_output.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_set_debug_console_settings.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_set_time_scale.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_sleep.py create mode 100644 blender/arm/logicnode/miscellaneous/LN_timer.py create mode 100644 blender/arm/logicnode/miscellaneous/__init__.py create mode 100644 blender/arm/logicnode/native/LN_call_haxe_static.py create mode 100644 blender/arm/logicnode/native/LN_clear_console.py create mode 100644 blender/arm/logicnode/native/LN_detect_mobile_browser.py create mode 100644 blender/arm/logicnode/native/LN_expression.py create mode 100644 blender/arm/logicnode/native/LN_get_date_time.py create mode 100644 blender/arm/logicnode/native/LN_get_haxe_property.py create mode 100644 blender/arm/logicnode/native/LN_get_system_language.py create mode 100644 blender/arm/logicnode/native/LN_get_system_name.py create mode 100644 blender/arm/logicnode/native/LN_loadUrl.py create mode 100644 blender/arm/logicnode/native/LN_print.py create mode 100644 blender/arm/logicnode/native/LN_read_file.py create mode 100644 blender/arm/logicnode/native/LN_read_json.py create mode 100644 blender/arm/logicnode/native/LN_read_storage.py create mode 100644 blender/arm/logicnode/native/LN_script.py create mode 100644 blender/arm/logicnode/native/LN_set_haxe_property.py create mode 100644 blender/arm/logicnode/native/LN_set_vibrate.py create mode 100644 blender/arm/logicnode/native/LN_shutdown.py create mode 100644 blender/arm/logicnode/native/LN_write_file.py create mode 100644 blender/arm/logicnode/native/LN_write_json.py create mode 100644 blender/arm/logicnode/native/LN_write_storage.py create mode 100644 blender/arm/logicnode/native/__init__.py create mode 100644 blender/arm/logicnode/navmesh/LN_get_agent_data.py create mode 100644 blender/arm/logicnode/navmesh/LN_go_to_location.py create mode 100644 blender/arm/logicnode/navmesh/LN_navigable_location.py create mode 100644 blender/arm/logicnode/navmesh/LN_pick_navmesh_location.py create mode 100644 blender/arm/logicnode/navmesh/LN_stop_agent.py create mode 100644 blender/arm/logicnode/navmesh/__init__.py create mode 100644 blender/arm/logicnode/network/LN_network_client.py create mode 100644 blender/arm/logicnode/network/LN_network_close_connection.py create mode 100644 blender/arm/logicnode/network/LN_network_event.py create mode 100644 blender/arm/logicnode/network/LN_network_host.py create mode 100644 blender/arm/logicnode/network/LN_network_host_close_client.py create mode 100644 blender/arm/logicnode/network/LN_network_host_get_ip.py create mode 100644 blender/arm/logicnode/network/LN_network_http_request.py create mode 100644 blender/arm/logicnode/network/LN_network_message_parser.py create mode 100644 blender/arm/logicnode/network/LN_network_open.py create mode 100644 blender/arm/logicnode/network/LN_network_send_message.py create mode 100644 blender/arm/logicnode/network/__init__.py create mode 100644 blender/arm/logicnode/object/LN_get_distance.py create mode 100644 blender/arm/logicnode/object/LN_get_object_by_name.py create mode 100644 blender/arm/logicnode/object/LN_get_object_by_uid.py create mode 100644 blender/arm/logicnode/object/LN_get_object_child.py create mode 100644 blender/arm/logicnode/object/LN_get_object_children.py create mode 100644 blender/arm/logicnode/object/LN_get_object_mesh.py create mode 100644 blender/arm/logicnode/object/LN_get_object_name.py create mode 100644 blender/arm/logicnode/object/LN_get_object_offscreen.py create mode 100644 blender/arm/logicnode/object/LN_get_object_parent.py create mode 100644 blender/arm/logicnode/object/LN_get_object_property.py create mode 100644 blender/arm/logicnode/object/LN_get_object_uid.py create mode 100644 blender/arm/logicnode/object/LN_get_object_visible.py create mode 100644 blender/arm/logicnode/object/LN_mesh.py create mode 100644 blender/arm/logicnode/object/LN_object.py create mode 100644 blender/arm/logicnode/object/LN_raycast_closest_object.py create mode 100644 blender/arm/logicnode/object/LN_raycast_object.py create mode 100644 blender/arm/logicnode/object/LN_remove_object.py create mode 100644 blender/arm/logicnode/object/LN_remove_object_parent.py create mode 100644 blender/arm/logicnode/object/LN_self_object.py create mode 100644 blender/arm/logicnode/object/LN_set_object_mesh.py create mode 100644 blender/arm/logicnode/object/LN_set_object_name.py create mode 100644 blender/arm/logicnode/object/LN_set_object_parent.py create mode 100644 blender/arm/logicnode/object/LN_set_object_property.py create mode 100644 blender/arm/logicnode/object/LN_set_object_shape_key.py create mode 100644 blender/arm/logicnode/object/LN_set_object_visible.py create mode 100644 blender/arm/logicnode/object/LN_spawn_object.py create mode 100644 blender/arm/logicnode/object/LN_spawn_object_by_name.py create mode 100644 blender/arm/logicnode/object/__init__.py create mode 100644 blender/arm/logicnode/physics/LN_Add_rigid_body.py create mode 100644 blender/arm/logicnode/physics/LN_add_physics_constraint.py create mode 100644 blender/arm/logicnode/physics/LN_apply_force.py create mode 100644 blender/arm/logicnode/physics/LN_apply_force_at_location.py create mode 100644 blender/arm/logicnode/physics/LN_apply_impulse.py create mode 100644 blender/arm/logicnode/physics/LN_apply_impulse_at_location.py create mode 100644 blender/arm/logicnode/physics/LN_apply_torque.py create mode 100644 blender/arm/logicnode/physics/LN_apply_torque_impulse.py create mode 100644 blender/arm/logicnode/physics/LN_convex_cast.py create mode 100644 blender/arm/logicnode/physics/LN_convex_cast_on.py create mode 100644 blender/arm/logicnode/physics/LN_get_rb_contacts.py create mode 100644 blender/arm/logicnode/physics/LN_get_rb_data.py create mode 100644 blender/arm/logicnode/physics/LN_get_rb_first_contact.py create mode 100644 blender/arm/logicnode/physics/LN_get_rb_point_velocity.py create mode 100644 blender/arm/logicnode/physics/LN_get_rb_velocity.py create mode 100644 blender/arm/logicnode/physics/LN_get_world_gravity.py create mode 100644 blender/arm/logicnode/physics/LN_has_contact.py create mode 100644 blender/arm/logicnode/physics/LN_has_contact_array.py create mode 100644 blender/arm/logicnode/physics/LN_is_rb_active.py create mode 100644 blender/arm/logicnode/physics/LN_on_contact.py create mode 100644 blender/arm/logicnode/physics/LN_on_contact_array.py create mode 100644 blender/arm/logicnode/physics/LN_on_volume_trigger.py create mode 100644 blender/arm/logicnode/physics/LN_physics_constraint.py create mode 100644 blender/arm/logicnode/physics/LN_pick_rb.py create mode 100644 blender/arm/logicnode/physics/LN_ray_cast.py create mode 100644 blender/arm/logicnode/physics/LN_ray_cast_on.py create mode 100644 blender/arm/logicnode/physics/LN_remove_rb.py create mode 100644 blender/arm/logicnode/physics/LN_set_rb_activation_state.py create mode 100644 blender/arm/logicnode/physics/LN_set_rb_friction.py create mode 100644 blender/arm/logicnode/physics/LN_set_rb_gravity_enabled.py create mode 100644 blender/arm/logicnode/physics/LN_set_rb_velocity.py create mode 100644 blender/arm/logicnode/physics/LN_set_world_gravity.py create mode 100644 blender/arm/logicnode/physics/LN_volume_trigger.py create mode 100644 blender/arm/logicnode/physics/__init__.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_get_global_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_get_highlight_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_get_midtone_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_get_shadow_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_set_global_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_set_highlight_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_set_midtone_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_colorgrading_set_shadow_node.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_bloom_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_ca_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_camera_post_process.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_lenstexture_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_letterbox_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_ssao_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_get_ssr_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_lenstexture_set.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_bloom_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_ca_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_camera_post_process.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_letterbox_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_ssao_settings.py create mode 100644 blender/arm/logicnode/postprocess/LN_set_ssr_settings.py create mode 100644 blender/arm/logicnode/postprocess/__init__.py create mode 100644 blender/arm/logicnode/random/LN_random_boolean.py create mode 100644 blender/arm/logicnode/random/LN_random_choice.py create mode 100644 blender/arm/logicnode/random/LN_random_color.py create mode 100644 blender/arm/logicnode/random/LN_random_float.py create mode 100644 blender/arm/logicnode/random/LN_random_integer.py create mode 100644 blender/arm/logicnode/random/LN_random_output.py create mode 100644 blender/arm/logicnode/random/LN_random_string.py create mode 100644 blender/arm/logicnode/random/LN_random_vector.py create mode 100644 blender/arm/logicnode/random/__init__.py create mode 100644 blender/arm/logicnode/renderpath/LN_create_render_target.py create mode 100644 blender/arm/logicnode/renderpath/LN_pause_active_camera_render.py create mode 100644 blender/arm/logicnode/renderpath/LN_rotate_render_target.py create mode 100644 blender/arm/logicnode/renderpath/LN_set_msaa_quality.py create mode 100644 blender/arm/logicnode/renderpath/LN_set_post_process_quality.py create mode 100644 blender/arm/logicnode/renderpath/LN_set_shader_uniform.py create mode 100644 blender/arm/logicnode/renderpath/LN_set_shadows_quality.py create mode 100644 blender/arm/logicnode/renderpath/LN_set_ssaa_quality.py create mode 100644 blender/arm/logicnode/renderpath/__init__.py create mode 100644 blender/arm/logicnode/replacement.py create mode 100644 blender/arm/logicnode/scene/LN_add_object_to_collection.py create mode 100644 blender/arm/logicnode/scene/LN_collection.py create mode 100644 blender/arm/logicnode/scene/LN_create_collection.py create mode 100644 blender/arm/logicnode/scene/LN_get_collection.py create mode 100644 blender/arm/logicnode/scene/LN_get_object_collection.py create mode 100644 blender/arm/logicnode/scene/LN_get_scene_active.py create mode 100644 blender/arm/logicnode/scene/LN_get_scene_root.py create mode 100644 blender/arm/logicnode/scene/LN_global_object.py create mode 100644 blender/arm/logicnode/scene/LN_remove_collection.py create mode 100644 blender/arm/logicnode/scene/LN_remove_object_from_collection.py create mode 100644 blender/arm/logicnode/scene/LN_remove_scene_active.py create mode 100644 blender/arm/logicnode/scene/LN_set_scene_active.py create mode 100644 blender/arm/logicnode/scene/LN_spawn_collection.py create mode 100644 blender/arm/logicnode/scene/LN_spawn_scene.py create mode 100644 blender/arm/logicnode/scene/__init__.py create mode 100644 blender/arm/logicnode/sound/LN_pause_speaker.py create mode 100644 blender/arm/logicnode/sound/LN_play_sound.py create mode 100644 blender/arm/logicnode/sound/LN_play_speaker.py create mode 100644 blender/arm/logicnode/sound/LN_stop_speaker.py create mode 100644 blender/arm/logicnode/sound/__init__.py create mode 100644 blender/arm/logicnode/string/LN_concatenate_string.py create mode 100644 blender/arm/logicnode/string/LN_parse_float.py create mode 100644 blender/arm/logicnode/string/LN_parse_int.py create mode 100644 blender/arm/logicnode/string/LN_split_string.py create mode 100644 blender/arm/logicnode/string/LN_string.py create mode 100644 blender/arm/logicnode/string/LN_string_case.py create mode 100644 blender/arm/logicnode/string/LN_string_contains.py create mode 100644 blender/arm/logicnode/string/LN_string_length.py create mode 100644 blender/arm/logicnode/string/LN_string_replace.py create mode 100644 blender/arm/logicnode/string/LN_sub_string.py create mode 100644 blender/arm/logicnode/string/__init__.py create mode 100644 blender/arm/logicnode/trait/LN_add_trait_to_object.py create mode 100644 blender/arm/logicnode/trait/LN_get_object_trait.py create mode 100644 blender/arm/logicnode/trait/LN_get_object_traits.py create mode 100644 blender/arm/logicnode/trait/LN_get_trait_name.py create mode 100644 blender/arm/logicnode/trait/LN_get_trait_paused.py create mode 100644 blender/arm/logicnode/trait/LN_remove_trait.py create mode 100644 blender/arm/logicnode/trait/LN_remove_trait_from_object.py create mode 100644 blender/arm/logicnode/trait/LN_self_trait.py create mode 100644 blender/arm/logicnode/trait/LN_set_trait_paused.py create mode 100644 blender/arm/logicnode/trait/LN_trait.py create mode 100644 blender/arm/logicnode/trait/__init__.py create mode 100644 blender/arm/logicnode/transform/LN_append_transform.py create mode 100644 blender/arm/logicnode/transform/LN_get_object_dimension.py create mode 100644 blender/arm/logicnode/transform/LN_get_object_location.py create mode 100644 blender/arm/logicnode/transform/LN_get_object_rotation.py create mode 100644 blender/arm/logicnode/transform/LN_get_object_scale.py create mode 100644 blender/arm/logicnode/transform/LN_get_object_transform.py create mode 100644 blender/arm/logicnode/transform/LN_get_world_orientation.py create mode 100644 blender/arm/logicnode/transform/LN_look_at.py create mode 100644 blender/arm/logicnode/transform/LN_rotate_object.py create mode 100644 blender/arm/logicnode/transform/LN_separate_rotation.py create mode 100644 blender/arm/logicnode/transform/LN_separate_transform.py create mode 100644 blender/arm/logicnode/transform/LN_set_object_location.py create mode 100644 blender/arm/logicnode/transform/LN_set_object_rotation.py create mode 100644 blender/arm/logicnode/transform/LN_set_object_scale.py create mode 100644 blender/arm/logicnode/transform/LN_set_object_transform.py create mode 100644 blender/arm/logicnode/transform/LN_transform_math.py create mode 100644 blender/arm/logicnode/transform/LN_transform_to_vector.py create mode 100644 blender/arm/logicnode/transform/LN_translate_object.py create mode 100644 blender/arm/logicnode/transform/LN_translate_on_local_axis.py create mode 100644 blender/arm/logicnode/transform/LN_vector_to_object_orientation.py create mode 100644 blender/arm/logicnode/transform/LN_world_vector_to_local_space.py create mode 100644 blender/arm/logicnode/transform/__init__.py create mode 100644 blender/arm/logicnode/tree_variables.py create mode 100644 blender/arm/logicnode/variable/LN_boolean.py create mode 100644 blender/arm/logicnode/variable/LN_color.py create mode 100644 blender/arm/logicnode/variable/LN_dynamic.py create mode 100644 blender/arm/logicnode/variable/LN_float.py create mode 100644 blender/arm/logicnode/variable/LN_integer.py create mode 100644 blender/arm/logicnode/variable/LN_mask.py create mode 100644 blender/arm/logicnode/variable/LN_retain_value.py create mode 100644 blender/arm/logicnode/variable/LN_rotation.py create mode 100644 blender/arm/logicnode/variable/LN_scene.py create mode 100644 blender/arm/logicnode/variable/LN_set_variable.py create mode 100644 blender/arm/logicnode/variable/LN_transform.py create mode 100644 blender/arm/logicnode/variable/LN_vector.py create mode 100644 blender/arm/logicnode/variable/__init__.py create mode 100644 blender/arm/make.py create mode 100644 blender/arm/make_logic.py create mode 100644 blender/arm/make_renderpath.py create mode 100644 blender/arm/make_state.py create mode 100644 blender/arm/make_world.py create mode 100644 blender/arm/material/__init__.py create mode 100644 blender/arm/material/arm_nodes/__init__.py create mode 100644 blender/arm/material/arm_nodes/arm_nodes.py create mode 100644 blender/arm/material/arm_nodes/custom_particle_node.py create mode 100644 blender/arm/material/arm_nodes/shader_data_node.py create mode 100644 blender/arm/material/cycles.py create mode 100644 blender/arm/material/cycles_functions.py create mode 100644 blender/arm/material/cycles_nodes/__init__.py create mode 100644 blender/arm/material/cycles_nodes/nodes_color.py create mode 100644 blender/arm/material/cycles_nodes/nodes_converter.py create mode 100644 blender/arm/material/cycles_nodes/nodes_input.py create mode 100644 blender/arm/material/cycles_nodes/nodes_shader.py create mode 100644 blender/arm/material/cycles_nodes/nodes_texture.py create mode 100644 blender/arm/material/cycles_nodes/nodes_vector.py create mode 100644 blender/arm/material/make.py create mode 100644 blender/arm/material/make_attrib.py create mode 100644 blender/arm/material/make_cluster.py create mode 100644 blender/arm/material/make_decal.py create mode 100644 blender/arm/material/make_depth.py create mode 100644 blender/arm/material/make_finalize.py create mode 100644 blender/arm/material/make_inst.py create mode 100644 blender/arm/material/make_mesh.py create mode 100644 blender/arm/material/make_morph_target.py create mode 100644 blender/arm/material/make_overlay.py create mode 100644 blender/arm/material/make_particle.py create mode 100644 blender/arm/material/make_shader.py create mode 100644 blender/arm/material/make_skin.py create mode 100644 blender/arm/material/make_tess.py create mode 100644 blender/arm/material/make_transluc.py create mode 100644 blender/arm/material/make_voxel.py create mode 100644 blender/arm/material/mat_batch.py create mode 100644 blender/arm/material/mat_state.py create mode 100644 blender/arm/material/mat_utils.py create mode 100644 blender/arm/material/node_meta.py create mode 100644 blender/arm/material/parser_state.py create mode 100644 blender/arm/material/shader.py create mode 100644 blender/arm/node_utils.py create mode 100644 blender/arm/nodes_logic.py create mode 100644 blender/arm/nodes_material.py create mode 100644 blender/arm/profiler.py create mode 100644 blender/arm/props.py create mode 100644 blender/arm/props_bake.py create mode 100644 blender/arm/props_collision_filter_mask.py create mode 100644 blender/arm/props_exporter.py create mode 100644 blender/arm/props_lod.py create mode 100644 blender/arm/props_properties.py create mode 100644 blender/arm/props_renderpath.py create mode 100644 blender/arm/props_tilesheet.py create mode 100644 blender/arm/props_traits.py create mode 100644 blender/arm/props_traits_props.py create mode 100644 blender/arm/props_ui.py create mode 100644 blender/arm/ui_icons.py create mode 100644 blender/arm/utils.py create mode 100644 blender/arm/utils_vs.py create mode 100644 blender/arm/write_data.py create mode 100644 blender/arm/write_probes.py create mode 100644 blender/data/arm_data.blend create mode 100644 blender/data/haxelogic.py create mode 100644 blender/data/skydome.blend create mode 100644 blender/start.py create mode 100644 changes.md create mode 100644 checkstyle.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..dc5d64d3a3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.hdr binary +blender/arm/props.py ident diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..b4d6ea4afa --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +custom: ['https://armory3d.org/fund'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..c5731b2837 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a bug report +title: '' +labels: bug +assignees: '' + +--- +Thank you for contributing to Armory! + +**Description** +Issue description. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +What you expected to happen. + +**System** +Blender: +Armory: +OS: +Graphics card: + +**Test File** +*(drag & drop the zipped .blend file here)* diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..e431c1b9c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Create a feature request +title: '' +labels: feature request +assignees: '' + +--- + + diff --git a/.github/workflows/krom.yml b/.github/workflows/krom.yml new file mode 100644 index 0000000000..31b9b2190d --- /dev/null +++ b/.github/workflows/krom.yml @@ -0,0 +1,22 @@ +name: Krom + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Get Submodules + run: | + git clone https://github.com/armory3d/armory_ci + git clone --recursive https://github.com/armory3d/Kha.git armory_ci/Kha + git clone https://github.com/armory3d/iron.git armory_ci/Libraries/iron + git clone https://github.com/armory3d/armory.git armory_ci/Libraries/armory + git clone https://github.com/armory3d/nodejs_bin.git armory_ci/nodejs_bin + - name: Compile + run: | + cd armory_ci + nodejs_bin/node-linux64 Kha/make.js krom --shaderversion 330 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..784978471e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..7153bbb6d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.detectIndentation": false, + "editor.tabSize": 4, + "editor.insertSpaces": false + "[python]": { + "editor.insertSpaces": true + }, +} diff --git a/Assets/blue_noise64.png b/Assets/blue_noise64.png new file mode 100644 index 0000000000000000000000000000000000000000..af5263ff6420930449394b01699f6a421f574164 GIT binary patch literal 16419 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%0APwB6fl81TGnsl|5D!=aT(`p* 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

1#_}7 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

wBFOH~U!4RUzla!CKU8*qyQBWEWKw%-qw; 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�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

TDBFJ$3IzI(! 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

SyvYubGgIr-WuP%AGi|_W)e+T!R}bDy zkQ+>>Du4^|xb6q?&5>{~NGaFJa!|dgS~Cmeg4CgSKSVw1L3S0WCVRm20^_90%kWm<5eyyO)myJC?DA#V5_NRI_O=YefO=d)$135CWbi5U09M(VmAK(RfS0=84 zH<0s=%xRD-RgQNP><)7xJ_@E(|6B}%oF1KyZ-QyeUd*h3I5V;5X&3kdPwt895H*k7 z^A~|BdwdxnJNh_|sv(~HczJjU;<@Ad$8Q2b)cW)ws7HG6Ndtrnsrl7=z&6R96CZ}` zpfpblAb!Y+v3sCHvEykWm~}K|SAr>{&fEkUFlNqztra)MW`hi5t2hI)MGjdHs8*Nt z5SSzCPUbYI4sk2H1az;yZtnv{lm^=z(Gs}%>}o9 z=z|W@G`k%DXpfo(0CUGWhCg4_0O;d#AAky-)ki&WW>tOGQMKU-2kbpPX14+(_K_Y0 z+rn|X2W+e=?0T@Xf)mUHd!2ew1YAsw+xx)WV4XM&To9LN1$#5IK#YLACx0P2z%Dl3 zW)1ii*?zkcytNax^nqSH0jGdW*ydjZvnn$`)eiCM=$1bSrit~zP0(ZJlD8d9VYp5{ z0DU4J)h!U;*Q?T9U~iiB!4T*Z_Le8XF0^O-La-&)?YIZ5lup`T5jo&vO~xs^I+E0` z+s?bsBeN!1-yKzzpIEYH2d*~rfbV+nmb&vU&r7GwS>$5xWq!62Ko+Zo?)juB`p~#D z11~lw0K`kN175(JAH4fM%U~}y}GX~tHn@SM- z>|HwmqRuS0G1yb4)1C#r+}_p;Ky;gfrUsa+E6g#VBbsgl5G~meTL)q+JH|5jKLY>@ W=d`oQ2+5rQ0000424fB8;eO1NIa6n4JeshVF+(cK9Ge=SGKbkQH`Mn%?mJGMnKfl;{_BsC9L!k2 z=hG76C$rm^v+%4V?gvjphEoUqQT*Ia5uxdp0~mIY(F#^JG0BYuOTGjnJI@R`6R< zyYgbDV@$V6XGWDRY~RWyHbp-3v?<1JGGQIgjAM{##Ki^-8#aLTWXx*OGYs7n!zO_n ze7n4-hUzNug`eU+lQLzzR-)}&II$g~h!v_ovJ7b&M`kF znKe_FvY~tpdtL-FU(u8mD}$LUo=?G5fh!KzI9x%vHsLD3H3ZijT$B0FY%$-@TB$B< zpgNhAs_ofgPkC3%`x{-pcINL{_NWWZrs(wNd_Lt(5nfC%$9wOi%tB+lOyA z-CUNhx7 z0}zPWMz#+NtN-B@yC(T(gI88_zK=v%Y1W?RjMn)NlSYL?gJ)=a7yToZQfw(CCo z2f73NFgs?G7_tBV0$dNxrUAmYG->ks`-fu&==26V`vwk<4I4Q*ySTc!dw6;^Zt`5y zX3sZoVf6Mfwe)xYhuikz7_Uk`jU|9H| z!9#`)8$Kc;GHPV>sF=~QW5&jf8$Tg_;@?~Q*R)*9*a8V#7<-Mea`rk_xR+mjg;g-N zRpzrjY;PH3d6Z^zle4nZ(%C-73fC@LeDKZrEF)*3?bd&Tm8|WcO8en4`#E1V`Y^XH zsw{YZzr8YkT)Rr%)@baP*te2TXjduPR`S-~?JAYF#{QLxZ~urG(`e%|volPwi+U1Dtk7qWTTfYll4W$n7k|VmMv?#3_W=IyZWEM4|DckGkX3#0}|+W znCA^gRroS_H+san>a%{WbpBEaJ zMaES1#M(RQupKLmim75RDBsT+-L%pKvc|&0&X8Mup6q4Awv{~#4;xu*%>GuaZ__H6 z^Y8m2-?2?4Yr_t3XVHcqaN)p1E}sVQHtY}|*rmOpZ-djTJlK>R>_ zA6>MrP%cYUlrUB6hrM}J)Z(9p~fYglZkHe4}0vU9Wxwu`o#ZC7Ad zYWJ$$VY?6PuGxKW-_m}#{Q~<6`4N@CyX>g>$nFbFX8awoNNOjoZaKzzv z$HtDqj^U2e9Sa>R9IG9_cYN5edBeW{?Z3edqZ*EDIIUrF!^I6ZHGHGtyA3~Y_-(_V z8#y;>-Dqf|DUFgFEo@Zk)X2%xsku{t(?F*vr-@E^PTQQ0IX!Xi?mW-A(D|-QGnW{b zH7-|NRo70glU++)@4Fe@CcCY0JLC4qJ>30WkB%OTJt^HTjbj?eG5~inVS@|5*Qrd>8|(j<70L=eE0p3nVmY#4B?wq zk0-CU>((u{OBXq|0=}xNFkzroW{+};$<3^jz&)BL@fEz0L|vw~iT_k6!B!>%kAH%; znz9xsSg|Z7e88yabwKTtCZ{dJ~-Mg0? zh9*oL88&pvl!&@Z`Q>|-56Cayy<9&mVbaL(VUs6C{nlGe?v0U8u-;dH)16|?nLk6s z1CAjiD=>v@oH!4LWDH&=|3=*8(^3}@!VOY%in~`xcgR#qG9m4i_}-`XY&bPKX$J4x zcX!UmHSytRr(VU-eYQ4R+v>f0qfdHz-+;-3BPQ@AiIr*T#RFbFwr^3)`iNoL{Ds@C zdmg9s?)UYeIlR1a&cgIn%IyioksU{M?=w0{>JEHZ)te!%g1#{~sc06Dm-u?!YOR>x zp!e+>KWV+Y zesSVB^{q)-&A|NSyH_Wir+R%w3(-)#EnPv58bw2`G3;`S68vPvunp;meK5mAq*Z`2 z`1lofPVUkrxpRESo;^FZ@7`TL=W5MYK7~py43)>)ey6u zVXZd#2qnnb)g0jJ=1-bqaG^;MqBf|x|HDnS=Er+AN>xG8s)b_d@}-LuF-tqDo#H{< z{3q|lJ87S2XByu7?9&?U^ZU21d~1^p+(Ge$?vPw}yiG2;AYZfi;)u3fxIV95TDs>f z^)n8tV+Ic|##*7ar+N!R(2~^Lo_nk2y8ispoRP~?mTj!!$`=>zYd>k(;>$&%WA65m z$!oT*c>mGW{hw){X`^lJjDdxJ;JQhurH zEY81Rq&=SMh(ecG&8SmV@bu9h(iybRw9{&9Ex}DjZN*}E% zQlDFrnzVFj%?b>FV3Vy@noNiDnV0F2-D*rS^69W3CUGive@?Pla4R+-n6}F%tLuk$Xc)anUnpI0N#- z*+BN5lC6eI`Es2HW{wB96UlWKp{fOzQ#@x4&psvlRmzK$YsyEEH2e)Y4AMR+*XoWd zz2&8}-)X7df%+D+Q_6L9kZrtVZ9z&?%M2yDu0ky1%5iP+8f{TE>KkPJLrI668j~J% z=RL3p*(73;!h$Vw;(Cw2oRtI-FeE=}cH)$IRpOhs-}z+Mtgw^yzRRYJWl>AU&q~ag zIcC?{3;Xv~%p6gwz4y$S5p25FMgOU;gaxtQ;I7A9pq0)qB-mB1)?SJp(;~!`dHbls zpm$Z}FE3gPh1+u+_Cs~$dQar`;BIm;)Sq|!{H>!w!OO!}t#Nb8I*~XcZ+M{F+PN$A zZd#Rg;C=1fn+}e}-2YPSTU~v-f0+10D?RAwSi|Q%c+4liQTuD75iwnpTJrXtdZumU z4}aCZ-!n4u`ww5?%DOf^ET7)F`6ge)OV4VB4 zxOh&KVnxoyfr4{2XHqo?Ne* zjLan{&=q3EYSJDPM8I9bf)N~|e%@5O^^>M)@2} zZ$8djZs*&zFHU`OzUI##KGjw3(ayggcTqe4s?fRCF6ZvdI`R>BlcpHFWu5)0PDs^7 zuF7;NH!VxWS9Oz=wU%}wL+rHFZPit7)7m~O(*b3$rlJg1aal_Fz_PVkS!@XvlSIB{ zzI?h7>d%H;u0t-6y)WeA(^B{cIWzA7)ypKjv5tD!#3E;!dOckGv+{s;zeHegJ-_9i zEvkdHnv-Yw%@Z?s1!?<)^NHz+yLsS+Tpq@!|L`gI)_&2f8GmRuI(Fs*u)$*MkIv54 zAFQL2oeUhzI=hn=pXzE+a}s?SudB4I63O)ZAr#8M^H}%syqRZISK}9FL0A_*K*|aC z%`P@q*XCM>C=MrUFMaWA_0okIYq_qn_VK0rHRtB9*s$bj`#M|u+(4~!GmN@udH}62 z`QGxLnEsu3rAl)LhmPW7%lx`ep7O?;Qz`fCeWg6je7ZPLxANpx$i2Q^eFT#5!R&<@ z6}v(G-SEwK^6UKi<7!vUXRT&(1N+*sY8muDL`<$tP)jY_E$_=J{w5oYn>SQncGo5u z)5(+K#tC&|?KYI&s7{5hrCaao>@jXG6z4$6Y(qynt-5;nlNW?FPDWdBP*PK}FztnCfSyzc){;7EK;-C+*KawQ@dsQ(0M&wnc$84TXeY07e7s~oo@h61{n&%~B<0;mdTF-VAv>Zsz>nZI zE937iPG}V;`PUD*{n~sUebF+T`xlk(EY+@wA(n%>%A21So)55Wa1?jf&0A88T}+JK zaLl7*^hx?@aF;WIv*q}{)>L4#Jk z_j2j34Pxnlk4j&=mHI(eLCO52(K%Cc%98e9O*xlauyNj}X=w9B^rZxSq18|h3Fc^t z*7k(fPMxB2ee#pebsKS80d5s|&K2CS@H%lH=8tJ-^N(EA3hXprH$Tw6702J$`SRr5Vf1Qzd@Ga-!yB*jk|c07g~IMQro^yyW-`gy}wV}QOz$@zpgy0 zv#UI%eEVd$>YJ2Q`z5NOK6FOg7x4qJi9O9&Oj;yF0gw9{5y@xYYMZpuZ}{h$`IfR! zw70ak_=rxHbD~CErt+{8>QR(Otb$@vfl)Yt&%35IsMZ=>7gxoXbxD?6qJ^@Wp2xYP znvLf%7v+2plJ15F@2vEyy{aloXN)>b&2IH4`! zS-S5ZXO@l#eDb6| z@6o_geIcIdDZWr(m&i}5UtlYazsB3-^45FRqgr$EnWdGiOSL8{a7bl4I_V*m*3DGb zl_1>0z0T@2ehmYNdo&o=wrkoA{1_pBkUlz$jimu z%Y)oUlTn7~FE}@N>EMnJIsf6!)VY%uA4xf#e)Nj!r#Xz;Vp^@emTHVx{MPdFqa))} zCJz`<5_7anYqUPbd1dUtnzN%ON_!yn(+EzDSQD_4ae}`=t_#6NR~HK43@+01QH|eL ze>lg!qG}FbcB^X5(nG^XzPV_faCxMCvU9W&eX3b>?_8|V+E!ddU&msNxR3rI z4v}=tjZm{(_ieqPbzhHl-&1s-w9amyF8%cEue93KFV?;Ni`_oEqQsS(H|1x>#_gGm zIGVNC_OS5U8;MIURGW^T2j}sq`)6==!oCZ317K#oz}O^ZCTx2yAufLK)5hOFaXhK2 z!anKDu^+2S7gvsme66rl_&w$w7K*?pEVYmaJ~ABscpLv^BNSDzIMn+aID1IeIv<2H zPaSvf<}z}^rURk|gEzL4#E|RS{hY5}`sA*~q#j&0vCurXQ2Sy|sjFyiSLn{YfA!gE zDbnt0mLWS%5APjwNjd-W+D0ok+jOxj8auD|uz4y72(WF1E*K=`=eZ2|?(E%rq@rh! z1|5Q8C*HZE>@7-qeEIh#ALhVmBex_6K53Mt_L{)9G& zk470s_^7a;&fY%$L9%;6E+`D=opv$@X}7xt8UwrcatjFx#(rk78d@8sopW7h*C$dv z%3HMUQC}Zt;GjB!5CWij9|T5HMwUlXtEwjP5x2B4{L?S^A6Z&~p4E-d;FGnkmSvJZ zm&a6mB+6p`l0O8}_{G_B*xXE;u5&Mt@`93+gz7gyOfG>YA1Npvs>?Y4O+@hP>3p)j zYWn=Cg$_sVyxpft4b5Ef#?T4c5=$F#J~bt0nk7J-zQ4KdE;iwx${zJg*`xk{3ZHk; z&pY=w;j5vATmCM5Sq1cE4CZ+t>Tr_#G;XBV4D(M$Ijnx4(iVS>nSZ0`EAEnYYt41- z!I9l7R=!%kV(A{?zg=6VUDO(Gt6jkZ>g?aV_SM+~UtN=Ofeq0nsLiQv%txCIL1)cU z6#q0L&A&c*dTv#@{oME8`=)A3;hw0-y^FRAmp`@3^DTesu4S&!u4zxyLs!;YYS&5E z4|R{%CMXY{*6k)Ymb{?nCYS%1>vvCAC)vS19Xa(w)z(F?jEdQ{c$@HJR^IBRSx@|g zn#o_RvpaHSjc_fxYLlrbLA?#Z7!!}wn8r?&w0`mJUR7@@V|E8OQ`Q+aTkvwn{MFK5 zmv-kU%J`v7pkK>pd#L|qvzbDE+g^(z=4y9d8Fp*m;oC=EnwXG0oqN0%`F+*Gb91Y7 z%aapxd5b}jU87QmFFJgD-Qd|V1N!vs9-TF6^}eyMOo*F>nU1x|dS68AdP-XXhZMGf zCbS2-?-%3(+u_2OoIfAj%h)Nb-@FB@Y%k)_P+3nQxL=1Z zILlZ=ZU#vwM4AQ&Q+Z+LrPtr$Rp+ksud1Ap`~GQBZRz)Lo8ne`hRR~6M>&GBsLB`X zxAY@^;g0WU^Hyq^@6;+iYR}m86#MYHPmq*zOD?iKl>GGYhWpX$zNLlUl=`UQ+j^l7Cn zIM1DwHnlCZUzLU%)WHd}@v02>S1J3>WP-78|G~ zw&}ZJ|3dnlSSd_F0o}0eh&95+!`*<5epfG%x&PpmD`MY4ze~G!D;Kpj0k@Bx{cZKH zA0N79PU)k4cmA8vv9~X3KlVm_`>+Gn*XEya%pKl${d(7wfLm3UAFTfE z{IRbCsJ?u@Mtz|8kQXkaxAs_@^&SRkPn+`=9L80BaCGUwyz=nYL!)*cUOsq5WzXja zk5}xseG#;-<&1G+$HksI8;I2L*juXKUv}0@>+~SHPxB`S(1`{f62MWhWRv+j9z%9@S3iQOQf2Q8E>d6~{Y7FY-*ioAfWa7oGJ4 z2J=SKu=Aw>sKa@QK}|sx5IMRq))EjTOWq{rrPS`cz43kp8E%3gClPGknuv#))wau z?D=jyB463(Y3hsOsNN0xb7^+W*}&3xAIk5ldWyx^kn>~djxs-;_do#w4xfn0>>)Tn zuFrP(y`FCM+vt1+Qb*#oPX;dCSXDOjlb-ZF?V)a4J`# zG@wd0);$Duw7pDt2_NNxw5|U<4_lTrc1-f@adAnCI%>|WINwpTlVTv7b=V8+z%D@s zuBw!QE3YEiARlj*J?(6bjemIi8#-x_|tF z4@=&b?Vb{{JHK?uu#)^eyBBU8Hn?PAg{9%{{DR%D6fCUZiPOZ{X$PO4=J5+LVe(vW z;HcW(bmkX6Foq}OmuF?sL0)F&?&V5Oenmy$z?Uj^FRyDR5A*6mo*nLm4@;O5fn&bO zk>YOsF2p{$VGOtw)NmAB+CX`*rV!kcrkQu~7YuM~b5 z-2ykh2+xgXrJ|ATIXCR&!k*vZ{@NF-xX?7^EiK1Q%i*hd5w(GHW@VYK5@U#68jO(+ z?>x%3y0F>GbokQnnL;4Rm!$}1)48Nz!D6$JapAMG4xG|@`-6?y#UO^yn6Xt`s^?id zwSpTi%9(}b&4GVcHOq2QF@lqjvA8Pt=oDK0q2V|?#|)v^ah0V&%;Vjk?bq=JC0{??v6JnN zL0Rd;%3#mi%iZ9OZ~!~{Awm9FjOik`6!9AAiWh42@>V>w{YrQ76|a3XXMeh~f5yB`^AF801aF<( zZPEvWw>)1r!3&Wb;1YPyDG|+b8B9p0Kpq$2@;B+s=$+Z02bpxEZs7B-c2b*I{RKYN zd;~8#Q5Vm<#9tHcA81#!!5JCi<}~HC(siZskWgMo3e{34)Vhz?ty@?6cHN!rkl$|W zeZ2x6Eo7_;xsyxcMRUi+)QBT~gfy|Q4jy13oppIOrqkf3m;!lN|9K0F-0z%yzlvv_ z|IxkZ$obld)ytPJDY&5Q{hQ}<=pA9XpsTEYo9neF+M1OmrK<#Vd5`tJ9nK=5XThXr zqf{9X8Ki9R=7{7Z=XV>{5p!HR&t}_u2L{Lp?;X%T+c+t>14qFe0vXB6-=u_?THp*9 zz|9@FLD_vb=48T>J*_VNGUage?U=(;?|;zh<%J2=V}6M~s12NBIxObSY|jtvUZ%UP#Iue!cGeyYi!LX`sJ+S#P)?P8nX` zk1jy2h_Q6r;D6Z8QXxRd5t5V23Exj{$4hq&0v1R1?NO@|lC*cK_|hugeeP6VC0==5 z4Lx~6dzlZ;&E$3C5dxhNUs;Z(EfD)GL*G3mWE(2{=?~S{F!2>w`R23-2>s{(eGE+} zZm-EnQG4*;!fB<2T#D7pC3WxoYg}FXlWp$A0G|f@`QN55ai2B2Y`>o8YI!_QJpbNhpB>G)FL#`` zbUxSf7A18-VRIf}&je8$*q$L(zUi$?2D>T{^S zIqLV&%dKdC7ei1;5H9FSh=;)gmrF2CK-A|%vG(V_;WK~7r@9uO>)mU}uwLB;bLanjSw~MV>olKTyl3fvg?m>gg$|h#5jHeoaHs{Z>HIhE>IgQ? zdQX`SU)D>Wi|XKw=n+_ixX`v4=t0_`sC1VRhg|N&2k%~cz<$*J=uk}5t70sBqX{DGn@{b(fiE65Tc*Kl*aq)+0xE>L$*ZJ|Q7~#*9g|o7LI1YjBWKq|TLkCD>G@M3wupj(DR$ zzGtS$RuD6~n6R+O+3Ky-tO#5YJEypKPORpzcFa1yQ0J|s^QBsbXrj&KMV34KoYsk- zgFgp7MYZY$*ct4K`3Ad6_C~qSh{Hgv(|!VCqcdMR9K_W37PppdQr^|xsNT9-bN_03 z=1Ohn4$CSfbNY3y*{-I%+1*6g*&tlr)-sd zD1~iXi+uqPxl2#cwZLHt^h!o(-g5smgu^rOcJWg^4;@|o`;y{E1&h8ZP%JEC)2Y|A z;*xC9EM;8sR?gQJab^4R--^3yhi7Lit+Tazytx#VjzwAj&>hrmCae7d>mc9b;s`V0 z@#1wdyx<@Kij4Ogq>JkAiPzkoz2KlT!6f*a75HDe?HS8=Xg}`=@0n3De?iI8q1}57 z8oXfeijW>X=Je=kF>L*uPjc-koix!WJ)}$i^sKDuz2lpPq)$yx*FpnBLj!|CL;24^ z9ou*6G&ZD(7~MH9?a3$P&*|{)!yo!c-JA6shcURL|7(u*Zh|EL*Bdsndr}o`hl@dh zc)b7Bh`iE_a;Z6RxJ>%8gI<^Zn zx0;xfH7#UJlU7C3=P#InNWwf<)a6rmF(#AH-!NTSeSiDnU5fuhf8FGQBZth^M}ITa z#P$k=fjSxVKX%d>z-zRZJ$r@c742A@wOhZr!Ardc4h;|M!aM%2yO_e)6jVIFG;7}K z1qqV|_3b{SOOF|CYDd zYw0J}E_`yno-5w(u-;Pxbob#GW3}b*o8*Ru!9N6H0ahw^Asr1&eC8$h*XzSIye%ra z#=RQaDDl+^FKu>yF|~vnc#F)M;%j*AlzqEm`}Sq4wyVu{DB6&|E569;QsU838>-C? zIHrC23O7A$be%hw9^K(w@zLWiyE(;9CtD`{jTG22MMhQ#V-QzxvA9C?dGZ?9D}%ME z^4*KAs&txJxwVaY8_(kW5Yv<55A}AHCAdoMdR6!16F2=LREg*MX}_s^$vz@hgUSTi za~y_wb7h9*sIsVblZdx$SDVzG75zmoOL%FK_&{7PF17TQW%r2X%02A!Aoh~uq1>x` zN9iM$)1FBW*vV4VYiB!?gJX=B13U7)?^wQG`9~G+!kek8rFLaMjn!h#!1SN}A8#Qox>HF*ne z6t4bp#PA1eE^^Pu!(S~-t_pN$oV;fE;#YNlzPc#oSdhJETJiA0SA^4AD_ebd|G~G5 zI4fzq7N>rP4(_kgPX5+#|Mg=p`?sD{9e=&b?u{Go>}kJ!z4Rvr8Y>cvYoT& zD7=>kBS9_{^vly051vrAt#hbrd+*KrcXo|x>>K>{j9q)&S2x_RYf^FCW0eC=UZs6c z;Eh!W<#*WkHi$OS&)^muHHCOU3>~ql)d$xpgvIBAo8CQ@eZo* zhulti+CA@o97*!x`kvW3s?GU(o3<|g)n_`e^kieSWW!IoHxS!Che#|0?op|u-*HlcDS@%Z`3N?4Odh`2$~571M9lv{e>Z7C>o2mV^=UVG*RKud^)aH<7T>?6 zmCi3so{}dPOiRcUeU42`jtU6;SSt$e)iXT2ch4~8$k@b)m|1btS_MBoHTRV6g|kVI zX7Qj?y8Aphsc!6osS_`7PsK94)u>Q_z;Z`EA;q;A2JI*-Q-Y+sngV=Y$=pW&~q6xrvAfq7?+_P6Xq(QR;>L71 zfByU+RTZkLkEnaG?nULJUqVA0`!5O4D_Ndv4(u5?zH2W`)e6gCwZS%3PB6?c=H42% zCl-fD_ICJ7Qr5}(Io4CXbv@95(AQdsbU*7w-~j7s;6TDK!a;;1toM;0K^SR$59ugM zkEFY!tny8cG1wo~voxwRoiLLyi!hropRj(hkJLfk`F~#KeJ^I1m#DCYdzp$i35{N9EgbnF>%1#WwL(B#DPgB4v1YPN+u3?|5~DC;)wk% zcVHJ-Pe;rrPv#76(Le{nG{SVkOu{U}Y{GoP0-!6Ea-~wP;2?8kDOYfiC`-A5gG5=% z6&#|m2Dw>{u-Y);;@tWMFpkpW&`)15lx?LGW)fx*W)tQU76ARAIk$nK*6)Gctnb6t zHne^R>}350*b6QCvv{=l0&})*0=g3VTJIq(%f5iJcY$3AyIHRy-QD^FFoT}VAJNR`@$0ci;f)kHCS1VT9q>Xg9t}JRvn=E5+fnc4$yp=9!Ymc zQ#uy2y90WC5*SD6xztuNVG6J#(`%JL2SQ(HUPmxF0c=BPCJZ9%r2UBe&erq5Q0oX_ z7wcSLH>(S9wDk%w)|vwxV=V*5Q96xkPAAMH%p%Mt%qJ`$l(C+fw8c!?VuqCNAxCP9 znY6`B+G0kFSCJ#N#Z1~_W>QILzSjLn`=f?V7!ir>tgWQz=4Eego6l2Sl1#yf-sVDq9{F*($Utp zaCek-1#q-=CotA}7&yjy7C6>=5Ew@}66i{4UvLP81bzdyAv6;P5%$6fZYW0ZGH?JiFcf{17)CgVFp}<$ zq;$0PYup_TxrL%He*ohMoSlHq)P)XyMoDgKxtLFl2z#n%PG^+vUMdJ(iI$JIu@+D zLP`?nQq9SPDZp;50p!_@#;zM{Wc^6$oV6X$#rgrzmD0XggSvswH^4T8?X5eIHd8u? zQ0j0uNX!M;neJlzk%nA>UC}}}{F6Do2>V$NAw2+gsT+DBF^n)A7PTAtB6CJyPIZHB zON^wPC`yl{bTp+$S2<0l%9lek!SDEhMFHw#~cZ`KZsSn-3 z?KW@>VH{x@YVHA>@Ds2zupfMecYqFrvDROZjw4K?oOHrW!YsmU!hFI4V1Lx`4bXv5 z&XN9*#xFoQQ~E<1GMz?u(+M*Pvk0>Z^9c)p15n!^KnKD!!gRt+!YsmU!hFI4U>HjM z3UnZh#Y_)F>k`u_2me12(wUUbBFrYtCoBNUzqxV-IuJ_R5>A>E4xRZP>4Ah{go6l2 z63S5yXK7R_oiLLyi!hropRfQp7(7dX4uolhlG|W#llfVM*@XFo1;An8a3463FpO{z z;Yi>Js&fR@IRgDHMUJd<1l2i$>Kp-&QRd4!N5Eq&Wf4>=f=WeDsR$|+L8T(7R0NfZ zpi&W3DiWnKv6JjTD77ULV|@=ObtV#H{WCC)?n(}ks7*efMVL*PPgnqqLT%pwrRGGD zb&Vp;i6YI3BF%|HZStLQEE;UxJi_n69sPHAa<1_7)>JxzZ>=q-Z5c6%-m?m z@JFEZ0HZN$B!t-~^-^AhvPW^$qd4kO9Q7!UdK5=J zilZLIp`PFHr0h`~^(YP!mi5RU#X-UnWsl+@VVN&`6bA`Qls$@rgq^T=F#sAk4sA&c zBOF8+NjMTX9@g(Zurn|X8n_-PBiS^HAJZs)OhY}Vks~A7G}I$e#*b+fKc-RqmR@Tt*leVk7>|bi86jngWgJ%@nahFR-%j_(}_tsF-a#T>BJX&m{ht z#6OFeWD%1rVvPp)t*&G0mYd&7m>Pp)t*&G0mk?xl}5b zO65|iTq>1IrE;lME|toqQh8J=k4oiHDd;9NB#%nvQK>vCl}DxWs8l}5DxYMPPqNA< zS>=1;nI)n3RD@Au9tDiE@Xh3|`_NK)J(H1}{;f+~Fxh%$N(5J3M8G9Ls@n zho=nj<1V1w;VGjXo-+7~@_D(#QwCr022k$sl)-{J0_6@*85l~GJ3M7zC{gb4l!2i{ zxx*tr&++bZ&yYSH*o68t$NI3XJ}q#@_Evpbp3GgSPpe4ZtWWEh8+WQt>sce-t3GXD zM*M$_w)*Xv9!C$hv;%9vYwFXE%uTq|ryH{NBELS}h_zPiu%F6Rd$1da%a-QoV{v_2 zAkulIJ}v($=AHVqiuC3Bv<|yhck0u6){L9#(+2!shr{dB_RNtVtWP^IcYdxu?a2K2 z5B2GWY^?CDPd8#w`2Py~up~AID_t^6WK-ERhW}v@en?0BZR3kb4Zz(b7mWjwlBUi~ zFow;Z)ZW-@=1gORyp>{%NJvRYPEVMO0ug9$BCb?iEFx)QQmXt$byKlq%|xh~jGqZp z(`LrY#J@lMFEvCHtrXBrLMLPw+oOYK971%VU$Vm?j729Trz9rLHU_kR(Hzo6az^Ig zTWR+nT!>=3f2CwZb5btxxKcqp9x_Q_v#1j@ke7rLkAKQo3VP5gmfPat{mhPP)P&@DFs{4Oha0BCK>5e)GXDb7ZfQT&)I(cvrJpu z|F<#dh{_WuCCpAqm~2d&JvkxSm^v-N*lSMwBz)E1X>0szSn#~m(Nr`QirvBvSoP(< z_GtL8#@nMS$xxUMP?*0HPeIOLXmKAzn?n&F^keNv5oD#QsdGX*bjZlaXdh3Fw@*r* z+F_=RT1tn(VSV}yjqKMhpgo?YSxjG6J%sRYACA*K{ExLb=d`^6jc2ZwmC{l2-~WHx F{|ArEaKHcn literal 0 HcmV?d00001 diff --git a/Assets/font_default_full.ttf b/Assets/font_default_full.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca GIT binary patch literal 171676 zcmbSz2V4|M6K{9V%q~e;lBg&wpkM}x>fM>nbmn}br=nuOoO6zdIe`(wnd7oXjBsX5 zm@q4tGZ;=iWoO^NXIR3&^D|2zz8-$gwNl3@@^;@;6{c7^#D8gns5#lkwS*xIs#L3B<384TI-K}4j zfpkPWZ$iAh9lNHD+Oy2rqt{YHF=-bP5pHF)Q6&n`m-5BgDN*-vQmaOdMf8g!c0Yaq#ZfW#m9@b-^Cb$KiQ>|1SNy zKV9!Jldw{_76S$j7&6q{kHml;1HV6TaQA`hdVD=gNYEd6=QqN6#r$34Ck$-R+4<+c zNij5o--%q8N*rIOEkAzsGF`V1wi|*=d%}1ret1vYR|e1QfBWhsH?$8{YEthhN2xF` z!EqibLRu3k|9)XK2jYkZ*Mc~m&c_`hEtf)3rH_h*%cKWy#?llaS%jIJ6MY91>Urh$ zn>DLXY7$~LX^2cn5AAY>8+0|lCsvaTxX=?pn2mnt|L8Z=4`9w@4Vy>e#5ZJwIG*&E z?vhyXAJUz6Cq3C=GL4-f0m5KXLYzsO(uJfX3n!(S7nv_KCa!pX2w*FqBcKx?9IzZP z70?7Q5D-Ii$#j}b%8D+effz%Uij~N8;WTNa4I;gzDyaJ#*&>CL5mF7ZMVt-ji|Zj| zw6uq8VHL@Ku{Q~qM3grrTQqsNhjM8m87?^~xF2ZWEFL4x#Z#oR6ijAIo}`4f82JV7 zR2450XEB|$WjbWq-#O**r^xYh%&BKBe|i4(GjuSOu{h0mm{ zaF!GSZii=T(hp=8u$wf9Ou_jqF&ezyNIIZDv0^B3rmx5%@i}P^`nqd=BOQcAWRZ|Y zdWikW1WiMfZzXe?iA)yjkuEHU)CN>%lgSt{jw}~mkY8C5vIt{36y>9ZKLM$v2gb9F zCY&_aR3!sIOC9ZK^Lw!liQ@bd50Z`IUu2dhjWiVvWW2b5)JI(%#Fb=;c$##UmXnh7 zD_J29!I(B9eyl9KH?V_xIqdl|CfPnrvNNMk-8;w+5411Ta3q>^$CS<+I}S&EFe)GdXO zVPbvIY9d21zC8hhFvdLq>k0Yl1*o90C&|$1m7EUA3%WfM^3;v26Xv5&cW_UF{H-Hi znl8ix<6{&Lkd=}>`4hHgxn?%0BTd10m|-t&p--2{W{krEyUO7AN3uYA3)#3z`hm|? z#d4&qwgL&0{(w9kCJAB!WNH9#Z!!gVrY4Bg5RaJeD|NRg%Ox4S%K`6=paJrN@!KN4 zMjOpwm#ULhTo1&}q!%Do+)t`O-hYxRp`DtLuTSK6&0-P-e#dCqk_f3hsVx3RT8dgS zN}ERNXois*TG&C&S~4Hc4i`(3?$Qq8FX>4=$VP&Oq3!pif}ke@M0Y~$%EGeeW0nFt z?f`oZ4<=5)c`(k`0l2NdtQve42D=TAYC=Y9z+LOg}JZY)jO6ss4q%R-}Hs&YTng}5p`T;wpX+wN8^T{T= z;o$izoR`PAjfWk%O?*T@QcfI7^il)TQECQ$KZAX`NP5{-BkLpzzI&3TngG&Ta|8Uj zM7n8uLl5S`Kg

WH`00`s@p7bm8Ow12>ck9v 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 ZWcapW`!cj)j__}02%m!NzuKSP`#%)sP|pAW literal 0 HcmV?d00001 diff --git a/Assets/hosek/hosek_radiance_0.hdr b/Assets/hosek/hosek_radiance_0.hdr new file mode 100644 index 0000000000..a9f3cb239b --- /dev/null +++ b/Assets/hosek/hosek_radiance_0.hdr @@ -0,0 +1,7 @@ +#?RADIANCE +# Output from cmft. +FORMAT=32-bit_rle_rgbe +EXPOSURE=1 + +-Y 64 +X 128 ++N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€+N’€/S™€/S™€/S™€/S˜€/R˜€/R˜€/R˜€.R˜€.R˜€.R˜€.R—€.R—€.R—€.Q—€.Q—€.Q–€-Q–€-Q–€-P•€-P•€-P•€-P”€-P”€,P”€,O”€,O“€,O“€,O“€,O’€,N’€+N’€+N‘€+N‘€+N‘€+M€+M€+M€*M€*M€*L€*L€*LŽ€*LŽ€*LŽ€*LŽ€)K€)K€)K€)K€)K€)K€)K€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)K€)K€)K€)K€)K€)K€)K€*L€*LŽ€*LŽ€*LŽ€*L€*L€*M€*M€+M€+M€+M‘€+N‘€+N‘€+N‘€,N’€,N’€,O’€,O“€,O“€,O“€,P”€-P”€-P”€-P•€-P•€-Q•€-Q–€.Q–€.Q–€.Q—€.R—€.R—€.R˜€.R˜€.R˜€/R˜€/R˜€/R˜€/R˜€/S™€/S™€/S™€/S™€/S™€/S™€/S™€2W €2W €2W €2WŸ€2WŸ€2WŸ€2WŸ€2Vž€1Vž€1V€1U€0Uœ€0Tœ€0T›€0Tš€/Sš€/S™€/R™€.R˜€.R—€.Q—€-Q–€-P–€-P•€-P”€,O”€,O“€,O“€,N’€+N’€+N‘€+N‘€+M€+M€*M€*L€*L€*LŽ€)KŽ€)K€)K€)K€)K€)KŒ€)KŒ€)JŒ€)JŒ€(J‹€(J‹€(J‹€(J‹€(J‹€(IŠ€(IŠ€(IŠ€(IŠ€(I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€(I‰€(I‰€(IŠ€(IŠ€(IŠ€(JŠ€(J‹€(J‹€(J‹€(J‹€)JŒ€)JŒ€)KŒ€)KŒ€)K€)K€)K€)K€)LŽ€*LŽ€*L€*M€*M€+M€+N‘€+N‘€+N’€,O’€,O“€,O“€,P”€-P•€-P•€-Q–€.Q–€.R—€.R˜€/R˜€/S™€/Sš€0Tš€0T›€0Tœ€0Uœ€1U€1V€1Vž€2Vž€2WŸ€2WŸ€2W €2W €2W €2W €2W €2W €2W €5[¥€5[¥€5[¥€5[¥€5[¥€4Z¤€4Z¤€4Y£€3Y¢€3X¡€2X €2W €2VŸ€1Vž€1U€0Uœ€0Tœ€0T›€/Sš€/S™€.R˜€.R˜€.Q—€-Q–€-P–€-P•€,O”€,O“€,O“€,N’€+N’€+N‘€+M‘€+M€*M€*L€*L€*LŽ€*LŽ€)LŽ€)KŽ€)K€)K€)K€)KŒ€)JŒ€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€'I‰€(I‰€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(IŠ€(J‹€(J‹€(J‹€(J‹€(JŒ€)JŒ€)KŒ€)K€)K€)K€)LŽ€*LŽ€*LŽ€*L€*M€+M€+M‘€+N‘€+N’€,N’€,O“€,O”€-P”€-P•€-Q–€.Q—€.R˜€/R˜€/S™€/Sš€0T›€0Uœ€1U€1Vž€1Vž€2WŸ€2W €3X¡€3X¢€3Y¢€4Y£€4Z¤€4Z¤€5[¥€5[¦€5[¦€5[¦€5[¦€5[¥€7^ª€7^ª€7^ª€7^ª€7]©€6]¨€6\§€5\¦€5[¥€4Z¤€4Y£€3Y¢€3X¡€2X €2WŸ€1Vž€1U€0Uœ€0T›€/T›€/Sš€/S™€.R˜€.R˜€.Q—€-Q–€-P–€-P•€,O”€,O”€,O“€,O“€+N’€+N’€+N‘€+M‘€*M€*M€*M€*L€*L€*LŽ€)LŽ€)KŽ€)K€)K€)K€)K€)KŒ€)KŒ€)JŒ€(JŒ€(JŒ€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(JŠ€(IŠ€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(J‹€(JŒ€)JŒ€)JŒ€)KŒ€)K€)K€)K€)K€)KŽ€*LŽ€*L€*L€*L€*M€*M€+M‘€+N‘€+N’€+N’€,O“€,O“€,O”€-P•€-P–€-Q–€.Q—€.R˜€.R™€/S™€/Sš€0T›€0Uœ€1U€1Vž€2WŸ€2X¡€3X¢€4Y£€4Z¤€5Z¥€5[¦€5\§€6\§€6]¨€7]©€7^ª€7^ª€7^ª€7^ª€7^ª€9a®€9a®€9a®€9`­€9`¬€8_«€7^ª€7^©€6]¨€6\§€5[¦€5Z¥€4Z¤€3Y¢€3X¡€2W €2WŸ€1Vž€1Vž€0U€0Tœ€0T›€/Sš€/S™€.R™€.R˜€.Q—€.Q—€-Q–€-P–€-P•€,P•€,O”€,O”€,O“€,N“€+N’€+N’€+N‘€+M‘€+M‘€*M€*M€*M€*L€*L€*L€*L€*LŽ€*LŽ€)LŽ€)KŽ€)KŽ€)K€)K€)K€)K€)K€)K€)K€)K€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)KŒ€)K€)K€)K€)K€)K€)K€)K€)K€)K€)KŽ€)KŽ€)LŽ€*LŽ€*LŽ€*L€*L€*L€*M€*M€*M€+M‘€+M‘€+N’€+N’€+N’€,O“€,O“€,O”€,P”€-P•€-P–€-Q–€.Q—€.R˜€.R˜€/S™€/Sš€0T›€0Tœ€1U€1Vž€2VŸ€2W €3X¡€3Y¢€4Y£€4Z¥€5[¦€6\§€6]¨€7^©€8^«€8_«€9`¬€9`­€9a®€:a®€:a®€9a®€;d²€;d²€;c²€;c±€:b°€:a¯€9`­€8`¬€8_«€7^ª€6]¨€6\§€5[¦€5Z¥€4Z¤€3Y£€3X¢€2X¡€2W €2VŸ€1Vž€1U€0Uœ€0Tœ€0T›€/Sš€/Sš€/S™€.R˜€.R˜€.Q—€.Q—€-Q–€-P–€-P•€-P•€,P•€,O”€,O”€,O“€,O“€,N“€+N’€+N’€+N’€+N‘€+N‘€+M‘€+M‘€+M€*M€*M€*M€*M€*M€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*L€*M€*M€*M€*M€+M€+M‘€+M‘€+N‘€+N‘€+N’€+N’€+N’€,O“€,O“€,O”€,O”€,P•€-P•€-P–€-Q–€-Q—€.Q—€.R˜€.R™€/S™€/Sš€/T›€0T›€0Uœ€1U€1Vž€2VŸ€2W €2X¡€3X¢€4Y£€4Z¤€5[¥€5\§€6]¨€7^©€8_«€8_¬€9`­€:a¯€:b°€;c°€;c±€h·€>g¶€=fµ€f¶€>g·€?h¸€?i¹€?i¹€?i¹€Al½€Ak½€Ak¼€@j»€@i¹€?h¸€>g·€=fµ€f¶€>h·€?i¹€@jº€Aj»€Ak¼€Al½€Al½€DnÀ€CnÀ€Cn¿€Bm¾€Bl½€Ak»€@iº€?h¸€>g·€=f¶€=e´€g¶€>h·€?i¹€@j»€Ak¼€Bl¾€Cm¿€CnÀ€DnÁ€DnÀ€FqÄ€FqÄ€EpÀDp€DnÁ€Cm¿€Bl¾€Ak¼€@j»€?i¹€?h¸€>g·€=fµ€=e´€g·€?h¸€@iº€@j»€Ak½€Bm¾€CnÀ€DoÁ€EpÀEqÄ€FqÄ€FqÄ€HuÈ€HtÈ€GtÇ€GsÆ€FqÄ€EpÀDoÁ€CnÀ€Bm¾€Bl½€Ak¼€@jº€?i¹€?h¸€>g·€>g¶€=fµ€=e´€f¶€>g·€?h¸€?i¹€@jº€Ak¼€Bl½€Cm¿€CnÀ€Do€EqÀFrÅ€GsÇ€HtÈ€HuÉ€HuÈ€KxÍ€KxÌ€JwË€IvÊ€HuÉ€GsÇ€FrÅ€FqÄ€EpÀDoÁ€CnÀ€Cm¿€Bl½€Ak¼€Aj»€@jº€?i¹€?h¸€>h·€>g¶€=f¶€=fµ€=e´€g¶€>g·€?h¸€?i¹€@iº€@j»€Ak¼€Bl½€Cm¿€CnÀ€DoÁ€EpÀFqÄ€GrÆ€HtÈ€IuÉ€JvË€JwÌ€KxÍ€KxÍ€N{Ñ€M{Ñ€MzЀLyÏ€KxÍ€JwË€IvÊ€HtÈ€GsÇ€GrÆ€FqÄ€EpÀDo€DoÁ€CnÀ€Cm¿€Bl¾€Al½€Ak¼€@j»€@jº€?i¹€?h¹€?h¸€>g·€>g¶€=f¶€=fµ€=e´€g¶€>g·€?h¸€?i¹€@iº€@jº€Ak»€Ak¼€Bl½€Bm¾€Cn¿€DnÁ€Do€EpÀFqÄ€GrÆ€GtÇ€HuÉ€IvÊ€JwÌ€KxÍ€LzÏ€M{ЀM{Ñ€N{Ñ€PÖ€PÖ€P~Õ€O}Ó€N|Ò€M{ЀLyÏ€KxÍ€JwÌ€IvÊ€IuÉ€HtÈ€GsÇ€GrÆ€FrÅ€EqÄ€EpÀDo€DoÁ€CnÀ€Cm¿€Bm¾€Bl½€Al½€Ak¼€Aj»€@j»€@iº€?i¹€?i¹€?h¸€>h·€>g·€>g¶€>g¶€=fµ€=fµ€=fµ€=e´€f¶€>g¶€>g·€>h·€?h¸€?h¸€?i¹€@iº€@jº€Aj»€Ak¼€Al½€Bl¾€Bm¾€Cm¿€CnÀ€DoÁ€Ep€EpÀFqÄ€FrÅ€GsÇ€HtÈ€IuÉ€IvÊ€JwÌ€KxÍ€LyÏ€M{ЀN|Ò€O}Ô€P~Õ€PÖ€PÖ€V†Þ€V†Þ€U…Ý€T„Ü€SƒÚ€PÕ€O}Ô€N|Ò€M{Ñ€LzÏ€Ly΀KxÍ€JwÌ€JvË€IvÊ€HuÉ€HtÈ€GsÇ€GsÆ€FrÅ€FqÄ€EqÀEpÀDo€DoÁ€CnÀ€CnÀ€Cm¿€Do€DoÁ€DnÀ€CnÀ€Cn¿€Cm¿€Bm¾€Bm¾€Bl½€@iº€@i¹€?i¹€?i¹€?h¸€?h¸€?h¸€>h·€>g·€>g·€>g¶€>g¶€>g¶€>f¶€=f¶€=fµ€=fµ€=fµ€=fµ€=fµ€=fµ€=fµ€=f´€?h¸€?h¸€?h¸€?h¸€?h¸€?h¸€?h¸€?h¸€=f´€=f´€=f´€=f´€=fµ€=fµ€=fµ€=fµ€=fµ€=fµ€=fµ€=f¶€>g¶€>g¶€>g¶€>g·€>g·€>h·€?h¸€?h¸€?h¸€?i¹€?i¹€Bl½€Bl¾€Bm¾€Cm¿€Cm¿€Cn¿€CnÀ€DnÁ€DoÁ€Bm¾€Cm¿€CnÀ€DnÀ€DoÁ€Do€EpÀEqÄ€FqÄ€FrÅ€GsÆ€HtÇ€HtÈ€IuÉ€IvË€JwÌ€KxÍ€Ly΀LzÏ€M{Ñ€N|Ò€O}Ô€PÕ€SƒÚ€T„Ü€U…Ý€V†Þ€V†Þ€Y‹ä€Y‹ã€YŠâ€X‰á€W‡à€V†Þ€U…Ü€TƒÛ€S‚Ú€RØ€O}Ô€N|Ò€N{Ñ€M{ЀLzÏ€Ly΀KxÍ€KxÌ€JwË€JvË€IvÊ€HuÉ€JwË€IvÊ€IvÊ€IuÉ€HuÈ€HtÈ€HtÇ€GsÇ€GsÆ€GrÆ€FrÅ€FrÅ€FqÄ€EqÄ€EqÀEpÀEp€Dp€Do€DoÁ€Bl½€Bl½€Bl½€Al½€Ak¼€Ak¼€Ak¼€Ak¼€Ak»€Ak»€Aj»€Aj»€Bm¾€Bm¾€Bm¾€Bm¾€Bm¾€Bm¾€Bm¾€Bm¾€Bm¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€Bl¾€@jº€@j»€@j»€@j»€Aj»€Ak»€Ak¼€Ak¼€Ak¼€Ak¼€Bl½€Bl½€DoÁ€DoÁ€Dp€Ep€EpÀEqÀEqÄ€FqÄ€FrÄ€FrÅ€FrÅ€GsÆ€GsÆ€GsÇ€HtÇ€HtÈ€HuÉ€IuÉ€IvÊ€IvÊ€HtÈ€IuÉ€IvÊ€JvË€JwÌ€KxÍ€Ly΀LzÏ€MzЀN{Ñ€N|Ò€O}Ô€RØ€S‚Ú€TƒÛ€U…Ü€V†Þ€W‡ß€Xˆá€XŠâ€YŠã€Y‹ä€]é€]é€]è€\Žç€[Œå€Z‹ä€YŠâ€Xˆá€W‡ß€V†Þ€U…Ý€T„Ü€SƒÛ€S‚Ù€PÕ€O~Ô€O}Ó€N|Ò€N|Ñ€O}Ó€O}Ó€N|Ò€N|Ñ€M{Ñ€MzЀLzÏ€LzÏ€Ly΀Ky΀KxÍ€KxÌ€JwÌ€JwË€JwË€IvÊ€IvÊ€IuÉ€IuÉ€HuÉ€HtÈ€HtÈ€HtÇ€GsÇ€GsÇ€GsÆ€EpÀEp€Ep€Dp€Dp€Do€FrÅ€FrÅ€FrÅ€FrÅ€FrÄ€FrÄ€FrÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€FqÄ€DoÁ€DoÁ€DoÁ€Do€Ep€Ep€GsÆ€GsÇ€HtÇ€HtÇ€HtÈ€HuÈ€IuÉ€IuÉ€IuÉ€IvÊ€IvÊ€JvË€JwË€JwÌ€KwÌ€KxÍ€KxÍ€Ky΀Ly΀LzÏ€LzÏ€M{ЀM{Ñ€N|Ñ€N|Ò€O}Ó€M{Ñ€N|Ò€O}Ó€O~Ô€P~Õ€S‚Ù€TƒÛ€T„Ü€U…Ý€V†Þ€W‡ß€Xˆá€XŠâ€Y‹ä€ZŒå€[ç€\è€]é€]é€b•ï€b•ï€a”î€`“í€_’ì€^ê€]é€\Žç€[æ€ZŒå€YŠã€X‰â€Xˆá€Wˆà€V‡ß€V†Þ€S‚Ù€T„Ü€TƒÛ€SƒÚ€S‚Ù€R‚Ù€RØ€Q€×€Q€×€QÖ€PÕ€P~Õ€P~Ô€O~Ô€O}Ó€O}Ó€N|Ò€N|Ò€N|Ñ€M{Ñ€M{ЀMzЀMzÏ€LzÏ€LyÏ€Ly΀Ly΀KxÍ€KxÍ€KxÍ€KxÌ€KxÌ€JwÌ€JwÌ€JwÌ€JwÌ€JwÌ€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€JwË€KwÌ€KxÌ€KxÍ€KxÍ€Ly΀Ly΀Ly΀LzÏ€LzÏ€MzЀM{ЀM{ЀM{Ñ€N|Ñ€N|Ò€N|Ò€O}Ó€O}Ó€O}Ó€O~Ô€P~Ô€PÕ€PÕ€QÖ€Q€×€Q€×€RØ€R‚Ù€S‚Ù€SƒÚ€T„Û€S‚Ù€V†Þ€V‡ß€Wˆà€X‰á€YŠâ€Y‹ã€ZŒå€[æ€\Žç€]è€^ê€_’ë€`“í€a”î€a•ï€b•ï€f›ö€f›õ€fšõ€e™ó€d˜ò€c–ñ€b•ï€a”î€`“í€_’ë€^ê€]é€\Žè€\ç€[æ€ZŒå€Y‹ã€YŠã€X‰â€X‰á€Wˆà€Wˆà€W‡ß€V†Þ€V†Þ€U…Ý€U…Ý€U…Ü€T„Ü€T„Û€TƒÛ€SƒÚ€S‚Ú€S‚Ù€R‚Ù€RØ€RØ€Q€×€Q€×€Q€Ö€QÖ€PÕ€PÕ€P~Õ€P~Ô€O~Ô€O~Ô€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ó€O}Ò€O}Ò€O}Ò€O}Ò€O}Ò€N}Ò€N}Ò€N}Ò€N}Ò€N}Ò€N|Ò€N}Ò€O}Ò€O}Ò€O}Ò€O}Ò€O}Ò€O}Ó€O}Ó€O}Ó€O}Ó€O~Ô€P~Ô€P~Õ€PÕ€PÕ€QÖ€Q€Ö€Q€×€Q€×€R×€RØ€RØ€R‚Ù€S‚Ù€S‚Ù€SƒÚ€TƒÚ€TƒÛ€T„Û€T„Ü€U…Ü€U…Ý€U†Ý€V†Þ€V†Þ€W‡ß€Wˆà€Wˆà€X‰á€X‰â€YŠã€ZŒå€[æ€\Žç€\è€]é€^‘ê€_’ë€`“í€a”î€a•ï€b–ð€c˜ò€d™ó€ešô€f›õ€f›ö€k¢ü€k¡ü€k¡û€jŸú€ižù€h÷€gœö€fšõ€e™ó€d˜ò€c—ñ€b–ð€a•ï€a”î€`“í€_’ì€^‘ê€^ê€]é€]é€]è€\Žç€\Žç€[æ€[æ€[Œå€ZŒä€Z‹ä€Z‹ã€YŠã€YŠâ€Y‰â€X‰á€X‰á€Xˆà€Wˆà€W‡à€W‡ß€V‡ß€V†Þ€V†Þ€V†Ý€U…Ý€U…Ý€U…Ü€U„Ü€T„Ü€T„Û€T„Û€T„Û€T„Û€T„Û€T„Û€T„Û€T„Û€TƒÛ€TƒÛ€TƒÛ€TƒÛ€TƒÛ€TƒÛ€TƒÛ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÚ€TƒÛ€TƒÛ€TƒÛ€TƒÛ€T„Û€U„Ü€U…Ü€U…Ý€U…Ý€V†Ý€V†Þ€V†Þ€V†Þ€V‡ß€W‡ß€Wˆà€Wˆà€Xˆà€X‰á€X‰á€X‰â€YŠâ€YŠã€YŠã€Z‹ä€Z‹ä€ZŒä€[Œå€[æ€[æ€\Žç€\Žç€]è€]é€^é€^‘ê€_’ì€`“í€a”î€b•ï€b–ð€c—ñ€d˜ò€e™ó€fšõ€gœö€h÷€hžø€iŸú€j û€k¡ü€k¢ü€8T8T8S€7S€7Rm¤þ€l£ý€k¡ü€j û€jŸù€ižø€h÷€gœö€f›õ€fšô€e™ô€d˜ò€d˜ò€c—ñ€c—ñ€b–ð€b–ï€b•ï€a•î€a”î€`“í€`“í€`“ì€_’ì€_’ë€_‘ë€^‘ê€^ê€^é€]é€]è€]è€\Žè€\Žç€\Žç€\æ€[æ€[æ€[Œå€[Œå€ZŒå€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ä€Z‹ã€Z‹ã€Z‹ã€Z‹ã€Z‹ã€Z‹ã€Z‹ã€Z‹ã€Y‹ã€Y‹ã€Y‹ã€Y‹ã€Y‹ã€YŠã€YŠã€YŠã€YŠã€YŠã€Y‹ã€Y‹ã€Y‹ã€Y‹ã€Y‹ã€Y‹ã€Z‹ã€Z‹ä€ZŒå€[Œå€[Œå€[æ€[æ€\æ€\Žç€\Žç€\Žç€]è€]è€]è€]é€^é€^ê€^‘ê€^‘ê€_‘ë€_’ë€_’ì€`“ì€`“í€`“í€a”î€a•î€b•ï€b•ï€b–ð€c—ñ€c—ñ€d˜ò€ešô€f›õ€g›ö€gœ÷€hø€ižù€jŸú€j û€k¡ü€l£ý€m¤þ€7R7S€8S€8T8T;X„;X„;W„:Wƒ:Vƒ9U‚9U‚8T8T8S€7S€7Rm¤þ€m£ý€l¢ü€k¡û€j ú€j ú€jŸù€iŸù€ižø€hžø€h÷€g÷€gœö€gœö€f›õ€f›õ€fšô€ešô€ešó€e™ó€d™ó€d˜ò€d˜ò€c—ñ€c—ñ€c—ñ€c–ð€b–ð€b–ï€b•ï€b•ï€a•î€a”î€a”î€a”í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“í€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“ì€`“í€`“í€`”í€a”î€a”î€a•î€b•ï€b•ï€b–ï€b–ð€b–ð€c—ð€c—ñ€c—ñ€c˜ñ€d˜ò€d˜ò€d™ò€e™ó€e™ó€ešô€ešô€fšô€f›õ€f›õ€gœö€gœö€g÷€h÷€hžø€ižø€iŸù€jŸù€j ú€k¡ü€l£ý€m£ý€m¤þ€7R7S€8S€8T8T9U‚9U‚:Vƒ:Wƒ;W„;X„;X„?\‡?\‡>[‡>[†=Z†=Z…[†>[‡?\‡?\‡B`ŠB`ŠB`‰B_‰A_‰A^‰@^ˆ@]ˆ?]ˆ?\‡?\‡>[‡>[†=Z†=Z†=Z…[†>[†>[‡?\‡?\‡?]ˆ@]ˆ@^ˆA^ˆA_‰A_‰B`‰B`ŠB`ŠFdŒFdŒFd‹Ed‹Ec‹Ec‹Db‹DbŠDaŠCaŠCaŠC`‰B`‰B_‰B_‰A_‰A^ˆA^ˆA^ˆ@^ˆ@]ˆ@]ˆ@]‡@]‡?]‡?\‡?\‡?\‡?\‡?\‡>[†>[†>[†>[†>[†>[†>Z†=Z†=Z…=Z…=Z…=Z…=Z…=Y…=Y…=Y…Z†>Z†>[†>[†>[†>[†>[†>[†?[†?\‡?\‡?\‡?\‡?\‡?]‡@]‡@]‡@]ˆ@]ˆ@^ˆA^ˆA^ˆA_‰B_‰B`‰B`‰C`ŠCaŠCaŠDaŠDbŠDb‹Ec‹Ec‹Ed‹Fd‹FdŒFdŒJhŒJhŒJhŒIhŒIgŒIgŒHgŒHfŒHfŒGf‹Ge‹Ge‹Ge‹Fe‹Fd‹Fd‹Fd‹Ec‹Ec‹Ec‹EcŠEcŠEbŠDbŠDbŠDbŠDbŠDbŠDaŠDaŠCaŠCaŠCa‰Ca‰C`‰C`‰C`‰C`‰B`‰B`‰B`‰B`‰B_‰B_ˆB_‰B_‰B_ˆB_ˆB_ˆB_ˆB_ˆB_ˆB_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆA_ˆB_ˆB_‰B_‰B_‰B_‰B`‰B`‰B`‰B`‰C`‰C`‰C`‰C`‰C`‰Ca‰Ca‰CaŠCaŠCaŠDaŠDaŠDbŠDbŠDbŠDbŠDbŠEbŠEcŠEcŠEc‹Ec‹Fd‹Fd‹Fd‹Fe‹Ge‹Ge‹Ge‹Gf‹HfŒHfŒHgŒIgŒIgŒIhŒJhŒJhŒJhŒMkŠMkŠMkŠLk‹Lk‹Lj‹Lj‹Kj‹Kj‹Ki‹Ki‹Ki‹Ki‹Ji‹Ji‹Jh‹Jh‹Jh‹Jh‹Jh‹Ig‹Ig‹Ig‹Ig‹IgŠIgŠIgŠHgŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠGdŠGdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠFdŠGdŠGeŠGeŠGeŠGeŠGe‹Ge‹GeŠGeŠGeŠGeŠGeŠGeŠGeŠGeŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠHfŠIgŠIgŠIgŠIg‹Ig‹Ig‹Ih‹Jh‹Jh‹Jh‹Ji‹Ji‹Ki‹Ki‹Ki‹Ki‹Ki‹Kj‹Kj‹Lj‹Lj‹Lj‹Lk‹MkŠMkŠMkŠNl…Nl…Nl†Nl†Nl†Nl†Nl†Nk†Nk‡Mk‡Mk‡Mk‡Mk‡MkˆMkˆMkˆMkˆMkˆMkˆMkˆMjˆLjˆLjˆLjˆLjˆLjˆLjˆLjˆLjˆLiˆLiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKi‰Ki‰Ki‰Ki‰Ki‰Ki‰Ki‰KiˆKiˆKiˆKhˆKhˆKhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆJhˆKhˆKhˆKiˆKiˆKi‰Ki‰Ki‰Ki‰Ki‰Ki‰Ki‰KiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆKiˆLiˆLiˆLiˆLjˆLjˆLjˆLjˆLjˆLjˆLjˆLjˆMkˆMkˆMkˆMkˆMkˆMkˆMkˆMk‡Mk‡Mk‡Nk‡Nk‡Nk†Nl†Nl†Nl†Nl†Nl†Nl…Nl…Öû€Öû€œÖû€œÖü€œÖü€œÖý€œÖý€œÖþ€œÖþ€NkNkNkNk€Nk€Nk€Nk€Nk€Nk€Nk€Nk€Nj€Mj€Mj€Mj€MjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚MjLiLiLiLiLiLiLiLiLiLiLiLiLiLiLj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚Mj‚MjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjMjNjNkNkNk€Nk€Nk€Nk€Nk€Nk€NkNkœÖþ€œÖþ€œÖþ€œÖý€œÖý€œÖü€œÖü€œÖü€Öû€Öû€™Ï怙Ï怙Ï怙Ï瀙Ï瀙Ï瀙Ï耙Ï耙Ï耙Ï耙Ï耙Ï這Ï這Ï這Ï這Ï這Ï這ÏꀙÏꀙÏꀙÏꀙÏ뀙Ï뀙Ï뀙Ï쀙Ï쀙Ï쀙Ï쀙Ð쀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀘Ð퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘ÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏÏ퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ï퀘Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð퀙Ð쀙Ï쀙Ï쀙Ï쀙Ï쀙Ï뀙Ï뀙Ï뀙Ï뀙ÏꀙÏꀙÏꀙÏ這Ï這Ï這Ï這Ï這Ï耙Ï耙Ï耙Ï耙Ï耙Ï耙Ï瀙Ï瀙Ï瀙Ï怙Ï怙Ï怓ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÑ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÓ€“ÆÓ€“ÆÓ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÕ€“ÆÕ€“ÆÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÇÕ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÖ€“ÇÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÆÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÇÕ€“ÆÕ€“ÆÕ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÔ€“ÆÓ€“ÆÓ€“ÆÓ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÒ€“ÆÒ€“ÆÒ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€“ÆÑ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€Ž¿Ä€Ž¿Ä€Ž¿Ä€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ä€Ž¿Ä€Ž¿Ä€Ž¿Å€ŽÀÅ€ŽÀÅ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÅ€Ž¿Å€Ž¿Å€¿Ä€¿Ä€¿Ä€¿Ä€¿Ä€¿Ä€Ž¿Å€Ž¿Å€ŽÀÅ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÇ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÅ€Ž¿Å€Ž¿Å€¿Ä€¿Ä€¿Ä€¿Ä€¿Ä€¿Ä€Ž¿Å€Ž¿Å€ŽÀÅ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÆ€ŽÀÅ€ŽÀÅ€Ž¿Å€Ž¿Ä€Ž¿Ä€Ž¿Ä€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ã€Ž¿Ä€Ž¿Ä€Ž¿Ä€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€ŽÀÄ€Œ½¾€Œ½¾€Œ½¾€Œ½¾€Œ½¿€Œ½¿€Œ½¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼À€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¿€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¾€‹¼¿€‹¼¿€‹¼¿€‹¼¿€Œ½¿€Œ½¿€Œ½¿€Œ½¿€Œ½¾€Œ½¾€Œ½¾€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»¾€Š»¾€Š»¾€Š»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€Š»¾€Š»¾€Š»¾€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€Š»½€‰»½€‰»½€‰»½€‰»½€‰»½€‰»½€‰»½€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»¾€‰»½€‰»½€‰»½€‰»½€‰»½€‰»½€‰»½€‰»¾€‰»¾€‰»¾€‰»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»À€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€ˆ»¾€‰»¾€‰»¾€‰»¾€‰»¾€ˆ»¾€ˆ»¾€ˆ»¿€ˆ»¿€ˆ»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»À€‡»À€‡»À€‡»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»Á€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€†»À€‡»À€‡»À€‡»À€‡»À€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€‡»¿€ˆ»¿€ˆ»¿€ˆ»¿€ˆ»¾€ˆ»¾€‡»¿€‡»¿€‡»¿€‡»¿€‡»À€‡»À€†»À€†»À€†»À€†»À€†»À€†»Á€†»Á€†»Á€†»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Á€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€„»Â€„»Â€„»Â€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ä€„»Ä€„»Ä€„»Ä€„»Ä€„»Ä€„»Ä€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Â€„»Â€„»Â€„»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Â€…»Á€…»Á€…»Á€…»Á€…»Á€†»Á€†»Á€†»Á€†»Á€†»À€†»À€†»À€†»À€†»À€‡»À€‡»À€‡»À€‡»¿€‡»¿€‡»¿€†»À€†»À€†»À€†»À€†»Á€†»Á€…»Á€…»Á€…»Á€…»Á€…»Â€…»Â€…»Â€…»Â€„»Â€„»Â€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Â€„»Â€…»Â€…»Â€…»Â€…»Â€…»Á€…»Á€…»Á€…»Á€†»Á€†»Á€†»À€†»À€†»À€†»À€…»Á€…»Á€…»Á€…»Â€…»Â€…»Â€…»Â€„»Â€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Å€‚»Å€‚»Å€‚»Å€‚»Å€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»È€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Å€‚»Å€‚»Å€‚»Å€‚»Å€ƒ»Å€ƒ»Å€ƒ»Å€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€„»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Â€„»Â€…»Â€…»Â€…»Â€…»Á€…»Á€…»Á€„»Â€„»Â€„»Â€„»Ã€„»Ã€„»Ã€„»Ã€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Å€‚»Å€‚»Å€‚»Å€‚»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Æ€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»È€»È€»È€»È€€»È€€»È€€»È€€»É€€»É€€»É€€»É€€»É€€»É€€»É€€»Ê€€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€€»Ê€€»Ê€€»É€€»É€€»É€€»É€€»É€€»É€€»É€€»É€€»È€€»È€€»È€€»È€»È€»È€»È€»È€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Ç€»Æ€‚»Æ€‚»Æ€‚»Æ€‚»Å€‚»Å€ƒ»Å€ƒ»Å€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ã€„»Ã€„»Ã€„»Ã€„»Ã€„»Â€„»Â€ƒ»Ã€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Å€ƒ»Å€‚»Å€‚»Å€‚»Æ€‚»Æ€‚»Æ€»Ç€»Ç€»Ç€»È€»È€€»È€€»È€€»È€€»É€€»É€€»É€€»É€€»É€€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€»Ë€»Ë€»Ë€»Ë€¼Ë€¼Ë€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€}¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ë€¼Ë€¼Ë€»Ë€»Ë€»Ë€»Ë€»Ê€»Ê€»Ê€»Ê€»Ê€»Ê€€»Ê€€»É€€»É€€»É€€»É€€»É€€»É€€»È€€»È€»È€»Ç€»Ç€»Ç€‚»Æ€‚»Æ€‚»Æ€‚»Å€‚»Å€‚»Å€ƒ»Å€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ä€ƒ»Ã€ƒ»Å€‚»Å€‚»Å€‚»Å€‚»Æ€‚»Æ€»Æ€»Ç€»Ç€»È€»È€€»È€€»É€€»É€€»É€»Ê€»Ë€»Ê€»Ë€»Ë€»Ë€¼Ë€¼Ë€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Í€~¼Í€~¼Í€~¼Í€}¼Í€}¼Î€}¼Î€}¼Î€}¼Î€}¼Î€}¼Î€}¼Ï€}¼Ï€|¼Ï€|¼Ï€|¼Ï€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ï€|¼Ï€}¼Ï€}¼Ï€}¼Ï€}¼Î€}¼Î€}¼Î€}¼Î€}¼Î€}¼Î€}¼Í€}¼Í€~¼Í€~¼Í€~¼Í€~¼Í€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ì€~¼Ë€¼Ë€»Ë€»Ë€»Ë€¼Ë€»Ê€€»É€€»É€€»É€€»È€»È€»È€»Ç€»Ç€»Æ€‚»Æ€‚»Æ€‚»Å€‚»Å€‚»Å€ƒ»Å€‚»Æ€‚»Æ€»Ç€»Ç€»Ç€»È€€»È€€»É€€»É€€»É€»Ê€»Ê€»Ë€»Ë€~¼Í€}¼Í€}¼Î€}¼Î€}¼Î€}¼Í€}¼Í€}¼Î€}¼Î€}¼Î€}¼Î€}¼Ï€}¼Ï€|¼Ï€|¼Ï€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ñ€|½Ñ€{½Ñ€{½Ñ€{½Ò€{½Ò€{½Ò€{½Ò€{½Ò€{½Ó€{½Ó€z½Õ€y½Õ€y½Õ€y½Õ€y½Õ€y½Õ€z½Ó€z½Ó€z½Ó€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€z½Ô€y½Õ€y½Õ€y½Õ€y½Õ€y½Õ€y½Õ€{½Ó€{½Ó€{½Ò€{½Ò€{½Ò€{½Ò€{½Ñ€{½Ñ€{½Ñ€{½Ñ€|¼Ñ€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ð€|¼Ï€|¼Ï€|¼Ï€}¼Ï€}¼Ï€}¼Î€}¼Î€}¼Î€}¼Î€}¼Í€}¼Î€}¼Î€}¼Î€}¼Í€~¼Í€»Ë€»Ë€»Ê€»Ê€€»É€€»É€€»É€€»È€»È€»Ç€»Ç€»Ç€»Æ€‚»Æ€»È€»È€€»È€€»É€€»É€€»É€»Ê€»Ê€»Ë€~¼Ë€}¼Í€}¼Î€}¼Î€}¼Ï€|¼Ï€|¼Ð€|¼Ð€|¼Ð€{½Ñ€{½Ñ€{½Ò€{½Ò€|¼Ñ€{½Ñ€{½Ñ€{½Ñ€{½Ò€{½Ò€{½Ò€{½Ò€{½Ó€z½Ó€z½Ó€z½Ó€z½Ô€z½Ô€z½Ô€z½Ô€z½Õ€y½Õ€y½Õ€y½Õ€x¾Ø€x¾Ø€x¾Ø€x¾Ø€x¾Ø€x¾Ø€x¾Ù€x¾Ù€w¾Ù€w¾Ù€w¾Ù€w¾Ù€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€x¾×€w¾Ù€w¾Ù€w¾Ù€w¾Ù€w¾Ù€w¾Ù€w¾Ù€x¾Ù€x¾Ø€x¾Ø€x¾Ø€x¾Ø€y½Ö€y½Õ€y½Õ€y½Õ€z½Õ€z½Ô€z½Ô€z½Ô€z½Ô€z½Ó€z½Ó€z½Ó€{½Ó€{½Ò€{½Ò€{½Ò€{½Ò€{½Ñ€{½Ñ€{½Ñ€{½Ò€{½Ò€{½Ñ€{½Ñ€|¼Ñ€|¼Ð€|¼Ð€|¼Ï€}¼Ï€}¼Î€}¼Î€}¼Í€~¼Ë€»Ë€»Ê€»Ê€€»Ê€€»É€€»É€€»È€»È€»È€€»É€€»Ê€»Ê€»Ê€»Ë€~¼Í€}¼Í€}¼Î€}¼Ï€|¼Ï€|¼Ð€|¼Ð€|¼Ñ€{½Ñ€{½Ò€{½Ò€z½Ó€z½Ó€z½Ô€z½Ô€z½Ô€y½Õ€y½Õ€y½Ö€y¾Ö€y¾Ö€x¾×€x¾×€y½Õ€y½Ö€y½Ö€y¾Ö€y¾Ö€x¾×€x¾×€x¾×€x¾Ø€w¾Ú€w¾Ú€w¾Û€v¿Û€v¿Û€v¿Û€v¿Û€v¿Ü€v¿Ü€v¿Ü€v¿Ü€v¿Ü€v¿Ü€v¿Ý€v¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Þ€u¿Þ€v¿Û€v¿Û€v¿Û€v¿Û€v¿Û€v¿Û€v¿Û€v¿Û€u¿Þ€u¿Þ€u¿Þ€u¿Þ€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Ý€v¿Ý€v¿Ý€v¿Ü€v¿Ü€v¿Ü€v¿Ü€v¿Ü€v¿Û€v¿Û€v¿Û€w¿Û€w¾Ú€x¾Ø€x¾Ø€x¾×€x¾×€x¾×€y¾Ö€y¾Ö€y½Ö€y½Ö€x¾×€x¾×€x¾×€y¾Ö€y¾Ö€y½Ö€y½Õ€y½Õ€z½Ô€z½Ô€z½Ó€z½Ó€{½Ò€{½Ò€{½Ñ€{½Ñ€|¼Ð€|¼Ð€|¼Ï€}¼Ï€}¼Î€}¼Í€~¼Í€»Ë€»Ê€»Ê€»Ê€€»É€~¼Ì€~¼Í€~¼Í€}¼Î€}¼Î€}¼Ï€|¼Ð€|¼Ð€{¼Ñ€{½Ñ€{½Ò€z½Ó€z½Ó€z½Ô€z½Ô€y½Õ€y½Õ€y¾Ö€y¾Ö€x¾×€x¾×€x¾Ø€x¾Ø€w¾Ù€w¾Ù€w¾Ú€w¾Ú€w¾Ú€v¿Û€v¿Û€v¿Û€v¿Ü€v¿Ü€v¿Ý€u¿Ý€u¿Ý€u¿Ý€u¿Þ€u¿Þ€u¿Þ€u¿ß€u¿ß€t¿ß€t¿ß€tÀ߀tÀà€tÀà€tÀà€tÀà€tÀà€tÀá€tÀá€tÀá€tÀá€sÀá€sÀá€sÀá€sÀá€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀâ€sÀá€sÀá€sÀá€sÀá€sÀá€tÀá€tÀá€tÀá€tÀà€tÀà€tÀà€tÀà€tÀà€tÀ߀t¿ß€u¿ß€u¿ß€u¿Þ€u¿Þ€u¿Þ€u¿Ý€u¿Ý€v¿Ý€v¿Ü€v¿Ü€v¿Ü€v¿Û€v¿Û€w¾Ú€w¾Ú€w¾Ú€w¾Ù€w¾Ù€x¾Ø€x¾Ø€x¾×€x¾×€y¾Ö€y½Ö€y½Õ€z½Õ€z½Ô€z½Ó€z½Ó€{½Ò€{½Ñ€{¼Ñ€|¼Ð€|¼Ï€}¼Ï€}¼Î€}¼Í€~¼Í€~¼Í€~¼Ì€}¼Î€}¼Ï€|¼Ï€|¼Ð€|¼Ð€{½Ñ€{½Ò€{½Ò€z½Ó€z½Ô€z½Ô€y½Õ€y½Ö€y¾Ö€x¾×€x¾Ø€x¾Ø€w¾Ù€w¾Ù€w¾Ú€w¾Ú€v¿Û€v¿Û€v¿Ü€v¿Ü€u¿Ý€u¿Ý€u¿Þ€u¿Þ€u¿ß€t¿ß€tÀ߀tÀà€tÀà€tÀá€tÀá€sÀá€sÀâ€sÀâ€sÀâ€sÀâ€sÀã€sÀã€rÀã€rÀã€rÀä€rÁä€rÁä€rÁä€rÁå€rÁå€rÁå€rÁå€rÁå€qÁå€qÁå€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁæ€qÁå€qÁå€rÁå€rÁå€rÁå€rÁå€rÁå€rÁä€rÁä€rÁä€rÀä€rÀã€rÀã€sÀã€sÀã€sÀâ€sÀâ€sÀâ€sÀá€tÀá€tÀá€tÀà€tÀà€t¿ß€t¿ß€u¿Þ€u¿Þ€u¿Ý€u¿Ý€v¿Ü€v¿Ü€v¿Û€v¿Û€w¾Ú€w¾Ú€w¾Ù€x¾Ù€x¾Ø€x¾×€x¾×€y½Ö€y½Õ€z½Ô€z½Ô€z½Ó€{½Ò€{½Ò€{¼Ñ€|¼Ð€|¼Ï€}¼Ï€}¼Î€}¼Î€|¼Ð€|¼Ð€{½Ñ€{½Ò€{½Ò€z½Ó€z½Ô€y½Õ€y½Ö€y¾Ö€x¾×€x¾Ø€x¾Ù€w¾Ù€w¾Ú€w¾Û€v¿Û€v¿Ü€v¿Ü€u¿Ý€u¿Þ€u¿Þ€u¿ß€tÀ߀tÀà€tÀà€tÀá€sÀá€sÀâ€sÀâ€sÀã€rÀã€rÀä€rÁä€rÁä€rÁå€rÁå€qÁå€qÁæ€qÁæ€qÁç€qÁç€qÁç€qÁç€pÁè€pÁè€pÁè€pÂè€pÂé€pÂé€pÂé€pÂé€pÂé€pÂé€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€oÂê€pÂê€pÂé€pÂé€pÂé€pÂé€pÂé€pÂè€pÂè€pÁè€pÁè€qÁç€qÁç€qÁç€qÁæ€qÁæ€qÁæ€rÁå€rÁå€rÁä€rÁä€rÀã€sÀã€sÀã€sÀâ€sÀâ€sÀá€tÀà€tÀà€t¿ß€u¿ß€u¿Þ€u¿Ý€u¿Ý€v¿Ü€v¿Û€v¿Û€w¾Ú€w¾Ù€x¾Ù€x¾Ø€x¾×€y¾Ö€y½Õ€z½Õ€z½Ô€z½Ó€{½Ò€{½Ñ€{¼Ñ€|¼Ð€|¼Ð€{½Ò€{½Ò€z½Ó€z½Ô€z½Ô€y½Õ€y¾Ö€x¾×€x¾Ø€w¾Ù€w¾Ú€w¾Ú€v¿Û€v¿Ü€u¿Ý€u¿Ý€u¿Þ€t¿ß€tÀà€tÀà€tÀá€sÀâ€sÀâ€sÀã€rÀã€rÁä€rÁä€rÁå€qÁæ€qÁæ€qÁç€qÁç€pÁç€pÁè€pÂè€pÂé€pÂé€pÂê€oÂê€oÂê€oÂë€oÂë€oÂë€oÂì€nÂì€nÂì€nÃì€nÃí€nÃí€nÃí€nÃí€nÃî€nÃî€nÃî€mÃî€mÃî€mÃî€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃï€mÃî€mÃî€nÃî€nÃî€nÃî€nÃî€nÃí€nÃí€nÃí€nÃí€nÂì€nÂì€oÂì€oÂë€oÂë€oÂë€oÂê€oÂê€pÂé€pÂé€pÂè€pÁè€qÁç€qÁç€qÁæ€qÁæ€rÁå€rÁå€rÁä€rÀã€sÀã€sÀâ€sÀá€tÀá€tÀà€t¿ß€u¿Þ€u¿Þ€u¿Ý€v¿Ü€v¿Û€w¾Ú€w¾Ú€w¾Ù€x¾Ø€x¾×€y½Ö€y½Õ€z½Ô€z½Ó€z½Ó€{½Ò€{½Ò€z½Ô€z½Õ€y½Õ€y½Ö€x¾×€x¾Ø€x¾Ù€w¾Ù€w¾Ú€v¿Û€v¿Ü€u¿Ý€u¿Þ€t¿ß€tÀà€tÀá€sÀá€sÀâ€sÀã€rÀä€rÁä€rÁå€qÁæ€qÁæ€qÁç€pÁè€pÂè€pÂé€pÂé€oÂê€oÂê€oÂë€oÂë€nÂì€nÂì€nÃí€nÃí€nÃî€mÃî€mÃï€mÃï€mÃï€mÃð€mÃð€lÃð€lÃñ€lÄñ€lÄñ€lÄñ€lÄò€lÄò€lÄò€lÄò€kÄó€kÄó€kÄó€kÄó€kÄó€kÄó€kÄó€kÄó€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄô€kÄó€kÄó€kÄó€kÄó€kÄó€kÄó€kÄó€lÄò€lÄò€lÄò€lÄò€lÄñ€lÄñ€lÃñ€lÃð€mÃð€mÃð€mÃï€mÃï€mÃî€nÃî€nÃí€nÃí€nÂì€nÂì€oÂë€oÂë€oÂê€pÂê€pÂé€pÂè€pÁè€qÁç€qÁæ€qÁæ€rÁå€rÁä€rÀã€sÀâ€sÀâ€tÀá€tÀà€t¿ß€u¿Þ€u¿Ý€v¿Ü€v¿Û€w¾Ú€w¾Ù€x¾Ø€x¾×€y¾Ö€y½Õ€y½Õ€z½Ô€z½Ô€x¾×€x¾×€x¾×€x¾Ø€w¾Ù€w¾Ú€v¿Û€v¿Ü€u¿Ý€u¿Þ€t¿ß€tÀà€tÀá€sÀâ€sÀã€rÀä€rÁä€qÁå€qÁæ€qÁç€pÁè€pÂè€pÂé€oÂê€oÂë€oÂë€nÂì€nÃí€nÃí€nÃî€mÃî€mÃï€mÃð€mÃð€lÃñ€lÄñ€lÄò€lÄò€kÄó€kÄó€kÄó€kÄô€kÄô€kÄõ€jÄõ€jÄõ€jÅõ€jÅö€jÅö€jÅö€jÅö€iÅ÷€iÅ÷€iÅ÷€iÅ÷€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅø€iÅ÷€iÅ÷€iÅ÷€iÅ÷€iÅ÷€jÅö€jÅö€jÅö€jÅö€jÄõ€jÄõ€kÄô€kÄô€kÄô€kÄó€kÄó€lÄò€lÄò€lÄñ€lÃñ€mÃð€mÃï€mÃï€mÃî€nÃí€nÃí€nÂì€oÂë€oÂë€oÂê€pÂé€pÂè€qÁç€qÁç€qÁæ€rÁå€rÀä€sÀã€sÀâ€tÀá€tÀà€u¿ß€u¿Þ€v¿Ý€v¿Û€w¾Ú€w¾Ù€x¾Ø€x¾Ø€x¾×€y¾Ö€x¾×€w¾Ù€w¾Ù€w¾Ù€w¾Ú€v¿Û€v¿Ü€u¿Ý€u¿Þ€tÀ߀tÀá€sÀâ€sÀã€rÀä€rÁå€qÁæ€qÁç€pÁè€pÂé€pÂê€oÂê€oÂë€nÂì€nÃí€nÃî€mÃî€mÃï€mÃð€lÃñ€lÄñ€lÄò€kÄó€kÄó€kÄô€kÄô€jÄõ€jÅõ€jÅö€jÅö€iÅ÷€iÅ÷€iÅø€iÅø€iÅù€hÅù€hÅù€hÅú€hÅú€hÅú€gÅú€gÆû€gÆû€gÆû€gÆû€gÆü€gÆü€gÆü€fÆü€fÆü€fÆü€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆý€fÆü€fÆü€fÆü€gÆü€gÆü€gÆü€gÆû€gÆû€gÆû€gÆû€hÅú€hÅú€hÅú€hÅù€hÅù€iÅø€iÅø€iÅø€iÅ÷€jÅ÷€jÅö€jÅõ€jÄõ€kÄô€kÄô€kÄó€lÄò€lÄñ€lÃñ€mÃð€mÃï€mÃî€nÃí€nÃí€oÂì€oÂë€oÂê€pÂé€pÁè€qÁç€qÁæ€rÁå€rÀä€sÀâ€sÀá€tÀà€t¿ß€u¿Þ€u¿Ý€v¿Ü€w¾Û€w¾Ú€w¾Ù€w¾Ù€w¾Ù€v¿Û€v¿Û€v¿Ü€u¿Ý€u¿Þ€u¿ß€tÀà€tÀá€sÀâ€rÀã€rÁä€qÁæ€qÁç€pÁè€pÂé€oÂê€oÂë€nÂì€nÃí€nÃî€mÃï€mÃð€lÃñ€lÄñ€lÄò€kÄó€kÄô€jÄõ€jÄõ€jÅö€iÅ÷€iÅ÷€iÅø€iÅø€hÅù€hÅù€hÅú€gÆû€gÆû€gÆü€gÆü€fÆü€fÆý€fÆý€fÆþ€eÆþ€eÆþ€2c2c2c2c2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€1c2c1c1c1c1c1c2c2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c2c2ceÆþ€eÆþ€fÆþ€fÆý€fÆý€fÆü€gÆü€gÆû€gÆû€hÅú€hÅú€hÅù€iÅø€iÅø€iÅ÷€jÅö€jÅõ€jÄõ€kÄô€kÄó€lÄò€lÄñ€mÃð€mÃï€mÃî€nÃí€nÂì€oÂë€oÂê€pÂé€pÁè€qÁæ€rÁå€rÁä€sÀã€sÀâ€tÀà€t¿ß€u¿Þ€u¿Ý€v¿Ü€v¿Û€v¿Û€v¿Û€u¿Þ€u¿Þ€u¿Þ€t¿ß€tÀà€sÀá€sÀâ€rÀä€rÁå€qÁæ€qÁç€pÂè€pÂê€oÂë€nÂì€nÃí€mÃî€mÃï€lÃð€lÄñ€lÄò€kÄó€kÄô€jÄõ€jÅö€iÅ÷€iÅø€iÅø€hÅù€hÅú€gÆû€gÆû€gÆü€fÆü€fÆý€fÆþ€eÆþ€2c2c2c€2c€2c€2c€1c1c1c1c1c1c1c‚1c‚1c‚1c‚1c‚1d‚1d‚0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0dƒ0d‚1d‚1c‚1c‚1c‚1c‚1c‚1c1c1c1c1c2c€2c€2c€2c2ceÆþ€fÆþ€fÆý€fÆü€gÆü€gÆû€hÅú€hÅù€iÅø€iÅø€iÅ÷€jÅö€jÄõ€kÄô€kÄó€lÄò€lÃñ€mÃï€mÃî€nÃí€oÂì€oÂë€pÂé€pÁè€qÁç€qÁå€rÁä€sÀã€sÀá€tÀà€tÀà€u¿ß€u¿Þ€u¿Þ€u¿Þ€tÀá€sÀá€sÀá€sÀâ€sÀã€rÁä€rÁå€qÁæ€qÁç€pÂé€oÂê€oÂì€nÃí€nÃî€mÃï€lÃð€lÄò€kÄó€kÄô€jÄõ€jÅö€iÅ÷€iÅø€hÅù€hÅú€gÆû€gÆû€gÆü€fÆý€fÆþ€2c2c2c€2c€2c€1c1c1c1c‚1c‚1c‚1d‚0dƒ0dƒ0dƒ0dƒ0dƒ0d„0d„0d„0d„0d„0d…/d…/d…/d…/d…/d…/d…/d…/d…/d…/d†/d†/d†/d†/d†/d†/d†/d†/d†/d…/d…/d…/d…/d…/d…/d…/d…0d…0d„0d„0d„0d„0d„0dƒ0dƒ0dƒ0dƒ1d‚1c‚1c‚1c1c1c2c€2c€2c€2cfÆþ€fÆý€fÆü€gÆû€gÅú€hÅù€iÅø€iÅ÷€jÅö€jÄõ€kÄô€kÄó€lÄñ€mÃð€mÃï€nÃí€nÂì€oÂë€pÂé€pÁè€qÁç€rÁå€rÁä€rÀã€sÀâ€sÀá€tÀá€tÀá€tÀá€rÁå€rÁå€rÁå€qÁå€qÁæ€qÁç€pÂè€pÂê€oÂë€oÂì€nÃí€mÃî€mÃð€lÃñ€kÄó€kÄô€jÄõ€jÅö€iÅ÷€iÅø€hÅù€gÅú€gÆû€fÆü€fÆý€eÆþ€2c2c€2c€2c1c1c1c‚1c‚1dƒ0dƒ0dƒ0dƒ0d„0d„0d„/d…/d…/d…/d…/d†/d†/d†/d†/e‡/e‡/e‡.e‡.e‡.e‡.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.eˆ.e‡.e‡.e‡.e‡/e‡/e‡/d†/d†/d†/d†/d…/d…/d…0d…0d„0d„0d„0dƒ0dƒ1d‚1c‚1c‚1c2c2c€2c€2cfÆþ€fÆý€gÆü€gÅû€hÅù€iÅø€iÅ÷€jÄõ€kÄô€kÄó€lÄò€lÃð€mÃï€nÃí€oÂì€oÂë€pÂé€pÂè€qÁç€qÁæ€qÁæ€rÁå€rÁä€rÁä€rÁå€pÂé€pÂé€pÂé€pÂê€oÂê€oÂë€nÂì€nÃí€mÃï€mÃð€lÃñ€lÄò€kÄó€kÄô€jÄõ€iÅ÷€iÅø€hÅù€gÅû€gÆü€fÆý€fÆþ€2c2c€2c€2c1c1c‚1d‚1dƒ0dƒ0dƒ0d„0d„0d…/d…/d…/d†/d†/d†/e‡.e‡.e‡.eˆ.eˆ.eˆ.eˆ.eˆ.e‰.e‰.e‰-e‰-e‰-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-e‹-f‹-f‹-f‹-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-eŠ-e‰.e‰.e‰.e‰.e‰.eˆ.eˆ.eˆ.e‡.e‡/e‡/d†/d†/d†/d…0d…0d„0d„0dƒ0dƒ1dƒ1c‚1c1c2c€2c€2cfÆþ€fÆý€gÆû€hÅú€hÅù€iÅ÷€jÄö€kÄô€kÄó€lÄò€lÃñ€mÃï€mÃî€nÃí€nÂì€oÂë€oÂê€pÂê€pÂé€pÂé€pÂé€pÂé€mÃï€mÃï€mÃð€mÃð€mÃð€lÃñ€lÄò€kÄó€kÄô€jÄõ€jÄö€iÅ÷€iÅø€hÅù€hÅú€gÆû€gÆü€fÆý€fÆþ€2c2c€2c€2c1c1d‚1d‚1dƒ0d„0d„0d„0d…/d…/d†/d†/e†/e‡.e‡.eˆ.eˆ.eˆ.eˆ.e‰.e‰-e‰-eŠ-eŠ-fŠ-f‹-f‹-f‹-f‹-f‹-f‹-f‹-fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,f,f,f,f,f,f,f,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ,fŒ-fŒ-f‹-f‹-f‹-f‹-fŠ-eŠ-eŠ-e‰.e‰.e‰.e‰.eˆ.eˆ.eˆ/e‡/e‡/d†/d†0d…0d…0d„0d„0dƒ1dƒ1d‚1c2c2c€2c€fÆþ€fÆý€gÆü€gÆû€hÅú€iÅù€iÅø€jÅ÷€jÄö€kÄõ€kÄô€kÄó€lÄò€lÃñ€mÃð€mÃï€mÃï€mÃï€mÃï€mÃï€iÅø€iÅø€iÅù€iÅù€iÅù€iÅù€hÅú€hÅú€hÆû€gÆü€gÆü€gÆý€fÆþ€3c2c€2c€2c€2c2c1d‚1d‚1dƒ1dƒ0d„0d„0d„0d…0d…/d†/d†/e†/e‡/e‡/e‡.eˆ.eˆ.e‰.e‰.eŠ-eŠ-eŠ-eŠ-eŠ-f‹-f‹-f‹-f‹-f‹-fŒ-fŒ,fŒ,fŒ,fŒ,f,f,f,f,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,fŽ,f,f,f,f,fŒ,fŒ,fŒ-fŒ-fŒ-f‹-f‹-f‹-f‹-f‹-fŠ-eŠ-eŠ.eŠ.e‰.e‰.eˆ.eˆ/e‡/e‡/e†/d†/d†0d…0d…0d„0d„1dƒ1dƒ1d‚1d‚2c2c2c2c€2c€3cfÆþ€gÆý€gÆü€gÆû€hÆû€hÅú€iÅù€iÅù€iÅù€iÅø€iÅø€iÅø€iÅø€iÅø€1d‚1d‚1d‚1d‚1d‚1d‚1dƒ1dƒ1dƒ1dƒ1dƒ1dƒ1dƒ1d„1d„0d„0d„0d„0d…0d…0d…0d…0d…0d†0d†/e†/e†/e‡/e‡/e‡/e‡/eˆ/eˆ.eˆ.eˆ.e‰.e‰.e‰.e‰.eŠ.eŠ.eŠ-fŠ-f‹-f‹-f‹-f‹-f‹-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-fŒ-f‹-f‹-f‹-f‹-fŠ.eŠ.eŠ.e‰.e‰.e‰.e‰.eˆ.eˆ/eˆ/eˆ/e‡/e‡/e‡/e‡/e†/e†0d†0d†0d…0d…0d…0d…0d„0d„1d„1d„1d„1dƒ1dƒ1dƒ1dƒ1dƒ1dƒ1dƒ1d‚1d‚1d‚1d‚1d‚1d‚1d‚/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ/eˆ \ No newline at end of file diff --git a/Assets/hosek/hosek_radiance_1.hdr b/Assets/hosek/hosek_radiance_1.hdr new file mode 100644 index 0000000000..44e1b7cb1a --- /dev/null +++ b/Assets/hosek/hosek_radiance_1.hdr @@ -0,0 +1,7 @@ +#?RADIANCE +# Output from cmft. +FORMAT=32-bit_rle_rgbe +EXPOSURE=1 + +-Y 32 +X 64 +/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€/Sš€2WŸ€2WŸ€2WŸ€2WŸ€2Vž€2Vž€1Vž€1V€1V€1U€1Uœ€0Uœ€0Uœ€0T›€0T›€0Tš€/Sš€/S™€/S™€/S™€/R˜€.R˜€.R—€.Q—€.Q—€.Q–€.Q–€.Q–€.Q–€.Q–€.Q–€-Q–€-Q–€-Q–€.Q–€.Q–€.Q–€.Q–€.Q–€.Q–€.Q—€.R—€.R˜€.R˜€/R˜€/S™€/S™€/Sš€0Tš€0T›€0T›€0U›€0Uœ€1Uœ€1Uœ€1U€1V€1Vž€2Vž€2Vž€2WŸ€2WŸ€2WŸ€2WŸ€5[¥€5[¥€5[¥€5[¥€5[¤€5Z¤€4Z£€4Y£€4Y¢€3X¡€3X €2WŸ€2Vž€1V€1U€0Uœ€0T›€0Tš€/Sš€/S™€/R˜€.R˜€.R˜€.R—€.Q—€.Q–€-Q–€-P•€-P•€-P•€-P•€-P•€-P•€-P•€-P•€-P•€-P•€-Q–€-Q–€.Q–€.Q—€.R—€.R˜€.R˜€/R˜€/S™€0Tš€0T›€0Uœ€1U€1V€2Vž€2WŸ€3X €3X¡€4Y¢€4Y£€4Z£€5Z¤€5[¤€5[¥€5[¥€5[¥€5[¥€9a­€9a­€9`­€9`¬€8_«€8_ª€7^©€7]¨€6\§€6\¦€5[¥€4Z¤€4Y¢€3X¡€2W €2WŸ€1Vž€1U€0Uœ€0T›€0T›€/Sš€/S™€/R™€.R˜€.R˜€.R˜€.R—€.Q—€.Q—€.Q–€.Q–€.Q–€.Q–€.Q–€.Q—€.Q—€.R—€.R—€.R˜€.R˜€/S™€/Sš€0Tš€0T›€0T›€1U€1Vž€2WŸ€2W €3X¡€4Y¢€4Z£€5[¥€5\¦€6\§€7]¨€7^©€8_ª€9_«€9`¬€9`­€9a­€9a­€>fµ€>fµ€=f´€=e´€fµ€>fµ€Bm½€Bl½€Bl¼€Ak»€Ajº€@i¸€?h·€>fµ€=e³€fµ€?h·€@i¹€Ajº€Ak»€Bl¼€Bm½€Bm½€HtÆ€HsÆ€GsÅ€FrÄ€Ep€DoÀ€Cm¾€Bl¼€Ajº€@i¹€?h·€>gµ€=f´€fµ€?h¶€@i¸€Ajº€Bl¼€Cm¾€DoÀ€Ep€FrÄ€GsÅ€HsÆ€HtÆ€N{ЀN{ЀMzÏ€LyÍ€KxË€JvÉ€HtÇ€GsÅ€FqÀEpÁ€Dn¿€Cm¾€Bl¼€Ak»€Ajº€@i¹€@i¸€?h·€>g¶€>gµ€=f´€=e³€=e²€f´€>gµ€?h¶€?h·€@i¸€@j¹€Ak»€Bl¼€Cm½€Dn¿€EoÁ€FqÀGsÅ€HtÇ€JvÉ€KxË€LyÍ€MzÏ€N{ЀN{ЀZ‹â€Z‹á€YŠà€Xˆß€R€Ö€PÓ€O}Ñ€N{Ï€LyÍ€KxË€JwÊ€IuÈ€ItÇ€MzÍ€LyÌ€LxË€KwÊ€JwÉ€JvÈ€Do¿€Dn¾€Cm¾€Cm½€Bl¼€Bl¼€Bl¼€Bl»€Bl»€Bl»€GrÀGrÀGrÀGrÀGrÀGr€Bk»€Bk»€Bk»€Bl»€Bl¼€Bl¼€Cm½€Cm½€Dn¾€Dn¿€JvÈ€JvÉ€KwÊ€KxÊ€LyË€LyÌ€HtÆ€IuÈ€JvÉ€KxË€LyÍ€M{Ï€O}Ñ€P~Ó€R€Ö€Xˆß€YŠà€Z‹á€Z‹â€c—í€c–í€b•ì€a”ê€`’è€^æ€]Žä€V…Û€T„Ù€XˆÝ€X‡Ü€W†Û€V†Ú€V…Ù€U„Ù€U„Ø€Tƒ×€T‚Ö€SÕ€R€Ô€R€Ó€QÒ€P~Ñ€KwÉ€JvÉ€O}ЀP}ЀP}ЀP}ЀP}ЀP}ЀP}ЀP}ЀP}ЀP}ЀP}ЀO}Ï€O}Ï€O}Ï€JvÈ€JwÉ€P~Ñ€QÒ€R€Ó€R€Ô€SÕ€S‚Ö€TƒÖ€Tƒ×€U„Ø€V…Ù€V…Ú€V†Û€W‡Ü€XˆÝ€TƒÙ€V…Û€]Žä€^æ€`’è€a”ê€b•ì€c–í€c—í€m£ø€m¢÷€l¢ö€k õ€jŸó€hò€g›ð€e™î€c–ë€c•ê€b”é€a”è€a“ç€`’æ€`‘æ€_‘å€_ä€^ã€^â€]Žá€\á€\Œà€[‹ß€[‹Þ€ZŠÝ€ZŠÞ€ZŠÞ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€[‹Þ€ZŠÝ€ZŠÝ€ZŠÝ€ZŠÝ€[‹Þ€[‹ß€\Œà€\á€]Žá€^â€^ã€_ä€_ä€`‘å€`’æ€a“ç€a“ç€b”è€b•é€c–ë€e™î€g›ð€hò€jŸó€k õ€l¢ö€m¢÷€m£ø€Z€>Yz±þ€z°ý€y°ü€y¯û€x®ú€x®ú€w­ù€w­ù€v¬ø€v«ø€u«÷€uª÷€tªö€t©ö€t©ö€s¨ö€s¨õ€s¨õ€s¨õ€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨ô€s¨õ€s¨õ€s¨õ€t©ö€t©ö€uªö€uª÷€u«÷€v«ø€v¬ø€w¬ù€w­ù€w­ú€x®ú€x®û€y¯ü€y°ý€z±ý€>Y>Z€?[€?[@\@\A]‚A]‚EaDaDaD`D`€C_€C_€B_€B^B]ƒ»þ€‚ºý€‚¹ý€‚¹ü€¸ü€¸û€€·û€€·ú€€¶ú€¶ú€¶ú€µú€µú€~µú€~µú€~µú€~´ù€~´ù€~´ø€~´ø€~´ø€~´ø€~´ø€~´ø€~´ø€~´ø€~´ø€~´ù€~´ú€~µú€~µú€µú€µú€¶ú€¶ú€€¶ú€€·ú€€·û€¸û€¸û€¸ü€‚¹ü€‚ºý€ƒºþ€B]B^B_€C_€C_€D`€D`DaDaEaÇû€Çû€ŽÆû€ŽÆû€Åû€Åû€ŒÄû€ŒÄû€‹Ãú€‹Âú€ŠÂù€ŠÁù€‰Áø€‰Àø€‰Àø€ˆ¿÷€ˆ¿÷€ˆ¿÷€‡¾÷€‡¾÷€‡¾÷€‡¾÷€‡¾÷€‡¾ø€‡¾÷€‡¾÷€†½÷€†½ö€†½ö€†½ö€†½ö€†½ö€†½ö€†½ö€†½ö€†½ö€†½ö€†½÷€‡¾÷€‡¾÷€‡¾ø€‡¾÷€‡¾÷€‡¾÷€‡¾÷€‡¿÷€ˆ¿÷€ˆ¿÷€ˆ¿÷€ˆÀø€‰Àø€‰Àø€ŠÁù€ŠÂù€‹Âú€‹Ãú€ŒÄû€ŒÄû€Åû€Åû€ŽÆû€ŽÆû€Çû€Çû€‘ÈÈÈï€Çï€Çï€Çï€Æï€Æï€ÅÅÅÄï€Äï€Äî€Äî€ÃÃÃÃÃÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÂÃÃÃÃÃî€Ãî€Ãî€Äî€ÄÄÅÅï€Åï€Æï€Æï€Çï€Çï€ÇÈÈÈï€Æà€Åà€Åà€Åà€Åà€Åà€Åà€Åà€Äà€Äà€ŽÄà€ŽÄဎÄဎÃá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Âá€ÂဌÂဌÂဌÂဌÂဌÂဌÂဌÂဌÂဌÂဌÂá€Âá€Âá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€Ãá€ÃဎÃဎÃဎÄဎÄဎÄà€Äà€Åà€Åà€Åà€Åà€Åà€Åà€Åà€Æà€ŽÂÓ€ŽÂÓ€ŽÂÓ€ÂÓ€ÂÓ€ÂÓ€ÁÒ€ÁÒ€ÁÓ€ŒÁÓ€ŒÁÓ€ŒÁÔ€ŒÁÔ€ŒÁÔ€ŒÁÕ€ŒÁÕ€ŒÁÕ€‹ÁÕ€‹ÁÕ€‹ÁÕ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÀÔ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÕ€‹ÀÔ€‹ÀÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÔ€‹ÁÕ€‹ÁÕ€‹ÁÕ€ŒÁÕ€ŒÁÕ€ŒÁÕ€ŒÁÔ€ŒÁÔ€ŒÁÔ€ŒÁÓ€ŒÁÓ€ÁÓ€ÁÒ€ÁÒ€ÂÓ€ÂÓ€ÂÓ€ŽÂÓ€ŽÂÓ€ŽÂÓ€‹¿É€‹¿É€‹¿É€‹¿É€Š¿É€Š¿É€Š¾É€Š¾É€Š¾É€Š¾É€‰¾Ê€‰¾Ë€‰¾Ë€‰¿Ë€‰¿Ì€‰¿Ì€‰¿Ì€‰¿Ì€‰¾Ì€‰¾Ì€‰¾Ì€ˆ¾Ì€ˆ¾Ë€ˆ¾Ë€ˆ¾Ë€ˆ¾Ë€ˆ¾Ì€ˆ¾Ì€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Í€ˆ¾Ì€ˆ¾Ì€ˆ¾Ë€ˆ¾Ë€ˆ¾Ë€ˆ¾Ë€ˆ¾Ì€‰¾Ì€‰¾Ì€‰¾Ì€‰¿Ì€‰¿Ì€‰¿Ì€‰¿Ì€‰¿Ì€‰¾Ë€‰¾Ë€‰¾Ê€‰¾Ê€Š¾Ê€Š¾É€Š¾É€Š¿É€Š¿É€‹¿É€‹¿É€‹¿É€‹¿É€ˆ½Ä€ˆ½Ä€ˆ½Ä€ˆ½Å€ˆ½Å€‡½Å€‡¼Å€‡¼Å€‡¼Æ€‡¼Æ€†¼Æ€†½Ç€†½Ç€†½Ç€†½Ç€†½Ç€†½È€†½È€†½È€†½È€†½È€…½È€…½È€…½È€…½È€…½È€…½È€…½É€…½É€…½É€…½É€…½É€…½É€…½É€…½É€…½É€…½É€…½É€…½È€…½È€…½È€…½È€…½È€†½È€†½È€†½È€†½È€†½È€†½Ç€†½Ç€†½Ç€†½Ç€†½Ç€†¼Æ€†¼Æ€‡¼Æ€‡¼Å€‡¼Å€‡½Å€ˆ½Å€ˆ½Å€ˆ½Ä€ˆ½Ä€ˆ½Ä€†»Ã€†»Ã€†»Ã€…¼Ä€…¼Ä€…¼Ä€…»Ä€„¼Å€„¼Æ€„¼Æ€„¼Æ€„¼Ç€ƒ¼Ç€ƒ¼Ç€ƒ¼Ç€ƒ¼Ç€ƒ¼È€ƒ¼È€ƒ¼È€ƒ¼È€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼Ê€‚¼Ê€‚¼Ê€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€‚¼É€ƒ¼È€ƒ¼È€ƒ¼È€ƒ¼È€ƒ¼È€ƒ¼Ç€ƒ¼Ç€ƒ¼Ç€ƒ¼Ç€„¼Æ€„¼Æ€„¼Æ€„¼Å€…»Å€…¼Ä€…¼Ä€…¼Ä€†»Ã€†»Ã€†»Ã€„»Ä€„»Ä€„»Å€ƒ»Å€ƒ»Æ€ƒ»Æ€‚»Ç€‚»Ç€¼È€¼É€¼É€¼É€¼Ê€¼Ê€€¼Ê€€¼Ê€€¼Ë€€¼Ë€€¼Ë€€¼Ì€¼Ì€¼Ì€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Í€¼Ì€¼Ì€€¼Ì€€¼Ë€€¼Ë€€¼Ë€€¼Ë€€¼Ê€€¼Ê€¼Ê€¼É€¼É€¼É€¼È€‚»Ç€‚»Ç€ƒ»Æ€ƒ»Æ€ƒ»Å€„»Å€„»Ä€„»Ä€‚»Ç€‚»Ç€»Ç€»È€»É€€¼É€€¼Ê€~¼Í€~¼Î€¼Í€~¼Í€~¼Í€~¼Î€~¼Î€~¼Ï€}¼Ï€}½Ï€}½Ð€}½Ð€|½Ñ€|½Ñ€|½Ò€|½Ò€z¾Ö€z¾Ö€{½Ó€{½Ó€{½Ó€{½Ó€|½Ó€|½Ó€|½Ó€{½Ó€{½Ó€{½Ó€{½Ó€{½Ó€{½Ó€{½Ó€z¾Ö€z¾Ö€|½Ò€|½Ò€|½Ñ€|½Ñ€}½Ð€}½Ð€}½Ð€}½Ï€}¼Ï€~¼Î€~¼Î€~¼Î€~¼Í€~¼Í€~¼Î€~¼Í€€¼Ê€€¼É€»É€»È€»Ç€‚»Ç€‚»Ç€€¼Ê€¼Ë€¼Ë€¼Ì€}¼Ï€|½Ð€|½Ñ€{½Ò€{½Ó€{½Ô€z½Õ€z¾Ö€y¾Ö€{½Ô€{½Ô€z½Õ€z½Õ€z¾Ö€z¾Ö€w¿Û€w¿Û€w¿Ü€v¿Ü€v¿Ý€v¿Ý€v¿Ý€v¿Ý€v¿Þ€v¿Þ€x¾Ù€x¾Ù€x¾Ù€x¾Ù€x¾Ù€x¾Ú€v¿Þ€v¿Þ€v¿Þ€v¿Þ€v¿Ý€v¿Ý€v¿Ý€w¿Ü€w¿Ü€w¿Û€y¾Ö€z¾Ö€z¾Õ€z½Õ€z½Ô€{½Ô€y¾×€z¾Ö€z½Õ€z½Ô€{½Ó€{½Ò€|½Ñ€|½Ð€}¼Ï€¼Ì€¼Ë€¼Ë€€¼Ê€|½Ñ€|½Ñ€{½Ò€{½Ó€{½Ô€z½Õ€y¾Ö€y¾×€x¾Ø€x¾Ù€w¾Ú€w¿Û€v¿Ü€v¿Ý€u¿Þ€u¿ß€uÀà€tÀà€tÀá€tÀâ€sÀã€sÀã€sÀä€sÁä€sÁä€rÁå€rÁå€rÁå€rÁæ€rÁæ€rÁæ€rÁæ€rÁæ€rÁæ€rÁæ€rÁæ€rÁæ€rÁå€rÁå€rÁå€sÁå€sÁä€sÀä€sÀã€sÀâ€tÀâ€tÀá€tÀà€uÀà€u¿ß€v¿Þ€v¿Ý€w¿Ü€w¿Û€x¾Ú€x¾Ø€y¾×€y¾Ö€z½Õ€{½Ô€{½Ó€{½Ò€|½Ñ€|½Ñ€y¾Ö€y¾Ö€y¾×€x¾Ø€x¾Ù€w¾Ú€w¿Û€v¿Ü€v¿Þ€u¿ß€tÀà€tÀá€sÀã€sÀä€rÁå€rÁæ€qÁç€qÁè€qÁè€pÂé€pÂê€pÂê€oÂë€oÂì€oÂì€oÂì€oÂí€nÂí€nÃí€nÃî€nÃî€nÃî€nÃî€nÃî€nÃî€nÃî€nÃî€nÃí€nÂí€oÂí€oÂì€oÂì€oÂë€pÂê€pÂê€pÂé€qÁè€qÁç€rÁæ€rÁå€sÁä€sÀã€tÀâ€tÀá€u¿ß€v¿Þ€v¿Ý€w¿Û€w¾Ú€x¾Ù€y¾Ø€y¾×€y¾Ö€y¾Ö€v¿Ü€v¿Ü€v¿Ý€v¿Ý€u¿Þ€uÀ߀tÀá€tÀâ€sÀã€rÁå€rÁæ€qÁç€pÂé€pÂê€oÂë€oÂì€nÃí€nÃî€mÃï€mÃð€mÃñ€lÃò€lÄò€lÄó€kÄó€kÄô€kÄô€kÄõ€kÄõ€kÄõ€jÄö€jÄö€jÄö€jÄö€jÄõ€jÄõ€kÄõ€kÄõ€kÄõ€kÄô€kÄô€kÄó€lÄò€lÃò€mÃñ€mÃð€mÃï€nÃî€nÂí€oÂì€pÂë€pÂé€qÁè€qÁç€rÁå€sÀä€tÀâ€tÀá€uÀ߀u¿Þ€v¿Ý€v¿Ý€v¿Ü€v¿Ü€sÀã€sÀã€sÀã€sÀä€rÁå€rÁæ€qÁç€qÁè€pÂé€oÂë€oÂì€nÃí€nÃï€mÃð€lÃñ€lÄò€kÄó€kÄõ€jÄö€jÅö€iÅ÷€iÅø€iÅù€hÅú€hÅú€hÅû€hÆû€gÆü€gÆü€gÆü€gÆý€gÆü€gÆü€gÆý€gÆü€gÆü€gÆü€gÆü€gÆû€hÅû€hÅú€hÅú€iÅù€iÅø€iÅ÷€jÅö€jÄö€kÄô€lÄó€lÄò€mÃñ€mÃï€nÃî€oÂì€oÂë€pÂê€qÁè€qÁç€rÁæ€rÁå€sÀä€sÀã€sÀã€sÀã€pÂê€pÂê€pÂë€oÂë€oÂì€oÂí€nÃî€nÃï€mÃð€mÃñ€lÄò€lÄó€kÄô€jÄö€jÅ÷€iÅø€iÅù€hÅú€hÆû€gÆü€gÆý€fÆý€fÆþ€3c2c€2c€2c€2c€2c€2c2c2c2c2c2c2c€2c€2c€2c€2c€2c€3cfÆþ€gÆý€gÆý€gÆü€hÅû€hÅú€iÅø€iÅø€jÄö€kÄõ€kÄô€lÄò€mÃñ€mÃð€nÃï€nÃî€oÂí€oÂì€oÂë€pÂë€pÂê€pÂê€lÄó€lÄó€lÄó€lÄó€lÄô€kÄô€kÄõ€kÄõ€jÄö€jÅ÷€iÅø€iÅù€hÅú€hÅû€hÆü€gÆü€gÆý€fÆþ€3c2c€2c€2c€2c€2c2c2c1c‚1d‚1d‚1d‚1d‚1d‚1d‚1d‚1d‚1d‚1d‚1c‚2c2c2c2c2c€2c€2c€2c€3cfÆþ€gÆý€gÆü€hÆû€hÅú€iÅù€iÅø€jÅ÷€jÄö€kÄõ€kÄõ€kÄô€lÄô€lÄó€lÄó€lÄó€lÄó€hÅú€hÅú€hÅû€hÅû€hÅû€hÆû€hÆü€gÆü€gÆý€gÆý€gÆý€gÆþ€fÆþ€fÆþ€3c3c3c€2c€2c€2c€2c2c2c2c‚1c‚1c‚1c‚1c‚1c‚1c‚1d‚1d‚1d‚1d‚1d‚1d‚1d‚1c‚1c‚1c‚1c‚2c2c2c2c2c€2c€2c€3c3c3cfÆþ€gÆþ€gÆý€gÆý€gÆý€gÆü€hÆü€hÆû€hÅû€hÅû€hÅû€hÅú€hÅú€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€2c€ \ No newline at end of file diff --git a/Assets/hosek/hosek_radiance_2.hdr b/Assets/hosek/hosek_radiance_2.hdr new file mode 100644 index 0000000000..5c5b2a0ddb --- /dev/null +++ b/Assets/hosek/hosek_radiance_2.hdr @@ -0,0 +1,7 @@ +#?RADIANCE +# Output from cmft. +FORMAT=32-bit_rle_rgbe +EXPOSURE=1 + +-Y 16 +X 32 +?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€?hµ€Clº€Clº€Bl¹€Bk¹€Aj¸€Aj¸€Aj¸€Aj·€Ai·€@i¶€?hµ€>g´€>f³€>g³€?g´€?g´€?g´€?g´€>g³€>f³€>g´€?hµ€@i¶€@i·€Aj·€Aj¸€Aj¸€Aj¸€Bk¹€Bl¹€Clº€Clº€JuÄ€JuÀJtÀItÀIt€HrÁ€Gq¿€Fp¾€Eo½€Dn»€Dm»€Dmº€Dmº€Cl¹€Bk·€Bk¸€Bk¸€Bk·€Cl¸€Cmº€Dmº€Dmº€Dn»€Eo½€Fp¾€Fq¿€HrÀ€It€ItÀItÀJuÀJuÄ€U‚Ï€T΀TÏ€S€Î€SÍ€R~Ë€P|É€O{Ç€NzÆ€MxÄ€MxÄ€LwÀLv€Lv€KuÀ€KuÀ€KuÀ€KuÀ€KvÁ€Lv€LwÀMxÄ€MxÄ€NyÅ€OzÇ€P|É€R~Ë€SÍ€S€Î€TÏ€T΀U‚Ï€j›á€j›á€išà€`Ù€_ŽØ€^ŒÖ€e”Ù€d“Ø€c“×€b‘Ö€Y†Ï€X…΀X…Í€W„Ì€`ŽÒ€`ŽÒ€`ŽÒ€`ŽÒ€W„Ì€X…Í€X…Í€Y†Î€b‘Ö€c’×€d“Ø€d”Ù€^ŒÖ€_ŽØ€`Ù€išà€j›á€j›á€t¨æ€t§æ€s¦å€r¥å€p¢â€p¢á€o¡ß€o Þ€n Ý€mŸÝ€lÜ€kœÛ€k›Ú€kœÙ€kœÙ€kœÙ€kœÙ€kœÙ€k›Ù€k›Ú€kœÛ€lÛ€mžÜ€nŸÝ€n Þ€o¡ß€p¡á€p¢â€r¥å€s¦å€t§æ€t¨æ€|±ç€|°ç€|°ç€{¯ç€y­å€x¬ã€x«â€w«á€vªà€v©à€u©ß€u¨ß€t§Þ€t§Þ€t§Þ€t§Ý€t§Ý€t§Ý€t§Þ€t§Þ€u¨ß€u¨ß€v©à€vªà€wªá€w«â€x¬ã€y­å€{¯æ€{°ç€|°ç€|±ç€‚·å€·å€·å€·å€€µä€´ã€~³â€}³á€}²á€|²à€|±à€|±à€{±à€{°ß€{°ß€{°ß€{°ß€{°ß€{°ß€{±à€|±à€|±à€|²à€|²á€}²á€~³â€~´â€€µã€·å€·å€·å€‚·å€…»á€„»á€„»à€„»à€ƒºà€‚¹à€¹ß€¸ß€€¸ß€€¸ß€€¸ß€€¸ß€€·ß€·Þ€·Þ€¶Þ€¶Þ€·Þ€·Þ€€·ß€€¸ß€€·ß€€·ß€€¸ß€¸ß€¹ß€‚¹à€ƒºà€„»à€„»à€„»à€…»á€…½Ü€…½Û€…½Û€…½Û€„½Û€ƒ¼Ü€‚¼Ü€‚¼Ü€»Ü€»Ü€»Ý€¼Ý€¼Ý€€»Ý€€»Ý€€»Ý€€»Ý€€»Ý€€»Ý€¼Ý€¼Ý€»Ý€»Ý€»Ý€‚¼Ü€‚¼Ü€ƒ¼Ü€„½Û€…½Û€…½Û€…½Û€…½Ü€„¾×€„¾×€„¾×€ƒ¾×€ƒ¾Ø€‚¾Ù€¾Ú€¾Ú€¾Û€€¾Û€€¾Û€€¾Û€¾Ü€¾Ü€¾Ý€¾Ý€¾Ý€¾Ý€¾Ý€¾Ü€€¾Ü€€¾Û€€¾Û€¾Û€¾Ú€¾Ú€‚¾Ù€‚¾Ø€ƒ¾×€„¾×€„¾×€„¾×€¿Ö€¿Ö€¿Ö€~¿×€}¿Ø€}¿Ø€¿Ù€~¿Ú€~¿Û€~¿Û€zÀÝ€zÀÞ€zÀÞ€zÀ߀|¿Þ€|¿Þ€|¿Þ€|¿Þ€zÀ߀zÀÞ€zÀÞ€zÀÝ€}¿Ü€~¿Û€~¿Ú€¿Ù€}¿Ù€}¿Ø€~¿×€¿Ö€¿Ö€¿Ö€z¿Û€z¿Û€z¿Û€y¿Û€y¿Ü€x¿Ý€xÀÞ€xÀ߀wÀà€wÀà€vÀá€vÀá€vÀâ€vÀâ€uÁã€uÁã€uÁã€uÁã€vÁã€vÀâ€vÀâ€vÀá€wÀá€wÀà€xÀ߀xÀÞ€x¿Ý€y¿Ü€y¿Û€z¿Û€z¿Û€z¿Û€vÀà€vÀà€vÀà€vÀà€uÀá€uÀâ€uÀâ€tÀã€tÁä€tÁä€sÁå€sÁå€sÁå€sÁæ€rÁç€sÁç€sÁç€rÁç€sÁæ€sÁæ€sÁå€sÁå€tÁå€tÁä€tÀã€uÀã€uÀâ€uÀá€vÀá€vÀà€vÀà€vÀà€sÁå€sÁå€sÁå€sÁå€sÁæ€sÁæ€sÁæ€sÁæ€rÁæ€rÁç€rÁç€rÁè€qÁè€rÁè€rÁè€rÁè€rÁè€rÁè€qÁè€qÁè€rÁè€rÁç€rÁç€rÁç€sÁæ€sÁæ€sÁæ€sÁæ€sÁå€sÁå€sÁå€sÁå€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€rÁç€ \ No newline at end of file diff --git a/Assets/hosek/hosek_radiance_3.hdr b/Assets/hosek/hosek_radiance_3.hdr new file mode 100644 index 0000000000000000000000000000000000000000..d57d6a3201c8430f78778cef1609e54d9a39071b GIT binary patch literal 486 zcmY$k4{~(zbo6s}Sb`b`3LzrhS(Y# z=_X~C#24kH#uue0rE&BH!moHzsa{bor`wt-+ z+3ISmt12s6%GoL^DywVj*fww4ux{;|mep)))~?&IX|ur7r;i^#xPR~N-Fx@%KYaZ3 zDO6iaOH*S*LrXndLqlUzOAE{H-Me-*?_}Dsqh%-auHCyC@Acn>sAO$wtgo+Yt6{CH zuWxLcu#fHf^&7Wt-)Xtab_eL5>(`;GD$2{tO4~|UOUug3E1LIr9cbRq4z%;Z!$*&s opRhfC^x*zosDZ6TZH28xEeARdwj5x3`n363+wSb`b`3LzrhS(Y# z=_X~C#24kH#uue0rE +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import re +import os +import math +import bpy +import random + +def load(filepath): + global _vertical_angles + global _horizontal_angles + global _candela_values + KEYWORD_REGEX = re.compile(r"\[([A-Za-z0-8_-]+)\](.*)") + + PROFILES = [ + "IESNA:LM-63-1986", + "IESNA:LM-63-1991", + "IESNA91", + "IESNA:LM-63-1995", + "IESNA:LM-63-2002", + "ERCO Leuchten GmbH BY: ERCO/LUM650/8701", + "ERCO Leuchten GmbH" + ] + + with open(filepath, "r") as handle: + lines = handle.readlines() + + lines = [i.strip() for i in lines] + + # Parse version header + first_line = lines.pop(0) + if first_line not in PROFILES: + raise "Unsupported Profile: " + first_line + + # Extracts the keywords + keywords = {} + while lines: + line = lines.pop(0) + if not line.startswith("["): + if line != "TILT=NONE": + continue + lines.insert(0, line) + break + else: + match = KEYWORD_REGEX.match(line) + if match: + key, val = match.group(1, 2) + keywords[key.strip()] = val.strip() + else: + raise "Invalid keyword line: " + line + + # Next line should be TILT=NONE according to the spec + if lines.pop(0) != "TILT=NONE": + raise "Expected TILT=NONE line, but none found!" + + # From now on, lines do not matter anymore, instead everything is space seperated + new_parts = (' '.join(lines)).replace(",", " ").split() + + def read_int(): + return int(new_parts.pop(0)) + + def read_float(): + return float(new_parts.pop(0)) + + # Amount of Lamps + if read_int() != 1: + raise "Only 1 Lamp supported!" + + # Extract various properties + lumen_per_lamp = read_float() + candela_multiplier = read_float() + num_vertical_angles = read_int() + num_horizontal_angles = read_int() + + if num_vertical_angles < 1 or num_horizontal_angles < 1: + raise "Invalid of vertical/horizontal angles!" + + photometric_type = read_int() + unit_type = read_int() + + # Check for a correct unit type, should be 1 for meters and 2 for feet + if unit_type not in [1, 2]: + raise "Invalid unit type" + + width = read_float() + length = read_float() + height = read_float() + ballast_factor = read_float() + future_use = read_float() + input_watts = read_float() + + _vertical_angles = [read_float() for i in range(num_vertical_angles)] + _horizontal_angles = [read_float() for i in range(num_horizontal_angles)] + + _candela_values = [] + candela_scale = 0.0 + + for i in range(num_horizontal_angles): + vertical_data = [read_float() for i in range(num_vertical_angles)] + candela_scale = max(candela_scale, max(vertical_data)) + _candela_values += vertical_data + + # Rescale values, divide by maximum + _candela_values = [i / candela_scale for i in _candela_values] + generate_texture() + +def generate_texture(): + tex = bpy.data.images.new("iestexture", width=128, height=128, float_buffer=True) # R16 + resolution_vertical = 128 + resolution_horizontal = 128 + + for vert in range(resolution_vertical): + for horiz in range(resolution_horizontal): + vert_angle = vert / (resolution_vertical - 1.0) + vert_angle = math.cos(vert_angle * math.pi) * 90.0 + 90.0 + horiz_angle = horiz / (resolution_horizontal - 1.0) * 360.0 + candela = get_candela_value(vert_angle, horiz_angle) + x = vert + y = horiz + i = x + y * resolution_horizontal + tex.pixels[i * 4] = candela + tex.pixels[i * 4 + 1] = candela + tex.pixels[i * 4 + 2] = candela + tex.pixels[i * 4 + 3] = 1.0 + +def get_candela_value(vertical_angle, horizontal_angle): + # Assume a dataset without horizontal angles + return get_vertical_candela_value(0, vertical_angle) + +def get_vertical_candela_value(horizontal_angle_idx, vertical_angle): + if vertical_angle < 0.0: + return 0.0 + + if vertical_angle > _vertical_angles[len(_vertical_angles) - 1]: + return 0.0 + + for vertical_index in range(1, len(_vertical_angles)): + curr_angle = _vertical_angles[vertical_index] + if curr_angle > vertical_angle: + prev_angle = _vertical_angles[vertical_index - 1] + prev_value = get_candela_value_from_index(vertical_index - 1, horizontal_angle_idx) + curr_value = get_candela_value_from_index(vertical_index, horizontal_angle_idx) + lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle) + assert lerp >= 0.0 and lerp <= 1.0 + return curr_value * lerp + prev_value * (1.0 - lerp) + return 0.0 + +def get_candela_value_from_index(vertical_angle_idx, horizontal_angle_idx): + index = vertical_angle_idx + horizontal_angle_idx * len(_vertical_angles) + return _candela_values[index] + +filepath = "/Users/lubos/Desktop/ies/JellyFish.ies" +load(filepath) diff --git a/Assets/noise256.png b/Assets/noise256.png new file mode 100644 index 0000000000000000000000000000000000000000..64cb2e3e6879e358b6c8fcd9a485e36aadef10b1 GIT binary patch literal 67406 zcmaI7b8s(Fw=Ejmwr$(CZ9DmeJGN~*+3}9;WXHB`+kX2y_nv#-AMd=bu3B@h8e`14 z=9*pIU0ofiq#y|og98Hu1OzWFC8h!d1pLng27&_pH`xBsQu{YxyNYYMssb!r-Hn~i zfrQNfCgw!a_QsaxD(1#!o=#)td_X{;WY%h0u3GYPJf;A92IK!=7(DD9|FMC9_yj#1 zjZJOMU5QN0Ev+5+Np3oONrW34o4XnldDz=IxbS%Jll+%1&p-P=(~KlU{{?Zi4;dEm{}N^{+%53%xpaD96T(XMF0CB`4`RE%z{TnOyYmX`seYJ zSh>17@-Q;GySp>EvoZjjEg6})xw-$N!NNlS4?*wZ>ELSYLGR!~`ris-<}Rkr){d^$ z00*M~C>omp++6ud{$={VmSFGre`Fn8{&$)F6^zlt*pZQ$f$2X>`fs4T{Qp1H-v0lf zU0hYn|8KtkpTaI`o{r{>D&{T#H)qp-8)rfKpHPlGqR!^Vt^j8>0Ko3QtEglJa0R$n z0UU`$RXK>L?25+Y2@-2crL1DLwmn>)DvH`na{a)tj#?thxW-tk{#F>`0@ zU*={K&H#I&|5`MU_5Yj;=l`hpzqw}rb1q!}BbV`CGK~M(+W*&9|M%3t{`rsje;W6n z^M6|3+~HrpJO69!6Mvnef7c6;lAM~j0U5OxoZ8Z2Q{%$&@`4-{x&C6yqU_=#^$t#_ zPF}=1tZ&BuLrl`)=A0M zlh0!&HMBK%TyaQB&L*fsh zS{VVdK0fZ81(5Jlhgb(_fC8MevYb1{H$a`ovyvrjILN&?{@DH`#yd`+uRD@LB{n$x zB%C`kSGU$wj%Q!&GC!#q` zpvACGpvBcg0RR9XGO{=z03ZfZF%ESU$UeylZCnm2iq#Q4hKy_y-6@BY&8~Sch^dLoQK^A7#EVnSp^jUE&55JIxzp1txY57W z!{6Jp)3?>vzbVj5($^=lBiy|!upuDSLntV`-P=t_(!)m}C_yOPvm-3nM=eM8TRz4r0fbH{)G&4-}CBwAD_8mg4>BN_|e+~d;BZFsJ?R$TjIVqmjt z@>1J>19u%Gr~}TOgt|jk`2-?<4g%h_%>NhN!Qjg%>tqo7+kM|rI{EHKFkzm`d}yE4 z`sK3Sd|upugQU&@{nf7=XS0qyA!7J1U9SqIrJyIT)oJkTdf5hhi;k$Tc}5gl@jEXB zY$Yex^F$bSOWx!22IHa3_L^7)mOzIKEet%2=OA%ueAelQ^XJgd2HIcoGCi2Md)X5H zF&2L24AMV_keh_X#ERMoyhBfeg|8$rWi1jRr@OGL{^k(MLo#RyD!knM(6gnprX)RM z4=o%R2bF;ZBUi<}gKxMm818og4tuw(^HYZ8^@>*!=K(2cxt(VN>Brd7PwK3v8-X&; zT9>>7d5FDEI{i&woC%h)2mT634~Wb=M45k43cn|2xeIEBs4vHQ8&U6XEDgDwD zIS*co-!E;iAU-AAB8A5obzKaSfO0$G&$X?brm41IdVpLB@I`Jh6iQuMHpbK3iF~!7 z21UWw^1%p5=8#GAlyg)IO<|D>0#DW^%w3xhGbSVvzZD?N)e_z7k2eGEtVSgu;ge6g z!f2m2rIP%aJqodpDUh2oAkHU#ev_?o1Sb!A_C= zq1M(zfqJtUAX4HRk>_ND6+=>>!zmbA7m4a>56LhV3#wo>;Yl_2bY+iKo1 zzF4{3^B}$<(P!6I`D0c?zlJn~e~L)`EW5WqfRn#mQ5tEcexp!6oK|{wnqb4cQ3?pA zw`&qFrjpC5^S*Ze2*N&tCCMV(vkt}kJiJ~>Tn9o{>KFe5BrifsiO?Y1Lii;(S&hOM;LIv7ZQ>NqVuTH(R4Hqb~{qzJp0X z1{Kg%`c5f>)k};-pA}s~zukdoqHSOAxx#;l$2%$bd~7b|NIL z?yg@dLd*RbY!L-)4tvo-F`0^LX#_lQX|~>&xR+rAI*ib^Cuc2K3-#wkO?OSE9Ue14 z<|j2j>&tx3Ul`2P*?-o{<+5gUmx#_7qAVUk`ADKL!z+AQmF%(jjinXjcSO&W$lS6G zT*{w7*Jw$e+<>}Rt(i8qv(D`rC;F!GvqRKN3a^W&ggj?g?omE+qmgPRT0kaxVWosp zrbDb)E|6@9RJijSuf&wS5nSH3@o08e+Vly;NO|0(U#bMIG)57jQs*xxeyKhPs;I2K z?QdUvuqNU-xhz*Qjh7x5(QMD7`amH4NpD0Dwv%@o6o_B!m%$_%EqEW>HItB~{p1~_ zI9wQX`5C;YGlkbvbTbNgZVy?A7kA=C3fqklwh1`vP8tqe?4_wd<c!fBUtK!c1ayVqlVm5h@MwhAciN7Ccue)FAO1G&qf-CT zNL1h)w5<%#5A2y$l>@ru_t;zOGwyqgRDE$Ck7-}0qr+!^BRjoogXkh+J>U3UmJ_xb zvaeUH8z0BFYWI-w*yickWW3I0Drhlh+#}-jDu<5%!wHd`WzHW}MSvYguIXX!CCx!K zEEU^3A(qCu$YUruL)n@~yml9sr1RA4F|Uw}QMH>YJs9qGipa6U=?-9Xg7cQ;+rcjM z@-Jw&q)hOJB6ig?`*BCTk^)DBb|3NuJ$~u4wmsNDGZ9`mk39Q!iuiCFYBWvvqQ~t! zR82)7=~KR?0RH=2(l(-grID*378pIe;UR4|f01Hv`_czW;%2*XEJ{l)9A~p{|I~`T z`^$08c67QZXIxzYV(zne8{K-;BJtA#?cg(d&=TupT37mD#cbcwP4A-zIb+k>Pn(&K z1v?pgQCi$Ktj7P-RB2xFfo>sRtvydhRQ6{_ETdi0NCIhRI9j%8(rzvv6k5moV{#!n>LD zYXJ;y50V1wPJr02B>_8dZuUz3h+f_yfKG*11Ky+G=?)=e9VaEptx?|OAt&1HNnpR5 zhv1)}b=1C6SR*)3^RANw5cLP%EKTs~&jmXEP+PMCC^QvkD;9tJ@xpGPPju^poe~%J zGS8Rp_$K{bNjc5aJ$tsy?U)a@NegJ!3rr=J8gfPy9~5hIL&@8z8RR-7=);{O?pQ;lMH^6#3f= zrrcIlJpcMbNbxAJuK9#XHk0bpPkY*GREe#LwZzgXl49Byt;P^rPh)dMS?{w`|rxCyg1*7{%Lc%0=o>Lk*!h-tT*7o_bSHxvDg20LRFuf*EQ z%vDDAq8k5ZZo;-u=lsBhHD*ZGk6Kin%pR5z3v~;G2;@#r3|?ifE6LuG9Dj539w|`C zfVD13glQv0eoXrrgNNRR%;9zZ*MX=G8bKYnKZO+{Fn-=2L$>~Sa7H*P)YxDJF9ypO z1c=*-eb{0F?O&ucR)mS;Wxow4`AAnO<*>s5?%70EK?RjRuRo}EA8U7B=m|d?p+0Co zl@b1aPw8~IF?Ev-TX(d?F>tTau4Q4)4`wyB)jL%gouj1l7f`(!e*Z6yuI3%eMgf#Gs7GgMi$%>E~ z2P~elxsS?uQZp$c@Og0#n*pf94D3z3XXy)ucuQp>9MJ5#Fl6yf-%$dd6gfY!JhAOz z2(e_eIQ75%Y`+SP3#OXGydz@z>5VDF#Y%UX8aV<9Q=;_S4?sM67ku*63r*w<47a!N z6p9UsrGA=(tHMEvyU_($T0MN(IWEn;CyIc?XG?O835vB}A)g7Y?k;%wu17w3MBQ5bgzzb;1-LSiDM2f zigYigfDl!-`m{cXq8B&R=gs`>>div4z?&ZLYM3xRcI~1>WPQhc8tew7?a6*Wv(S}M zw+`GQBV3)xM|b~FUx_^|dB;1F{$&5z1r;6VZ;}P0MJNqH z4c+RtZZnE6%5+#$NvkdNkElJ#Kyp%yzR{6N9VAvX`2DgiE;g>Hsd2k8Kk#;`JLc6A zQ{tnf-?gDj%w&i7w(=^}#e8}23m(CTz%W$)6BpIr9B*hX2Ts=N&q2PaSKJEWmUJ0R zi;*ojKWiN4xYnd8qE!(Uis6{&$gxoy#i-#)qyVwMtRcHu(FQ^vcxoMGEKvS>@(=-6393DUlsss*s8~gB+Re9rCQ*c zR~N8E=nRYPKv`4APT?ai{VNdl;RV!eWceX_-``^I*_3`^yslU~MBCf1%+ObHwr#)| zIX$!u>MERdXwkubgWv9m9jXOusw8TuHu4xFpwszD4Dswv9Lp+5_K6M|E7F%i)P#FR;aui z&IJp!eDjj2qyTXS<8%Z=MdAwbs)z9fDF1A;NVn3R>v?q*wd7U$qM>_j;>var$q#~< z#@ccEr!K_u*%nQwSUnpEc6`PAUCy-l3?+}dDW&Z%7ecPzzVmdvJnUGHv!iUmN7K}E zt`8<%#P1fHoxQL0&+dvJ;3DA;*?r-A!U$Y+f$IPssoUz66SBhNbtgsm!7eLINne&s z9(Xv}v?9PnKjFHGQs!A5#@6x)JoNGmo*}JH$*_Iebj=DIqfytN5#eAUQSfwgD3#ag z-u|{;;^${p{`%GO&CZe^5GKp)HraRRFRQdc5L9!ZMN!qYkNJ32OSwB&WMp7Ay39(q z#sw~h3CV$E1!sr?1MSxBvBH0gBeqLqFCa@X@F3?n=_hb{g7i0atJ>nw=-bRnBd5$s zo;|-i(XU#0Fyi=dHY2j$(~_y?1+qXoQoh0$Oc1EokwKCCuB#QJqMfv+INQ7gt zCGW#n8_vH6gv$?6A!DsW46V(i#k44pnYWki8ngE&-?6CTGvwYL^OvP6b@N@L)DUAW z<)JIN$IPF>UtlDOKND8rqFx5Xh`^w#CfTZmI^yuem(7@U^WaqN0klLfcIv;F>SZL|$eAdxvq(>h z*Ba<>A;v7K*J}KgppBFh;)Kw9kMy|5(MJG0V8xjEhtf=!yzdZu&Qe^wusu9}>XSyOAm0`!M$Bv4WFG7Wmdmzzd;h?gq!@E`1Y0 z3)j%M6D9KvMrPnFre?K2U<9{TiL37s`z0%k7MXR$ZJEsa_AFz|Xp`XYWTZEk4JEFn0q8SblG58roFIDx{wgl&8W;tKw;8XdTekAs) zoM)OQLxurn)@nFV&!jp^yA1m4UKq(=h~Gu{N`Zn;BY#JSs1H7tUTeI@7${1(v3YQ> znF)y4QGRyGeB%lYt3{*T_AIR&bjC5OhH~2Eig@%RqM6;^GhMj$p=fT}_$DLSqpLEFIIFxst&d72-XJZ7GK?QMY_zr=v?=2nBo&y{R z;FVh|n#gZK6cJ8xz4z?9n%0{%;lnHUk-kmj^%lF>;a%Bs6MnhltI&zKSW#Z9*cMGv zC5$9(OlNOo(g$}O(OHidJ7sLw@+x!dYSnnTV1b{VJ*5^kxN=M-n$@s&ocMCFixNEcUD_nc?Lliudln#hD<#LxNK%zRaVCnXXBewuo>4L@bd*7lBgBZM^d z8iDVLh})At&JSW7e|ks{}%1sKkmPAE#?Hi>HyLBFR8gxBLe8xsaK<#G^4XhTL{i?O- zq`2u`cGS67ZOrH9hFC_nm^cr*&B$1X*TWyU3Xd>%m2^fQDETW^NAX(rQL&Oll0tt) z$`h4{v6Y$F`d^?)6o`3V62_&Cb;2!c&T-XS zZn0DDk2UDcK{#0H&s;hPhF4wbE8c)~Q_+i9Xx{Ju1Q_sxy7FbDQry}iQA@KQx64rs z?~Bv>QtKO*PEl0kb3Q|>2ZFvqQU*td%i7Aa?bt~;Z@PeZfeF?uX}1QXRKqV!t*0eF z>z6IqH@;|4OV_+0kz%7H30d%RPBKNIH#yz2S;KtQ{QYXPWjKk*2IP8Vk64&My*wNQ znhY#fhC zz6O4Dr6)+S6zXnNL>+}Dso<7iyVp_hm@|(Or5W77j!@W|rF6}cn?;3Q;H4w>y=sun z9fTDvmkuHuCdigOW?15iHbL2f5W5&b+##xDv-Y&dMdAYo7$&#HdnO4JQJe?}$2VU| zw(Qx5Lyb^l?_+`vkNaDIZB&O@A<>6oq21(4#x~ylSy(D_YgWfUsfkY6B#Mqk{%sCY42LVsJ{ucLyI_u;q zA`??g!3zJ!P_XuuZZ(3kRmFEgA6erp+yS#AuWyOw`z`T^PIE#~{7dL&ARYeg6BO6( zYbhhN*Kg5u>LIsvQ`#B>g6fexsJK^=fl!6S#K3r-OKF+#Ncs0}01Pci;DK#~hy?3+ zUf!i>*b}97W5>OZfw-rYq&Co2aIhQV&gNP?Bew~K7}ehHLc$XqA~2@Su}nyIEQQAn z79Hf?IQaCu=dL_RMCfFKL-sSU8=_q2%SLVfAv@=q;an-0Y{OI?-Vo{A&1(oAL0uYs zx)K$B#b!z`=9A?iv}c{@pxzXlh5n3`*_!R$F6LYEkT!p%-L{8`^ZGLS{7|Vr!UsBZ%J{Zgftnk2tSsTM7QWy7knVR}eEJiVUjAgEXe(3T3fHGjBgtQc!X7Bh zlp!#>G+_17H*W`1kJ9bYp*;!n9+YIA_x|iegi+~ZVZZMT0g%Fsk|ceOB!h)Rq1sZ~ z*{#F8uEK`rVEEO;-)&R9H~62b7uG)1F*DY*`)iov?jyTiAFeq;HqV=r^VVrK@k9k0 z`DUs>w@ICn$=zf8}B9YvI^06L9 zdbj66CGA`m+vt)xKHk#20HT`aO0W+t=Q+ylJ?xQSSp3}*q?OUmotfRiw6H)NNbqdm zJV_a_o8ooh+Z8!u%0i?@kNeavTzV1Q?xgj}6#Nzy7%g*(8~vrUdpC>q{9S%73YR&a z*6%iN3f_Gd55^=vH<|2N-x7Uyt9uqdaji~uBHuAo^tMhHTn1`1394TMTA=YEj~8rR zydwlXAaOVKDgNdkxdD6a-_<4cX@(Iux7hsP>;Hy~Gq(m?quRs&Ot|bX5K2_c#-_hE zvH!O0_qqDZCu`^4(5wD8h>sr8G{rOkkgSE z-JGTh#QV0b4|c{tKi0=`Oh`YlhKL42X{@nR+otbalkLx{Mkl_}h~4!Q5fb8uspg>Q z0*ckxyJ5E_GZizC-V*5#>rr^uDo9`(&yqf$vqdrZ@Uh-Io~5ox5WxDM6n{~LIA)D?&NH}q+h z;XUFp*eeM8}gJjEx8{eArNqN^XX{J9E~%z-J68t&$nwl*|)F!Owx?EHgNj-AfEbz!~xNodGOE@|_t$v*&#~627QJft8-}FkDer?x{-98S5+7y`FBo6-5L-6ts%6P z-*zYp+SdmbAD62uB<4j8q5tzgOfC{?JVlAblT705g`;NXdp*@7pj-vQvj4y z?|pIhO;(jS>?{i&t5=NCZHx-2^&eOiaHbo#MAgi8jSJ?VX_zG=Q>JgciR4}`sb&}a z5}xeHVN{ixE;;F*kg@FBE<8^{RutDEzkRuiPiQJ^Nz1?RnsV>2#89eDW%hNxn`t{1 zy-Wzk`bAOUwFZuxAweF59bjPhFh;NNJfGsGg6y;RJ+mxZy6dlHl@W;m)6=0Q&1&aY!lS=SwV zIS?+8--YW6Q@NnoZkKtp!h$uX#iDb&lT^7+GsS8%^h8ULu9Odg$?be^rJ+LZ_&S4l zU}Tx0PP#$2Jf+RLIe(jafTY%t}P+e zGt`CnhL*!H@ao3sbNMb7Su}1Em+{)SR^MvuF=3bwG3%XX3*lL&{Cnk9qTF2G`5?ro z5hsYR5;%rtdJ2lry`!HI1#zfu*;)Z9L{EC?b}>CN0o7zt$A1&!f-5Q;?l~+}RDZ z#0n>eyVQPjbkT);;PBO;TMVA6q!%t-p=8}GusqbCI+N@f5F<;Jv5+|eG+bg3v1BwL zj+i;`VO$OgKd%5v0WhNH8}+=4qEx+l?pA1+vBCS(*Y4gOn9oorJL+igeO-U&2Fa-e zs!X)gfh5b(1pmlJ-E$4vZYffGkVh@Ykgi1Nz*VjbT4>m@BOEBi;dVBcI+o-+7$uFM z)yq^WW+ds2LK^C;u5i7@ImL=Vv)trE40A^xVx#-9d)YIrQil#?&JT#e1Q2Vnq24m2 z+`5TW-*A3m6z0dnSG>J&>u5&<5j<^8{_X8>D<4kU*dQ6HkR#Yj1;QTdmO%Zz` zMC$~pW^fZ0u)mWoGnf=!I!!Egsto(xS#S9p^6<4A+h!|&vO$DqcXgixzNQbXZOyEc zNO;7}pJ#*hQRjtGkat`7-6rU;`6y})bS`e$Lsg+W?4B~u z;v-nTHHqjkK{IpL+++>|+cs@_sz=OJj!%aoDwI?0X*Jq40eGdJ}q)%4Uhd+JCs z3fcuZ?Jva9_l15IZm#Skz4W_I(e!}*zktiF`%g*Y2-TXokybC})K~tT!au(8aJ>hk zp3(8ME_={>=x%};iU46u9-x$elw#7En# zVF4^G@s@Jm{guremN?$x&hoSEinY%FM4M6~{j6xbzLGc55qMk+vDn^Mg0SXotY|rT z>hfob-v5TKYpxqMP4i(Z!osM@KWd{3sJc^a?Y#G#{W=_PG4nK(%=R@6<5wPuu?)#^ zHa@q9%n8Nugwq(KA4* zIrE#$Qhh{xM=70i&446!ea`g6a;Y(s!PB4U7*n78;Z`C;umT^({KI>B#wWWz6{QQD4=G7@0(_2C~Qa^>E5k zXO3w#0W!hT$$ZVgWqh;@e17fFl=rns)bxG_*kmtnO%DWXf`a*GO1K={}C(yYbKRnfn#$CG`VDiz`LYjQ+-(UrD8JqS3;M`-d& zj~A^ZN?1!3vl`T4M(A*^hLP{8qu7E~mgr`C5n7qexMleQnR zfd(5eB#C%D%JEI7yfQ;q5JKO>6r;WPGb0gCsifWJXqZHe#K>Mu;+ zY|80fT;8B8w!n-kfS{7DV9!wau~#3pUI!6CvcK-N>jo3tJwujyDY0mEV z{8YnA3$#+_l?{4bz4KX$f$u$ddP_G@qZr7clQ{tU{W%g zVAm*>@(gT)%tbtbj=4>*SIM8B%X(jJ;#c1>duhES<5-ETDb(oV1f4%QuOWGfsdGb~ zQBp24Y&ntvt)<$EiBbR+no( z=P>`iSz|*%K9u)74el8I;1(4U?8N!gF7&CKBt!S$n2%`2YiHOZyy;kZ^b}An`Ddno(cr%R=-!-P| zFQ@v|fv`kE?qp-l&Q();*bsRX{KGp$&9i8D7FpJl$?LI!%m zI8kw&_M^8xp!jfT2))LiTDmhyT8Zh?9@b;R~!=v zvv6KRTmNzV2qK4UV!o`5(397diIDiLlTUZiJIi^u-YL|JX*++E0Bag`12J1}=E)gr zJWpi#$mKSS9>#IR(Sme?ttB%`6}k%p{FulM6+!@7Ft^H)d?6KTZ#P*0!?}FR6~f7b zUbTnLj`-WFFR9nJtwXG}TBI$S7iaifs z#pwC%MU$d^ogA=PN^sxjM$7HS_>d)JfDb|WN@F`#uCn@BohjOldD*vcLNAI&&+FV+ zt&2zRXeNH_iQP+;=Yn=BIxU1P+M$8}LDzDta2Fd*#n$-L6ZT9mz5q9695S64i+bBn zD%k=TWEB{@5)F;#BLv^NH;j=AeGk_KO8~wJkXa`r)76&@C_6*rQ>-PVZpy3<%)3XMx|g zai*P4Wi0tZ#%aEQG0gss9VuG~enycW^W`RUR3;teM|xFnOij{Yx7~A4Qa~%-Q&0>d zV(c9?5jpg+)KYxFqJo&qITGr3_ur@%4>v(-S8cBY>8Z^C?oDYB8Lu)WZi9stnve0s za-%)?b^34I0WFfRdt70FbZ6_hY*eB)gI4yNpM`WY)f>-C*^z1)ZgZc*`>#9;T(c1y zxcDAecOW{uBSYj5g;+Ob(AJ;2oNA%p^mP~45wFE7 zd7GLb($~o!+1xBkzET^4$!r`OnD9&E2zMo@K-P2{jQ65$n!pQtGu$26Y%{{sg{d#y2y3Q=Yh7dyjELm(_s-ksmbsDzXPlmQoMf& z1d1}YRCLwW+Vd{q5GbSHAatNTaXX5;P+F|4>vm(-+6OW4l7U4#H!8!ogF<-S6T)XK zl9P&fvvq52zomzlGzGv7)SMuG<4)C_E%%kY$wEhszFoLHuDBi1+l+Cb>P-ARkp`)K zHFYgNUH$@_z_PdB=g6a$b?oxq62ik_%JQ(Pe9Md*t%B-ys-J%CLDS-Ouh*O8PaNfIa&_9TQ$7GKw*Ig z961MQs;1HQF+47jj%(hwy~r-(Kk0c<_b>r=2MAK0Guf-kq_O1k_mNQ^sNHwclU zp&yAnNUI9g6f3qIDVv@mROU@hw1RCaxb=&g|4PI@n#(g?HMF&W@DU`IQh5PJ{%$ zGZUWYt$oKd>VY)wy<;J6>g)%b%f~0BWC$#dHr69JtMX*%WNOu7^N}buc>GSyDGb67 zjkL7F`^;kI;wJoFL{pv%f~OzE;laGra>z!K#+zC8J7$}ZKCPb>Ea1yrxQ z;1H-8EksTM{wVPmO$fPpuR`dL^w|-Y|GL0*oIfnrul9_&_yE>tv^#&=!2ldEeW+7) zmu>t6-MK*fXIvf6rXfDEgbr)mUOv$pfQ>2<;qA-$s*XX?G^V9e;|MAvxUz3@<=+Sh zN=>*u^cPFa^C!FL_%ukucr?^}uz|hAn9!P4ucek8HDR6pQ#9d|wCvi4Rj{j7-P5i$ z5Le9_jKiC^yZ|XGmDHoJFVY$h!cf{Sb%oHBcEc(&l3XmA%6+)t-#i>g0-4`>1)iUV z1%4G4RlVl?%B8ipn}yg5yA7i*jL5r`>TmY-SDl#O8GY;g?o0`KGqiN07SI}?wvE0h zzv^exO!%u+o}z}#oROewK&rgZJ4nxd`QTJ_+~=C#lo5=YT=(l#pGrjTNF!6!kVi+d zS^mAbwwzkxfYG&Svgzc>&>mRrl+MHo-$seuLU>8mh$W<3if*5C08^H%Z9H9Qsz z{v+I_Y{sfDJWqvBJ&s0l_LAI`%jytm4r!yN3G4T=Z8+|v{C7ztlWaS%5hKWhRA5dI zgx5m(w4=)l@7<)izWI4CWe|I#l5SFg>9SFmNeXj8hIe?p@ngn&fPQBKU8+n(rA=v* zhd6uekK$04n|6#5W^9~VnQt=e;(Bm=uKtcn#yOpuoA(b1qdk9E~njAAE zE5ZA+Xtns3MBoYoK4@Os3HTi-lusueFDI^Fh>8E+C`>712~wCF_itefqtB=uv0Lg( zE4AECcMnVYWqKvdZN2x&Fs_SZ<VsY$?l7lOYmk%E)gQ(2f>#-_p?Zu}V!XwrBjG5mIfUxM{B`8xw{Rq8?kAm_S z*m2_K%mw4NUg}v*z|@M{tvJP7>*(yOlH}g`zQ3Di%8MlwAWy=a7AQ~c8~)jsq#`L!f9g9TZCMtuC!wvX#OPTR6)pvBfJ8Z8zzh@3Sj{jri-?relCQG_>7CnrsppgU&n#vp_;FfQg zd4>fl!5>Usg_8{b0we_M!6?2e8A*$J$3*UpFdcb__9QE=pcZ7js;S3n( zh;-dTB#7d`_Tcd?Qw0?QVb5Y2*zf^=bi*^gqfYImL9r&eS*vt$K2E+WV&9c@7bSSO8QQyp?d0*aciB6yAE`WZoG z;?kw9k4(?(4N{Z##wT|9uD#dHKS>1jxN z{GU>spSlO7puUnlQ8=?2lSav^>1i%oXMoJFCliTHo169x^~9h|-d7mX8gaOzwhi{$ zhVPJ>AN0wm9D1A~E%iaEF_!xtIu-FV2USt_%(iEA1R+nx96bhD6&EJ~sFq_zA3gBL zuAV~&Jy6K=^mM)(vv}k^qO7 z@7ygWRg|af;sWzshab*!Qc6?~yZ3qK7*U=0K4&-%2MpiSHCmO<5b#OtD zFMYMXXAjLescS#v%>zU3UB>Zft3T!U#rT6F!24zWTT_jyP#mIDUhQ`n?nhIDw~VD! zKN~?wN5wt&p?<`e>>cNv&P&I!Dn4NnX3kPJE^y2TffJw;FI<5q*l7b_iM?y1S_FoC zcS_8`9G)s7-Bv%9lim2e`diIdq}f5zG2+t!J6(DmwRIIvoqfH(KN3*N$*_ZG(w-vw zVb-4l{dboKCHcc!xi!wgo({9lA0k6naQ*;~!G2F_MeQCTV zf586A`NfApSMr#(uL;rZMYTAVajeN}ftJ{k#lj0Q*q;d~j) z^u?>c_SM#o2=g<0phy@l&5vf~*t<5kzO#&lk%7__tHoBG*FDLd_@SZ4a+@@YLj!%6X~7ilT;+ zTdA*N9|VIn*MMV}m!ykm$44pb^YkmBf6OVU)>3Kd?W`!bVbQ;S4zGW8*+8X0qHHNBVREocVisd9T~TfM4l+->(#z9z@qQ+_M*c)Q%d#(f1kuG zf9ns49Gt;`@wxX5->|4#c^<}jD8QRNCuN}!iRHJd-q9CO5m_CV^+AmZPd>X@r2kd> zSw!1_yE`4FM;dl4f!EL;QgXTv*c5i2fAzV!l7R*qYUcZ-tn6z1g6X15xx7i%gL66k z28|XKJK#Fj-9ElT9~iy%yVIupwDXAd*vV12ek3e*?g$xR^`N_2+t;vtjw1!Pun)wT zXv+?mEbE|f&50^Qf2XHz%>*`q%wdB{#DqmkmTW|1b>qX?uN6l9_5Ii>S&TD%X{~sb zLWpPOo2G$nr*qiVpJ1wN9n1QwAT~oUiiX6C7PO&+s&wd-a|>m z;{bNl0(!-G0dw`2pODNuO%{ae&w=3O$RF{n+HCU=+%iR>%Ad&E60xN=aB)9+X7Pp{ z^Uk0@;%P@KbZ7i~H6v4@qxRMHn}F&ldS^bO4|5RMHy4eC+uxYWv5R{y4dsUF7I7BA zsxY(KmRLkU?TR8AzB_F9H?$<=X7!vT+XFCSrtCJgVhU;fVU{~Q zoIvLS51r;66~h_f=-(Rx9KRj;_*(=$BVxed7KDzE!J(VCU6&$taxNB8ZC6V?=Hpu~ zgu`UyYbeuK-kGiBRS`lOR3?%Qp7Z*OR^I zG59+g!N^5HAQb$G9Pm3(B7DY4=FKs1y8zHv@+OCzsp62iKzVe*uvN4Xn%$Cz>vdOOpggN zqfn2(u3&uKvsa%9>@$HJ7bPE93KW$cVTY)8eF_lA=+IUmj#b={;R}gNufh{yn?jya zT6s$)BsMKr{SdZUirqtu?2XW3mKAgSKL9{LzrTLLR2iI4uD|6&q3juC&}FEKbL!IV zmHrO0LkXz4qNMJ)=h@FeLVfk;_S$AGyz@%KCmQw>hu>oeL|lK?JeQHSCdJS*zSg`% zSu)GZUwwEUE?L(eI)*{mDYqD&N=vMnjkba*cB?V-`r&>|@!?w157L`MIrF2K&GM4wg=_1T$@rI9ncO(;9*;(OK3% zGjdmkF~hUdcI?~SD!ZGp#UU&?N?-wJvbH(p%iu+z`55O#f_Y&lrMg=+rEOrMR4*Lq zdr3)2&!!5yw5m5{cfZ&8??Hg9k3Z0MsD?(Xcorl7;V&)i99XB2qKTGTI27kn0}2$V zfc{hi^%vA*+zC?wm3G%7;u;>fp;GQ6QvJ3G)0ZxX`$-MMKEY>?_S>9LMP%(iIwXf3 z9rj#6(kRww%c#Eza}9CR_~fZ^uRM8bB2b=Lf9IWO;^31{az3TOy2j&VCc?M733mR*Jw*SO z8c7-z3eqPZQmy-OSs!_esT9Ja2*y6Xy$d45fk*X)x25R%y3+s3Jtzc804?J1P2dLS zP6lQpNLW4oHOn>3kp%D=_U}AHWss;D4wKWySR(pK%CE6$?_2B{s{g5I=)%lb6*efH z@d(cfRAB|to1^7?!<8~=Kb^DIAjXp`gPa|xrZtdNHUS8iDKs5wEwuhedIU#;k?%W? zgdLZ3EvSb(ALjFW-wtT*F1OZ#%k!{9_ACNQ;}>EUi`RwfkMtiASUE`CIK57gl>bVq zIdj`MZ`1fD|0mIl6jH6Gh;=|W)M4R+hLXUkTjB!>Vot}5`zGeal*uzVS84N>Fbrcd zfgCGGu)}m=^mlUhUy0h&%V_D6M>S2FdCwtQF+j^J$v+oV_7+COAY;T7;4i!-IzlaQ zIblfts0)+(4XleDxME;`>8Z+~dB-v8$ot!ySlsED)@T9GvQ^2S)0ecmgWm%p@<=!J zUKWN4V^+bY^|Z~Z)KNI{;At{wS#uSPh-){FG8U1m^Cy6uvKsWI-EB%evTS-fiL&0t z89#NaL8Tub7;p6z|1T_FG2IkLE{uXt^buOa)VM{*+k}co?WPKAzwSd0^Ydj z{XO}I|IaaH)_x4Tr?o^4?Yx}{FBF*cF#T-l_7*;r?9{EwCvkLw7OGI7pw=)N`AQWU zq^6#w7e0%Rem#v0u(c8crr{truToGxxRAPHbxQTwbX{j$qUtgr-33}_>TfunaZ4D> zL{NDy(0zlW2Frp#x3rO_kq*?kTLpi5UP`ARQ;?;~mmUHU1i8veysee@fjMq!yniOu;%sf8AVuV1BS#KoN>zo3j44EaJ;ftL{5!|b_sCT%ihhK zafd-Y5UNBsue+mL7w}&Q^gL$YvdLo&*)3LW`5(h&{RsEtfbsX?be(P|HaX42$(e4` z%*QY$rVi6?PEXF%Fs6IBVVu(w)22;Mo9;N@?|<<8@;tBSr5FTre{OtFpl`CLFndkp z3zBY|WXqnT8i9*$@#Q9bh`-HFb3Q{01rpa^2WsVA5eHI^=T>`>QqE@q)`{loWQn0T zdOc2@imqk>`y9-nEH%TDw-?Qa;4;ews?D)|55{78nX}H>pZ!DtEl080Y0}VaF>Bdy z9ErcxAlr9XW!GQA>)plQ3CzJxYQhXmBwOObZvxvQT+S&#EzlaIZq6t)=YYm2Eq38` zJ&tt|ORzDUD&HLC$>;`j-%v_3M#v#kR%g2~XHx2)6i6~+H_l3^k_MoT#;d;U$T>W^ zrq#{~l>B==kEwXN`A9Gjin4W=n*L1>(p7!;mBZtWMy1-HY{dr3ukbL&@#TDyOLOR# zOW^YIn2VTK>PGCE3`-4f_`dpi2{-_+9!lwpb8gEuh)sg2ZM|}@3XItq0Ts%~O zHrw+!4}400twQ9OiKmo5R_ursDyI$LfD2v=uHa^Mxg=W9%T+k{d|MzP%){w$TV2I= z{aVR{;COZJSIQCZepLBe+xS`kt|`0z!zY4fMbT&o^&URwN9W5h5R!|x@c5Ng^*(16 z`qpy&K1LELkNN%!S9-wMF=1EEQp!wpYp5}#hwTVT(8~PU2e`5R;Qa%l0zIjf+Ym-9 zz1$wh_1B#u_O<(FS-vF+sigsH>mcQWg(#i_)nEfJXPqMh5+Xx(l}e(LLaQ^GDc_ed zp|O)wuiMQ*NdHHLg9@yw;Aqyk5>#R{RkHCUOV5{|OQ3i~1%Y{{vJ3mhMJ=rJX`<&K zWouNRwErT6T=YiLholQ}(kZ>`vLtdzD_9+pqW`8(K2G&>yFbcC>ymBRi1I%Kq=%fg zr4X~zPkixnT`!d@id--c8B=h>m6w`2EgyYgb7w*v;VV`vqAN_jW-Cd8FzDop2IUnW zrsn+NKnkL5`Nv5giw!`0wxb8C>pbu>`@TJ`I;?bX2a?mSHp$sdCl90X62NBQ6a7O> z;Rd!sE~CxhZAn@hsW1A20A%QL2l^+300e$e>e_yjtH_w5Rk9fb598w$A2o(Msa`1V zJg9Sep{g!puQk{fT$6I|71434`JfjC!-~vX=&%tlZXTmvzX0-QpQK@5HGwU^&}<2x zvDFd_$`HOlaN;q0{w?ec-S~+^pX3wfn0}r~$~R&kswy(3Ws?8ks*yW@h!N^%p~|k4 z1WMC;y3xU<=~m#aFC6%jYyTw4eryCY%h=xtL?ZudrGXS}4rSafkLK$=nXJr^|G2fQ zXS6x=lZmlv<9fJ$xBwJ4ur;kl%Eu51O3BUj0&MC+<$}h(ibPyGWL#~0Yb7r~!n--n z|3le>c%`9-oy3HMB`*bw9yM4g5uNcSS7u*QCcb7s{#}F^+_{8qvlMfGhvEIV`<)Lo zxXYOZJo|T;Lz)ORI+QUyAez-N86x}2d0O-G23_*x}@M5vv>KYy>?hC5{XMqT2{Rb2%W?Wo^-p|H7a~S#AlJ|$#IPw53TFr+LDn`z{M;%_AX*xz{$h$NFhl&a(~Fs zZ*Dj}Q!!dNOzQ=D*Q_fU;%9mSx&Y`K)K!NWu$E=Uvn6d`WOh-Ce2rl5v&uCB{Z9W; z|5Po-x4UCLl~Y*C5T`ZNB9{I-XD1(bY9&RfDb=*=-(Byv5G9G+kp%Mt{ ztiBRF=X>U0zMc6Z%*^uou9T>sR2iDpQ|qT1_`QAp^VbVPSNdF1PzvzLy*ZkKVLCy< ze2wq}nq&<;7vT3RDxEI8k?p*U9{?g3zG@Zjst)0asfp9U;tcbZQvb9dAj5ywH<$R| z8`DwvsPu%l;G9yJkrLwB&;vymeCSMciT#CV$4XN)?Bz*qKGfAlAVfiFMcmx&5{NpwgmyEg0yYn^3AFI4P^WvV0PZS$iA%W)!JJ=wx4jf?~GGD|9PGg zY{L27d!}lS3^-PkXu3!?5--AedZ=$yD$Q8R(a#r1>t2K+b8q&tr+<>_x^A>=Mn{Ww zHw;ZTkzV^cPUr?rapd_8>2U~yzkWk%cX@zn)h&XhYzr7LfMEW+d~56m?702CkbA_4 zn--C&xxi$-)y!{lrsH{Gt0%Se5Q$4mTpM_RBgRoIjsF)TXMz{nCiWdb!E%v*NsO1b z!t3kA_4?C{mNs|#5nxLs=-P4i+f|&ui)>uT;==Ik`t4L{{wu!S@91D)Sl{KN*#F@V zrVaQWaOG(sUqbq~3G&tWK-%fyMEG*l(!NARRo;oO&QgGs*&n~sSGc1Imrq*+kFrcIPO z0~f6*%KIV&2+$=Bc{j8=ngrnN}_OqMI+ld-$$87~?HnRgva9M81u}46}*j zchC<-#_7F@>(BdW6VK?ykBy;ixpwbT$mOh_IE*!uuCyt=cxXc>4oZTGe~p7ngy>I0 z3g?FP+|Z^b*@uLJc%Dz+8|^<>mnf=#c%P2cRg~-{oY4%81UEvYoZA#U1Sy9@8>BpT zcj2AV0FRAB9~f3e$6%HbtUrQ-5_$N4JB>)9SBW#3Qm!kBfAv)X`<3DNqh(?8$|0nn z(I7op9WyIaN*WdBVU@(z+{wXf$x57jUdS2H2q)hzl7@U(Y9xk zN>Yo8N0csx{QcA99q||$QIyI7j#3Qh9UlxbcqJ0y5HsznAzo=*BTlYvJgZ`33 zsxVXPhypzS{i>fCVIG*Hmlz9I)+9?<44XQ+u$HNN);XRM#ok-RkLoxnk0y*4=0ovQ z*?}XYrJI86+}HPv{dDstgLv}Ph&YHQ!2$%lZ;)x`_706^@Z&EN79g={b9IP z%t#z~ZddKj4ytJz~b^k9x*C)t@OaB5al;n*=pR5xlVTdeL9!;Mv)Yh4ORj^>3?zR z>Xm=Vk{_7%9URs{Nb#Kv5+o4xsuo9j2pci2pDPINmMI4NrFah!t2k)j<1eb9PUV7!l|?=MnwPip(VP1j*p^PN8*%n^6V zR-@I)pJVxblA5FJp45<<16Gu4!y!hrCCXd_LUu zd+&&6CDVgqD9pxY&~+Q{u>odRPt>HUIUbB4mkH~Z^=>N{J3qz$@%O#}#xto1raXE? ztZ=yWk^qch+a#W9%?FfyT*n*T&!FGvl83saCbO4h!r*MZnzM0bbRA_|Pnn0qM_;Yn zlwh{FPS>60?M+*!10bt~plEHx*>{_wLc1iUM!Axg3F`nZvLSbZ-5x^wzK6cI;E>gS z#3|T#u>QoD2s+_5PCAj%M?lXgzXMK>+78LwG(FX|u0sQ`|C;0N>H|r{_>rtd6#lR5 zCR-QlMUKR7EK%r`TR5m=^zF6wx>2^R*bnNNR@kaOB$jlF!9$?f;2pslb8;}&ZT_eW8qV@bh}{MFeT~}Py7AYbLBDoG z`x1H&NEATPecp+VIJY2^@w&0MY#W%1I3G4~DXQKU#h4HFp#(Xs`!hQ|nI7<4kq~ow zJ9!k((7$hJ&cm+HVor%c%h$F%Jq;3kC_I`R!S|g|KA$u+n&xX8ccRvlW#x6$0P}a< zG0}KZY`qJpV&x4HTbQeroZHxIJ2$myf-S(ANTARX5>tA)Qom&O?#RqQ`f`tGPG^kJ zR)pRMM9JOUHo|odtHcEOA_IIZdb)nM1uQQJl^-6UtvlyO(3g(IbIxl3xPJ-8c~OL> zOb4PaQu2q9V=SA>1}S2daA{^Oz=ND-x4K!!q*vJ;tGEule)f4?M^+~y1+l;LYiVroYsV!o6g_l z31!F)-WLNN?j1itZa5xS9t^->@MAva)kRND;3w^en_}7*`~K_f9q+pY0I|h=t1wl@ zgp6bLsTi2xJWK=4xA3Yx8mOoF@8I^pqGy8c7efFZ$=Sqr%ghVjG{{NwN=4P4{0YAc z@zKt9UeyhC@3V8?dm2Xkzq@LES{_&?vKEa5-K`%ujaC_8vQ&rx=tNZ~;{N&S zS?>N!TJu$%^%+l5mB6G!d?ShL0QP#g`ca?w(mas&5R(pGp=y^{E~_fjkmsiyM{qrn zB6*ZAtTGEK;VJK;gO;uQU2}lNN!fFpFr`|PETU&vkEPGPV$Lw7{%qjuD$xygcG9|C za16NPn&f8ZNyPIzXg6lI>aV~X&S7)`f`6jT*uhdV#4h1h^7)4~J0dkH5&BPFl=ytI z#yKQVvYK^XD7Elf7hYXn3k7jTW0T7kRLB_CmXrm1+exU|(q-I92zY&pO#S}JOWMTE z;{}Ee`eX1{1%*YklBwpo%HD=^Bxiw)2uVoC?rdLx5t$bT`~*N5iP24y5Z zqL4*o=k&Tw{OaWbBO!|sJg)jr{{YL{S-@j88x2e=Tr!8k#aJc{xn3v!%v`g5JhS(a z>v}sYW$Od+k1$f08gRp7Otn|n^cwoTBa&I!(qdWqteLP?^#z5r>fMjq$E7ez{a7Te z5GM*RCB-|VTjWJzcEH81!Iwms$qI*wR(t#SviC896?J!7L#{uvjL zMMOlfC~ZNmEo<23ha)}76D-cD7YLXgVFhwa-{5GZPv~BcMQ5faMfk(L!q=gR?cQ=F z!!s}%w${g^=~kJb7^oKpo^DBp?^lIOGmnhkJ{OLC4+rT@I~pip zUs-h9HqQ{=ct9?gjo-klDZ@~nclfvclD1kp1(_5%!y*RmWVL& zxMf=wLeu~Lms$xdIJLUO>j3FEl;he9c|uVD*=g&OBuYzzs&dE!$26ckaK%WGL)ZHN zR(syDH8&Wcse82=I6tUxJti*-Qqv`_9vl^4hr7Fxz&pxSE3Zn< zKDD`SyKf@}eRD5up?To3K`IfAGkbxK?{GW8eg_RoFSlp3mwK@E1=GcM8Hdl9d=~%d zl523C@UG@PbpT;$7O`6he|rf05BOCsk8isT2L;gjrd#1p|L%ZXhxM@@F393}m6eqw zSGy|?Qh|W52W7X9)CTX^cf6LfkxH45YpB1cSh3N`zJqX-GWLDmdL^#@{)TP47X4V? zT)341dsB(`xSEZ|0I1E^V*z}-u255ClV(xH8UG-auRnsaViPx*4cpYE;&BCL68LmK zqKaM0Qx=c8HG0@w>-h*jx3kPje-iw9w4`6Nt_fkM|4aYkTtm|W8;>Ft(t<%Mo~X0; zIkAR$&Rm2@w4%6ekKxTmR3{{7ncuG)XgG^Z)fFRtWbtSfXD@7M398_C^~Tdow!Uoh z-w-6|A)}R?Gv`;8At(P7<;-AOhFhya@&btjnicJ%7P>CREa$JF6Zqbcb)zSG22RRT zPnhpNdrCQr;gf>m634BRD7W#v_+^qdYz==E{|DR7z|?I;Q^dadyC0+>_;L`!(@hAa zC;!{jL&+E8=?d)M)6l4LVEOysH>Q=uKeGJLIM0_@XU?S$;tJ=(nlCApe#HCk`$JBRWJGuAmi0KNCIT^LAKIEMXdt8Hvl$@8h?eXUde*}%_Hup_ z(LFLU{=Hv7%!ATd<_bxV&7VR8rWoNWNI>cqz2*+vPm}Z(k-;-I?J9uvg&ea-$-sB6 z?DrLZ)899zUJcIp7x(|rfCO0XrR5IssiB{1xE}kFL1&FWzfh(ic^E!+p8xK@PDK#_ zOL^ut(V5z2<7e|a5%+%6{h7_Bnm3!2(fcfojc=MGN%dC~bq*7+R{sM=7T>$pxcmGE z1@Gd08&rHb_gfO+SSz6R191@M3b<0A_YN91t4WrpM19@5Yi#tr{dDg-Id;RB>p|dM zxC6(Spp_n@fiCM}Pc*j#yEnM5@QtEZ@->khj!+>(h}kQwNIle~QsmQDiir_m76dCb z(qz?52U-xm=$HXJ{c+A?QnXYx9`D@N=+!8b{wL|OZcNQZg|{&Mg=3Q8n+?24+HIIP z>^|*S9^{Lrx)J0@1h1z|1Bcu0L-r58<-D2mOzC4Sf6px2iVWME@$XRI*wcR*8k|L` z=1QI~vcBd|=8t+2^K%pQepML(R*M8)_#kH2K{}1+hW|DNngbWs1>Vq-%C@;~u-W$1 zKA&tb)Exw(-)_80Ax0jpzB7X8pFvB7TBe0K1jeDRbhISkD4(AxvXa3af?`Cr=U}f! z_k7fAnhDz!0JR(R36GpoR5YDjj-!#|G!lb%aYy+RCif6w#7KomOd`&;NUBuOAf<#5gQB^SUMA* zF~QaNH+yoPE5hD15-Zb`!d`$=Tz1iHIom67c1omEoG-$JCa@$`h8-(H+07ER#&cYa zRzPJ09M};?UhR$ildWVQ-Ppv%_TSn6T}QU2$zMZ8xfHb<&+6l84)9&%HvV>ilvIXe zsw{dZ`5wi~qgQ{_(F0uv^}9QGPaN+%9rDw4wcu29pcxeY-;uQYCJ~eIqAcSs|6g%z z7uXa_cL&8PX@;JbzWUyyHhRO(s*77dZTx8@#8?^Cz+a0je6R8S08D0)*(wPLTHzb% zr;CpkyGDdJN^h9;Z%F;gx+kUh7pxbdrNl^+*Xg<8pIx0ETx}lwRzdVqb~vA3AX8K_ z3q3o&2tL=e@)aGc(D4V_C}6h>e5o%~%Uiaxtf>-c3PPXYg7INFhi(_@#C&h8!;>ia zvoz@srogM*R&9R@Wli-;!npM>ac(y+HjJNfPm6-U5urCgOLigb=^q&neOAoO+W_(x zW!fx4F0|POyQ^iTny+RZb-hZ>sL!ZoV(9DKqAvKMIm4pP7nt zcncjbf9WY`_2TorMilT(`MvFa@cjHO|1RW6v)|Q?`j225!3F5rryGM?C#237sW@i| za-J7jd1SgB6L!iWB{ik2w7onl7)iwB~tNK^#su6!M*!bGsnLI8umRDRkI z$T}d6zy?Z`^U}%NP}YRep9DO&8|tv!s1qUM2?AgVKlAa$Wn{#w4`l_M-3D!ikGIgV z*r(jRvwLkBRTx-zU{A3*pg(jCcQM@{B%=Wo!^FcmnAv*{-je}W_HSuGc1N!$gfQV7 z+ePuO#lPmve9QCK)aY?xi+qVwmLh%D(Jtv&@TY&GfWGKol~|?kImF17SD=2Wpa9(e zk@eBb32hwr^ERLAYlOw~3vAyX8j%Ou5!PpL9nZHnItIaL;XK>p=n9%tFQT72tAf(^ zXvfsnf%EXTMj`~Y^j`P(j{b}Q&Pp{i@NvhthB?r2$upbZYuuj=K=L13(kUhEZv!hx z19k)BJ=07umOMdUYriQ)p!OWUAf*JJ|CSF_bz$rTLBG*;6@jS7BgI0T857;e5voGULHh zYUbujdCK85^?DQNMI05GwZ4%WXW}pkNX;@`MS|Qj-!92WoNj!^QZ2rqk>8Y42vLIMk-cbnzEy-6+bf?s9->Kl; zr(UDUs%Tv=R`_@{_>e}WX@!C3V8#p#co3W{b+g7MUMonZY5 zs{+nJB}%ccccXZS__r5!A`ij?g0SI|ThH7N6Z65k*bY`)+wP8HoN5Kk#7R)=T9-)> zdHK1YpbOrH>3Bs4vt;iq%sAy@e&{)k4!sFAm*uyw0ZO28>X>mxn_f@Bc^T;Q=hH+; zPzI)hURMdek%Fy+Ev?|gT& z*;ITZ-=ar##4SP0N-Bw0sl5W<7qKuYBr|Lm0|ai#J|4?{ZilwZZ66#ZM<%ZZO@~P) zAB6Rqvroam;w4nv=LleOt5mfn8q*m<0-iT4r|m)+zFG=($6}&Wm0&7xgLFM< z5ay+w&~mJz#;kNNhm5m@Y74o99c_LMwRc*-`IpD$o)Ya`J8BmIU<v=)@c`K9b2x<6#2;hN z|2=#_E6%AlQ)jmM2#RQ*lZATpw!MFBZE9^UL_0Z3$erFK3sZC-0d;92(|cBaMEzufqpQ=uG`z%n%Cv|~=gwfDML(gI9!QlR`B9(cGWbOUV3KNO z*{;`2@Euw5iR8}2%KO{dLK=^nN+xYAEmL8K1UQFK`HnOW&_z3L)ay8WjBdpQ#?WXc z!;Gz5gPq_j`g+W(BY9s8g2554y3XJBOTZQE(R=PKyXToV} z_Z!@P{7Ek<(;Ok-vNvjTfc7fjZdZHeFA^Yvd9Q^i{*nUw8t5JbJ3rdjN_JahT3imH zc?|9UjUbM&tK*(yvvw)6H9}q~QZH8#=9Kc=Z%;7W^c{X*-Rk>hH=zMG^}!F?c(l5^ z2lZkf+t_xH-e{a@90S;tpp72rfR#xRLDt6}R_nh1)Evzp>joVWGyCzL5Xl}eIMgaq z7G#`E%y0ID9SVc==@!AHeKV#M)xvb#C-K^0)2e+Rc?_U-NXZ@|BZY1L8Bfz1`tN`z zD{fbWk2}5H{Wf}W3k>1x1Imh+~jG6{0$*Qu>Bbm0Of=OGA`45s+01qRkF;+~hJd-ix*iP>>W4M~K) z3PXI_5t1q}0Kj|7uARECtqE1XMe+*2grp*}1>$8zNd;pD@sUN#KO-flhUNHvhie${ zf>Y#w?!4c*M!N+wW_g*{RpneR+a>Ttk6q4~!)juv2yZz)P3Ng(!p}DYPBLN5aPL z5r1x5W3)V)nk}2Qjb`_~K^7TWk9RXDo=90NE02Ge(Vo7~ z$^JytvWdADzH-L>ERHWd{H^}A*C0x5$5v+hAMXkF!BPNZ8NhBHf?Gpx+X^A=%P$%w zY67tS0H0|o0JV!e=5Rg6Vy-?B8-lF@$2E`Q@cv{YQ)mzxw>p!vwgJ4073t%~O`x6Cn`opwnxR}&XJf)i!Z_L-*`k*Z{Dr|c z=wME5HB%w28y}+B$O#07Lz~XUIMlZa)S3eVt(fz5f1ZkUIqg1@*4A0(WNb1d`@A&; zAkPW7Dz(J9_GSW2wxZeH+g9-^XB1)2D(gi~_Y=`(ci~ z!8Ty0^&VaNdKPZ8?t82r^jR9=jo?o(8xmcE9O=rU>Dz8`ih0cUwdV)=W*)$`0)AT( z^kb+|l3Yc3joZWA>8Fa^=FV1XZDmWM@W-y1zO^q`Pk_HzjXQEY@du2hf+ zZ$irKLSI0o#_;5M%OQFYzdBV?6$FUg3|TD9fb20v3spMV<8k{Y-kSpP*e{5aOv`pF zKA{vT%}D8e0+tO^7Holhg8MZ|pLY9Id`wl{HwOeCvPgxIBR;9dCd^Yz zk1=gFP(FEHAKz*b`sw1>(6#87pH?D9WTU0oc!8BI zUpt@vXlk&qQ-Yh`6Do#~-Up`nxZr)73?gGx>w9+OES2A`k-Jq3h3_@o0G{Ce?>OVc z0M+^Ug&jVG_MST;yJ`%5X~E3CMJ9o4>tlP-xpYX@K$?Rvb*GNT$-2d9xs-p5eBQZ7 zz&!fJ*qUWT4a34gQ|nH{4I= z`}Z5+L{+YRHrR;54`AW`CjC8;OEcz{taEGzBtI9EqDIx?sa^b(B!LnVskJ-)-U@5& zmf4B!)bmC8t6TlfNaUaW29bSf4=R`klvoMS6mf^zZw`Qg?@NTPyidFza1H8bJi}rc4fG28*&!ctT{st^JDY{sK@-&V&1wX?U_jIAmW~xo)v#lR} zN#H3+t>w=RhFXrcs)d4_uHj@nptnxKc?t*sTnjDkDg)*yB%JjQTf9O+gh;Gel1pw z_k~a~4#dt;Q)vA|!D7oTz`WnQhaJIj`0u5f^0rCnarF&7LQhFNiYE_OSfUes_5I`e zZPHD}4?nAO{7bGBOJCCB&n$UFtp4rnPnNJ2MW$G9PMbN*i@hE z0F$=bGqh>!G?$*+qS3i@#{1qwaVr%WA1tsqb^N!9#CE=ACCl4mbOJTggv?1o@1%6y z2^p2prZAkH?2qmD3m}XKmi7yyl8^YQ-WPt==SadB(c-^#Qh-pu*ldlFjomVHIi-BF zVCzqYhcz8cOnj@?@A@>Wojbj9g8Z^6C{XS@xyeH`qt4SxvXJ+h&>;ma0Xk?*6dC;Y1&pIBReUkdVtJ2yJxNhar*>8n!pb5xN-vsKVD0F$W z33!A;EEA3zqw<9lrf4PT)cKc+X~AlLDG98J11UxIRzEs~W1N=W?!PgS6$$xWS4PUR zM^7sgV*ASv&20UVBvkcN_IzY6N^sM(zJ^#I6?2JmDEb6&8K8mKq?`CDO$&%SK%@ZKb%xq{Aj00;<=u@@%7**+`=# z*5e_p;g8++$(R$qyGXjIan(at6pXfhMsGUn6!}DX0`W+0KM|p?vav$D`Dkxzx(o-> zzgkXw8$l&zisxo#LZX-QY4GZ$r6=SC$9mDY`JK3F1k1j=(_Y1|gm$*f^OYH`f$tEv zQ}-T88QB>YG$P*^GEdfM8JMiT#4?5YHC4X`yqaV_B8GV9oYI12;9t@bYaqYqd#qs; zIF&=$;)*1UB?E8?h9k`uU>>GP&ccsHd$$n1J%A_Bd_RJ6&{U=~0q?$hM{oAF z_PwufM>wCo?k3S{S0Hs=0|Y?OghUK=_$lCdyDazLRja^+(0>|t{IPuiD+P$Yl_hx& zxvxvYu}O!Eskve5Vgm?PU~C6c-|ItljG1)5bZs5^4ew8>_0QEU@}b{Wx8Ouh{i@ir z7my4>!X92~&?*srgmRv9V=-IyaR z)-;p!ec>+@QSHBfYs!xs6ra%y8ZG__Y%QYuB4_f=KA23G_xI!!&pI7+ITnR??L zNt*XZ{VBTTc0TE#Xpms?ThtTARVO`>G8DMPGEDHHW?OzX9!k@9IeNZlB3SinZ69ZU zJT7sLIuG;CE?K}P37A19_54>694&&n!#2%&gI)`ol(4g-6 zV!+laMPT)#8%nfG>Jcv1uga(TuCeUsrgo*(eY6tV3@_D`4k1k6rd2ykF=hSgh~fKN ziduC%oby!ui2z{atf9}Xh;B#zi|l^!)GOxBP-HSW6k6of675TudR+b=~DU zUA*n5MsbTxt{cO9f+EqqnWYKQ%^G1<6MFw#{TcroN=j_wj}9iPx80&A6v^;{gilpO zjrWtUG68V7%5Gxh7u0@`4d1)VB|x{mIEZP^LTKEwV(OEq)eT`h+NuokDI31 zKhEn4qMRIX_ye2AHs{!@5!B4V;DQzgZDx35aku6I)%QrhqIhKegTgi1HCB2E^#41? z0N-3=zRMV6Z^`>6N(-WXwP5NM03Vq(nY-tM&ARU8{b<4|8LF(hs%@(oY?u)A|93Ij z#M#l;l*+@#c7eee*h1`DO7%)k0e0?!57X+7U4U**Y3XDs4iK6O zNE454VnLg~eo)x84uQ>seMwgNdzE1@W32l`>&Z`Bw#3bV6`3pVL-UW)!(iY<^zV;* zmTa4_!};FY>IH+NUFy{zQP6Ru66x?R!sgAQtKK$xe6~96c%Y|<@(zcuY#n}hH+A(A z^_8kmkiYlGOu3Z>{o`+87FS2{qm^?yp!d1I*FiT2k@!uNzm8llooRqF+933m9hCN1 z73~3L4Rz_t;CTK`1J9W@Qt~ZP8P7e?(g1M9qtTqQ^Wvkw*d~hunms;QcfWP%-X`V6 zIM4>Ua4|QF*Slgur1uC?WgCW>$pkrs*1eVUg(C0ka$PCZi+*zhr6%~pFB851q{Q|M z#iT@T>12^xTAtVCWgCw23Ur7rhI0A|LH7zg;)%tGg>(eVAdk0qfUGjP&v%fnx}wNin}SZYKfTIlNBLWJeP|q)6 zj~Xb7pGqNXPsV?vlP_34_1mbh1t$?EhTDonJ@suX@p~@ns*mCiUkrHtr#@JXg2aW@ zE-i7S>%jVmtU_{dAuusyae#>6rzmYze?5QqIHqN&viyg;z6Gwh#|(QYaNOrofr(U& z^xj0f5_Rrii1k)fcRTYcIGd%n*-cHQ4FS-U@8nrb#uC$QqXbj9rv5hu?*@?QeWEJU zmPy=x!Hr1ZQDr3d@5@hF3^c`Vgq=9ALLr-x|BRfaC2RMduR1G^Cazm+q5B?w!5jW#MnTw<|Y4zFhHyD5t?La1HMqyOs{`t(Zu@vb8ns%^JO9F83gkth5(wz8Mh zO;S`9M?UGwd1R9iJsMO~NzNQtq=5Q6ugXGP;A%H*XVV%{-wv_?2;$I8^9Xm@us)pH zzan#aNvqt7Pm-m0_K}6{a36*Tedb;!ZKi@SW`WH1RpUwiL=PPS!9z3~pW!AoiLcNd zok|qT&K|Y-dA9vbG@CgB@5`Cbc+vJsC)EBNDP|V9$gc?T1$GW1HM(AC9yalu3_L!W z8h~vphrL=rQ{&2y*E=43MUZ`ROF(VB|3YAXI!$c23Aja6&j_BoB5C6 z(}(yBn3J9HJsP_Suaig3b^EnHv{DumUgtxeR+0ZS@}K4hT)^GALe{2?`)yodI$p%S zA`VL_1zz=AgP4$wI9AT#X%O7&N(5uO@$xFpXRcZ8ZP~l3aNx@riy7i-5IDAgE zG`JBG`apG+c0|0Jh(S0Z2j`fEnRXQtpN`she3Mr6~Bu-g-pFgXgQQ*|m1{gCuS(TZnR%&);*2 zfp0=B&Rz@=ySqUq_Ke;X9s>;Gwh_w`7FM8O8PnC_u@YWpG>v#nBYHlE?M-#4z=UkL zTUrA1xtegE8AYYcabLtBu1?l^y4%%4W!1=84s$Zu>{pY&h_ryJXDbAZsT@L(odL9G zF`zER!4J0Jm}Hi$sITXs6XhJ2B(GAmttbIFO&Tr8THqf8)WeAeBcBF$!wUXg)gKJ2 z21Hsj(J!{Y0H3)=-?k|}83Bc^TyV0tX5Pl34A1P`q^^g@-l^!6*i^%22^+H(-UhO{ zw9}VH0M|URmdG!f5`sd)Gz>Isx!kS|m%vWR;#1T^;eWl)anL_^y{`6`32?J6dI{MA z(`&mLvLIu5rKCtVddN>xIn@+AHPx>z>wmtc?~I79nJ$bb0i|JIy41Hm>h z!wq0Dc3KJe_{Kvd10wZN&9St=Hb>nr)YLl1JDD(a`DuU@zBUX?FGzAc{%baDkyL_Wadb|JqRV zrHwxw#c}^o{t}+SoD;|l2yJUGBBT(!Ik}ZQ5ZR4GoWIcavr>>4zz{h6)l?M)JRpss z9QoMnD9+pz?|VIv4i2b@G&jV2!;q`BbDS(u_G`CH*K?uk6Y)Q!E#ujGv&M4>Z4GDo zb2p)j5a91UX^+y=Nx&04wn~a<^PWI_=vEh+;dTJ}cl%psXu8l;95k7EQX}!Yo>bJ$ zeV#+;nu;#Z=u3nZ!sMdMzQCNVC7h&pT)Z`!S6&7+|)y`?J#=*1qB#+v_e zNbmSOXMm?%ZLFsl*DWw*?DsE`rMZk+1~2x;rzU&y*U4u*b9~=wZ4Rq<4z@x{xZg}Y zh%TO}x_$kk*ve_pQy^W$yx$!GgenEsfKK4olaYH5N` z`@l>$*7YkGxhDgEIVqH<+0qZvuN+j->e<>1W8O$eKCo<37%x?<9QDTFt?$GS9pVH( zcbVqlH=&T)nbHRk94}!>e`V)(@J`0E@T3_KITwtU-I!6v>OIYsn!-9Zadz+#`j4U- z7B15AFJe&8YluebF5&ZD``j|uFQxgSY<4rU3?(%K4Y|HA2v}3Mx2Op+wsD~ z%GaSaii$q^7>fv1Ib&|AC^ITIn%JIDJR!yTYm&HW_4^l+IkUA2I1-}&4S@hnK(fF4 zQZ-zV7v{0dYk-h~G6(baHqG_kT~W)=y7z(nEM5PZoaG<*0rt0NRmHb~+sgDE%-!ej zUabo{d;&SfZ@!QszIbhnAuDq@{Nkewi=cnnNUC z#imFMO>fzPw4rYp3BgOUbq|)o@75<`l7St!?uHKhk>~jF-%-|2^KK#$RwsYY7Dr63 zBXM3zyK_mBxd~mn&bqpQ(xY|_<=NV1NM=R~oz5O(BH=~{1pjOTpeaSq4NqTJ!ZNvP z9ix^elB57}LFf(oM5#oq((3a8$3NI3x&1c$a$yRnQ``Pl(OKjAvsojo_TrIR)_&g> znHd$htId)KHDso-UvoX;o!^53%=Hf#9@_j;NyF;DkdVHu->>+!pvIz?nI>@2B7qc= zs*yHficzm8F$3n`&C>y6Ue-WufC(P>YCa#ug{|49r8|ff>L}nlwLBLqwmGm|yf*$~ zh=IJtVm_;(Q3#G0dFimBBFEbx@Jm;M<@OdDHdP4#m%%gsYN9jwx#lZMH_%u}^3nYoUe9qap$F2{tbNy4w&+fPgs-2)aS*8f`lUUgf9;wbh8^*VzPW{C^zrk%IVFYH& z0}RHWmN(wSpeI=2H=B8CKOr+%m#Yo`nE|F#}53~&%eg<*-$m^^r z?%tmuHFpF~J$(^s+ykP0BWNe1KAd6DWMdZ2TX9JJ55F;habzI}2LJK0`-+1w7w_*i z!_$?;yK2SaGhP;6l%nv_GbC6qS|80jj*yHPM48&scJR~HZPQbUohi?Acedg}?>t40 zZS0{nKHyE0tvFI%ShBI6RO6T=<#=)!+Hu$5^&4%b;Kw$@nqkUq>atl)3ug3y?4fui z;ciYB0&d4Fk6l9IvO7jJJ=B_6U9{bTzqZxKN!Qgr_kf;hGe<-c!E<}YW1NnH{&NUE zo@3JIe4zXFM1JP@Y7D7m!3)WB=Tdrkn|2R2Qw*v5s%-Cyf#t*pP46W%RKC<_u3B9r zrUrSEMx)Ia$K2L2778fJZOVz%bM%;x+1viLpmF@?4wh*%&T2x?u>hO>CU|$NEzTHn zgG7)H+{UHqwvQVq$9fV8Ck27%gVIS)_9|MwGgHU178eGWeGP0K9&3tqEs_D@Z5^KO zPBZ-S`d#1}+Op_y&F4Jms>)s>m=v@!HHm28cw8%7PBHq#`q@RCN6;EDdbD&IAZ6BL z>%6UhNh#{b)jl6=_2ubqH)^(Vt@}-u?nwQj8b0X;5MHkufdV3HpZ1Hg^TF|NistdGS0SNEVYK8x6y) z_tHe!&au$WvmOIaplrhhTlp;Qs7vO~;}S)?SczPCpdlBB?pq5z=l zWalX7d%PS`_1LawzCn*+M<^S==#c0=yXy5W;AoBegx7nm^z*>a4H<^le~(FEKR2{~ z0N1|vcV+po!7F|D;U56tEk|)SokFtwKjp-DQNAL+fBX%XfMS?ZpKhoFHL{Rmk1|TJdWOVnRKs=MHIM%5xo$9o|6N&!p>= z*+CDn+9qI?1>GB$)=Q}PvAIc7bU;rk?dsEy;6}GsnAAkiF;{#YYpuUudO#0P z?t6SxZH#QHgHX%oh}Tw2J-2-G#r!!s5A57;5EzU{WaL@iAi1ef`VGh^{qC9v@ zVc?v{nYQ<~7(+OiZue8aZ^2FWuO6P;yKj$t)VDb*_1Nmcd32Xv@MG$u|0{*#_HiM& zxC8yja$(QxI)k zWYm7Cp9H@hl>9G{FBuTQ+J53reJ{i@ehOzu4%>F#gw=B93y}uaL+%aCX&X3Ih4<)V z|Cxa_sgm-tH476Qo=tq@y)Q?A*&ZF-wg{Mg*jX z^NALT#U)X?#DU#CJ&s7TjYx0MuTrO}{5FLaO8>Cq8@+U~9U+vPldjpAtcBN}q#^^c(| zlGfqZ#DL%n<8sH$v>NIC|6Bsq%Vw&ssk^l^{K~x&4`w#%%I37>MVCuV3aKVr?!KI7 zKMBmgYsbCGRX!n}J`vTy{fuQ>XPY2o9k4D83_d}o^!{{R;;>{~c*LmRTOmc}RVaVr5%3COiuFGBkym z;%!#{7{=}N4loKaTFoB=jtAuFS#G+Jme`)#Z3d{qedaYCw3218?v7-0u59UlQu)I5pzm?``&4yB;CIgw>D4aWQ%tSgnuhT<|_&!`l0OQ7;d7vRI06WH7z6E^7rC#Nl4Zu@yiCPYiw@||1 z*&V_#)$?9+C(*Cm*`?Hm^lB~WxkbZm2kgJgCHpLr2bVJy#TtOr z<^qm=08jZ3ig~M#+`1OV;6Ngk@ko|E#zg-=X^d!T`5ba7UtJ><4c2Ltor(m-EM< zGe%7>&~BBzf<*Sn;jeP(VVt=28M2fSdfx{NDBol1R|X|_3{&+>eq}$QJ*A~3h}%hc zdNhgfzN&zR@3!@QLV*Zh4o;pkfY$4~QRevHf71GY;pZvMt|nK^*Zo}WuA}cQIP}Ig zw5-1S+>s`UT~a2k0%c=e&n|pU^PGmGwaFTIzD<4CXK|a} zQZ?$GKf~QDnVKqCBFi(lI$10WKVbeQed-yd%X5(iQDa(s7{#>U?Bl8el!NB;>(-)W z2rX5%H#C6!tu@aRzSR0JaIA^RLpOzPW;r>gv!tc!=p{Q+p5te zL2eA%veO3XM8C&31GqxG+@s^_^AuXrNnqJy{_G^~f(K|)fV zM;rZkTOty_rKNF-d5XYPqWbWci0$jbsZ-{uVbI3IAmW5 zOI5v&AtV3&iu0O?&$C&+2r25Z-N>dVk{N@$u8QFZdkUHWF!->)FFG#^)Hzo@U>;x9``4;#Y*X6eC71`2q zg=OH{TKP^&28L|B^7~$Df{_;~6&ayudT@!Liy2JY`fFjno+2~-;_N|1V*zO}2aTNY z`M14mLHJ>m+t3^oeV&3k9w~=snN$hhT3A7*C6IfGt7G7pAaRw=7oUwwI=+Fgl^6%m zNM-Lo1o;q75+Z04FY|3SP(sozejBkhLcDDd1_%bI^a}ei4)xb%CLbY%Avcp}cbfwdQX02JoBYd`}ep^=5F|-X9sAKj) zv#!Y)fQA%enf}T>0D_$)pX@7wEgglG9Cd3uyeYq;R%%vqFrhU&w8Q#jg!3kzgz5f8 z!@=#Si^ufoGz?jN_W@c~HFBA7scUTdjp!++B1J8-MqZJ>TL?|^X`wH~uic222m0M; zK^Iiu`yHQs6WbrF4a`QZvOYp5^~Pf5iXte!X^qy*i?n<_iIvEwm+g;CcdDEE&^Vz+ z=F~%l_hx85D+gDK4qdygQYdQddNQBkQQVHyGEY^@9+ba$Tst7d3EJ9rmJRwYgTG-* z+PHteqZ3HdH4|Ro05qX!BP=k7yr%rV-xb@Ye*R&(B-o~4)16C?KHlB2z-Uv+Z%o36 zryY4;!A}?$`=HBnfACwcj!7f~;eOOVwNjQanxsO!vZKMZ9ZZ7FlDNKi7I0no*4%en z{t%6oscx0DvCXUKF@C=d2j-85XA#a@9~m2jlkNqFN~E#$0(AOa#b!+jcez z-c8Kv5hpamTn(rkUz&=Vu+S@W5JM;%qW`@f_aThjDYOp@K5KACH-kfl;g1P;8As5J z@w*^&jae(7I)SI)wkWB4y$J^%iRM4-Q9*%7jud#f&rjv5bw+ChB=KhNYnG_^8UlEl z(9yX&UYvmGkN$inwM+^zSh!zGp~+RgNYFq)Pn=Ub{UUd+~GdUil(Ggu0ff30rTOP#~u)B`oFA$3^V4-vCUvVBXWm zO!plxkV&8krx@OFgKX81$~R<;wgB}O6sV)O!syw^AT*gqJtXk)s2s6_6a(Hjv4cpOqzJ$&V}s;|dxm2`=xVE; z#+F$Dq$vATqX@Fi+cKO);ZH7G+4h@zAZ%lUOs=zFLfl7=hPjD#=#CwwRReFwIuM{E z1c}VK{bxm#r8wa7XsJuLcVMlknZXN}y8E)zG!Znx9$xtf5s44s`6iZN_wpWsg^QLF z1{7eeFs@-pKJf&E%Z}Kv=v34c*pQ2)v&Lp%n}TKSo@J@r!N3i_`a}*7g!vK!aOFy>(OGu1`Kz$1mAa5G<0_ z)Zm7~vEE76wNWfv6#cif|9mE2BAki#5%p0l8;lJbjy40DG>Znux~+H4fhqu<#$rdA zd-SE7^CP=P`H?yMbm8Qi;)LSWB5*yK97k+V&27_4c?Kx`@g50*u2#$iIpdy1;O}2Q z?cjxU>GjB}Q@5;;iMsYS$qbnz%4+y>sAbcFjPeNIqqTA)?z$cO%MT>>CN<}QMl(^o zlW&t73JEkZkwhOapORmAD$zU2q2+!QeSeZ$qxMeLo3OKMo{n(ZC}b;<0ou2Lpb(#; z7P|mC zHFYB;)RmeTwajb*!Cx1AUE#2Z-Wyh(JOS(PY1h56xkTi7!3BUq?ButD5YI@a$m5n= zCf%gz-}{8gY_pT>9$5;*7oTp=%Nhe&ZXe=veJZ_gEhB|8JWSM+zs2J#n0gp?0_MuM z_vPZ9?($&hlOYzEmP0uw2(Ee{>+>!}MT%?G(8i8`C@ENk>>hA{?dL35)oL!X^`R$y zL(25|LZD$k0wUV6Ja<<(y4Hw<5b>Axvoo0fLWR`dCa)j_1z&6m72&;6t?2kJZejVh2zO6 zEGHPPOlq|HJJMH(#PW}eW#{N_B$(<;#KwVC_QM4h49(i`NeFrDA&M;YQ+1YK>276# zr&)$)1?V`3WBCt7SNOexeV5M({Z&)Coo41%jE5UhdfdgcVvuD;`l% zxPOFLryNl>A24*z2~^}$kYYtr2ZY{vW0Y%X;y?_=0ad)goo`jI8^Vhjkv%=IurRDu z2zVWNSgkM~!AR~l{K!EUTg@SbK3($jyY2)5U_j5YCOMMXT*`-IK0WImFNiR6maIt! z5iPdKQEmWMdb=;+ATM2+|Nk+ZmjPYcFA+URlFXA<>*FWSh~)iEZ|B3Rbt9C_JB_;f zmdJy`RgSqr3pXZcbC{Irk)SPrVFE6j*$a4}Mw*UnVj~~^Net~~b+y*Ns)NLX3@*!P zhkx`5*{@RlVAQ;Fat;>-Xs?uqDXS4*rCJ*=9Ouj!syYMk!d&!n3>#jS8?XA7Xop(8 z;>v9xON}xxp&doF@U|^y;NSyY)0c`BUPdsBZjvxM7^A4r5cH(xoB=VusYCB9y=s_p z6n`}*g7v0A+EIIx+&I}V#(Wqw0{18bnSu9OBo3tknHuKQoj|O`574-W=PGLO#UW^z z{y;l}c_#(+8Z1eQV4 zvn2wi4GW@qAm52`!Nv}W8t)t5%9W-F(vw%viid{GtzS4?^K{fIu%7U$q#h@dd*BHb z;PT@oNLb*b#WNlZfJf^!h1sr}yq~48-=t2al3IK|bqh3w7kcc1pI=HG%-OqARUk1i zp4KN6F*i)7mo|EtKj{ODwlV{mkw&%1&++Of)B}$uAAP?og^(+I38MH1h>py5bA&Gu zEbwP=)f`Y^w1ESFgH@T!_kd$e4MXm2n^{+922a5o(67EyyWW)qO9SWLNjwY*s{)ON zGzhRckkm8{2kj&sREdMggh#=yHSA1Ni1(VU;Ck&wBPtN@TfgV(Nm`}crE}U^8q1{P zCc%Tjcbdv&H09+2NF;~eSht^`YJnicVzTaR)gGf2sl7g!k9bhSmESVDcjqi zo429LT?41jkN2c+i5zZW$A;_sC}&0v0|p?4wQp@72LEaCBQm4c29d8GQ{>f2{eJ3g zGI|r>Z3qN;3f(0Zgwg@QBjXa_w;+6|K_~i_!n;Nwtn!!xjE=An{QLCPh?-$RUmy#f zzF1CB=(M@SE@0Wz2B)~j_Sh?I-~!Z%>M+NZ@le)|4taPHN|&sW%YDSy4^P7|0^iAY zS%p-8F;R0rnL2FXFn_CpQpY?M9{wlESu-mBSyv@+{P;lBnc4Jvl)GSfgseOe3dpCa zXa74#Dz3lajS4MCso4g%jUh?d6V@UvzEY}#i^lIDM%10JPTVW&LXz~Fc#D;_^W8K{ zn5pGjp(OUBQFJ;kP2S(g+Ydlj4mud4x=*gwY5ImEg20k{^Zk^aQ!m2UAIduHng-Z75rH8_L$nIs!910n&e6>~9;>-)xvNTm?J zU@KLdtc1-485Jm{zQHpOdFP0yj%Z?(R5X{=jwq z7b!lm{q)l21#0r3>@}0qE(dVK{OH-~5|p`u^vMX_q`GDilgQ^^bbV6b3m&MM{u5U9 zxNcsdgKQ|>!F>`5$y?c~7286Xtv`@%l@};03%c6z7uv3->YC=Wx5bF(#-0obgEzR$ z>&WQ@zS}Yet*42~Rr^y!k@>!<295OLjaAnIj4W@yVWbdO=FBVM9?4b(Ay6#d;2C+y z=Wt-?e!+2lN&NweI53!$960Pyfqz?J;BGS_JLP+l?Tbx=|F@@wc%6WjQrJSzF|xJW zDkH@gIu;_*|*}eFY_59xlXfctZ zdL|)5D*tACfie zB5^nUV__%)?99vQ#o*%S@0}@8wQs*3ESQV^T2d1`BjzlPS^JA@Mx2Zwy~7PT4Ui+fSYfeK&|dG$nOdBe0cEN~DvGb<%!)uD@UI&H=tF zJXeHi*8wC%pdha_6XE9uz%6(1o3i@C=U?&9rN=ds@{S|485F>)4}T9e%V&lIPf(3j zMoRjBeR?!-rcqv=h>thi#pB{f3uR!>KV>N;3|JCx5~D2`y*dCLP1_nY)7 zhv95j`w4)y_+x`>5i-~uh`pj@&nu@S_^Z*$!<4p=N9TL*$2cA+?3D{EuYxm(iTgCG)j70g{(YTv0U|&u8 z(1*F|HaM^cDQm5T-SqD=eb!Glv;oOozQ5&8eUe=&s=^C|5OTTEFlx8flHUVE2ESY z*a>wLFDY^>E`{Eko^jL7ZPM* zT5q?7S=*$Y^)ZibLiD(hh8O#t9c;X*9{Dj-ha7vAfVl27DU}zk3+t+!*Mh9nfyYix z$P0Q63wjyVOj1-0omG>9DiUE7i6l}1?0d^-qUQY3?a{#AL_E%8Px1Y6a-@Xs#DvCI z@rCn_^R%QaNP&Z$O3^7UZ9ipY9sEErwzG^@kT2ukGMNwAO7$&jS95yhw@jecbNw^< zAMH|~Oj=jk8OncI6#5+g5Efp(OhDrs`Y9tI1D^_EIAChbPwV5CqU7I;5+rQS7c3>PM$|2Damo9bI?r`a;olC2LOUug8hw|`UFuN=4+7xE@KW~tuG2I(CW(PaTXadIJxpr zt1&!Jz_Q(vb)_G4<0phvFb^B9WxW3N^A#(AC5xs{c?~1>0Do1oJmub9l0_(`54M=l z`+zRiWFtAy{^=O0k6c&=cQ5Jma>9qgvxZ|~*}63s&m9&J@n&$H*^HGBd?)Y0S3bx_ zvfm@RwSJZGov(D)n{7W6N$g71%m0KA<6ygdi(|qqp1&i_X(YH|W{V7WwCD`}rIRGP zTz3@_b<2G|FZF7bgyoHVPNQT^CkheKj-F;Y(O`tRVz^=QGO!3t^Q=D2K1a3iY9$ z9~^RS#Lt02dV_t!6EN3;gMZA0W;;L!a;~wJfv2Wr@9y8zNzF|n-+U!aH9cu*$f~Y^ zU83`;71~z+S>p;e?l*?|k0=N2v^oDPxvGP}e|D2T zm7rZ9*?QtU`t{hwUCBNWPiS`AQ2$-wkc&%21DFilmb#!LtSYPH(Oo?;gkSCsG5M?a zu`gV9gtlsjy8A}`wMMQ%jz~W~06IFBOQX=}2H(y!O_GJ5>;u{8(Axi}^+EG97nLzB zPX?O;W{SSw6j=l85$f}uT0lSdP33B9N-htdkmtvB%OdMQ%?x=^+n@Ihp9QFEaN!iPv&oQxXXjBfS7Mg0 ztfwW1!@xcXM$__BT6}l3k6<16XwiRnn*WL0-yy{N9mskLV`pVh-i@}Xto>n@%r@UKJGd*h zkcDrH7rzX9hFQMjB8MuF?K>6Ki?{aHr)96{(L|aOuh7Jp zu3}A|6Jso@VlOIqrP>4;NcvliPT)5Z_8G*3vQ*aK)8+kP@F~u-&x*bRI#@C*dwF4c z7(Sm`I=ygbSnPE@IYc(qJ?vas!aL7=UIU4QrNx|^laA?vOW1DQHIb$cmTWhWa?Y^%+s>PZh%0qht-E&(orLl{#DmD4qV4-+P1a&zw?Hv z%&PCPd(rCOqK8-Ph+B|+fAQ>lEB&RWm|q5qAU@%P-!IMNG{$8bbx!6*j9TvN+pI#h zNUL`hn=M`pONp0tl<0x)uFMS_fZIuF_pO1(3OC(Mr6GuFlP~uxQ|`w9-qi*nFEAB% zX>>89P}=X{7}Z}w=||&r{7J{}Ld60r@dO3?MV9=`i^;W@w4K5@^fbjvH|k`M7yqOY zkkYSgSk!+giUon(oJ5?F7KTD5x02TuuK5kNsChi>n=j0&UjLMR^B@j7iBaim#z%0F zERymzrcQvXo4Fr0yL6~u7)q?(6D1PDrzI+eX~Qleen6?PzWCYRi48ZUe`>OeyXs^ zrPXDTr8~Mzb!BCv_^&{%Ea8*wEMAnsUnXs!p+S1I9M2h{{FoO)xfgf*u!c=l^+Vgd z(TLIB2)SEY>fZo-{C5yZL*L+^646s-VmJUmoRWcI#M}qM-`UkHyEA(Mn=TrgGozGDmuJ}DC2~@(8N4H z)nJ8<`b3P^Ja`LF;dJxMfcT&tu9-cUaIGSo<@f8CzP5cmz*DM9^=q7$CvWOdBhQr8 zm$AsWZx6*k)tn17KRq|#a_1wZf}Fs&ChCo+BW>ZDnD(d^;M653R-l&}E9&NDzVwsI z>fIpFs^-jdcfQf9udw$AOxlQmpqMm57X!u!Qk>68SKIM`97u{jheGF7p{y-(t)GU( zKRhaWZl|>8m2gy*N`1K6QH!kC;G<9kYejTZ5(#s07_&OPE$}BDR|{Z@s)#>tw4X98 zmko-v{HvqXAa!x7WWxZ;?^?g`VC+7LfS&yfpTFU{ZW9Q|E znlp;FfLR#rk;HWKm)6LPrM15azCtC#HXBoW!GRV0PIi-mjrNpyi^)Jw z;vx&jz3G#jPWVC8Q5@%VyYNL#IZnL4QVBnxUq2ukG?FHoJf5_UoB)x4cMS7rUnC>! zm2(+gOKqvD)}Ka^RKGjhHs$1=a(;B-L$>|xnqx~6W$t&KO;k?inW z_h9dKct#e7ki0ba{R-Y;w7qLbLhGR?a5iCWWOt%nGPO!6BlFIA@XA9_q6lg_Sku^P zCLGs#&(t)A1f@~(Se5MYEd@ml{*NLI_Iy5mJIT@E!*%8t>r~tEV7v_au8NK~o&MTK z%G~?-rWeBW+zuyFa)VVnSYiC)dnU|O>Q%rBxAVGk@&th8M8D7Kq~b__XmaHG@y~CW zn(NnTDA-9|OqLs^jE6Zj%&NFA!#)=<3L^ZsyUq_9w|$oSderP0eyT=i?<=4zC)(iF z(?(lj3J-`>x^NWgrTb~b>%XTqN%gU$3=Ni!`_iG1E4U%0!*)T4JTM-H(*Nlo#hROx`>R>$wY>XD*A>LF?e=-#E(csg^n)K~_4k~GhBuHY&UUkdkkFG)WGe4ODCR^t zf3lG-gn13=$)%Py_QYHr7l$e7emWk z`Ay{#qdrAdNPjzBY0E*Q< z<)~@f`Y)INkoj*+HHh$to=0RCp)Grw#6tPa&3jt-O{;LW;=)Nj78|#aB7>q*|;Q<)=9}%q&{&QM%PH`dLyM89PM)nb!?lo}& z@;6Yv@8y*EliiE*kEd8;c2Z$Jc=3u>))Gec6E2|9VE}k5;_?#BOrg4r`H4m%pZ$_s zcF{HerepZryQ$5wpQ9h925C3gdpw*Gtcchh08z_QUy1_>aAl*keZP5+Bv60@ZV5vZ zEUF4D1_poQNL<31zz(J<06vP^hV6d!mT3RKf1f>Z<)wtwDkMJEPNHhWlq-L)l`zp@ zn2#ixHO@@H5o0#i*BYy~4oP=+NM0P2F0Vf+2;AU6gio7P{)Co&z45xp9_(P0%( zr=w@&cDC9qo5J$uWrEnj;K&9leEgTf+H$CSnpb@t?>!Le+B8v~W+K)wu{6Qw^ZA&d zZr*I~LZ1;g2heZgQQe1dNo-b~<=PKBGc}yo?=jjkL zBwggn$1RMM29Uq~RM|iKTlg*r8Giq*u>ZmPv1SGp>bxF-S6cu2o2gjX=sijN{#FmB z-ps!{<@wNX6vx1<-Yc|wB6KU)t6NW%NDD0U6scAL1R_oF9>3{Mws2KB1#x=fg(xcE z1|2E*dnsc^xWzP%<_mitNyFAlmX*QnNp>v8m`MAaj=lt$v^sEQQ*}w8D}2s4iwANA z$?S=H=Ga>JBhegbAzp=1-c;sfFqt18l_kx5)2LT+-$|fWp6Dnn}H!4n~Wsx>Ef zDhc8H-etrft|jTS$&ePMyZujrQ^?WaJy7De0e~w&?No5tw&LMFHjy6jUAj+H`)`yb zEx{m|6QGt;0gse_KzW$H#>+q~@~Z*yY_7%r))mF7qQ{hCmO?aN-jNeYKUdh)=mQM? z<|g=ENURGT%Hb95&0Nd~pM!_A8?3uw)nd|YQl}v|adQoE@+NEx8SoUWxN9ZWPSOzM zrRw|i97UZn@8|-oSmfgv3?3Z6t{$QMHv}&k_F+VoX0-sGbiUIXUtc)5sVy9l=l>BR z)_G1VGZRFcz~0nuBiTqeyKdlg1%(jJs9z5W%Sb=9!RZ=DbzJW{t=Ai^Y8pN;dz(Uh zbm8NSu!$ex@fe=KS1$z5~YUKBX2?u2Y^F+gbsp=+80@tG+KsO=!%7q={$X*{S!8z3M-}j+y z!xw}{k&IKwviL+ch5Nkz%Sx`LI=pb+miXnM4`%b@nA?Cx3pVzZcVxAJog|%43D+H` zVsdh&y~xa$JH7u$rSD@p+%CHcax!$%9|TqzKzwa^4QCX+k0pljWS^4nGA+h3In>9J zzsaE=Xfl;-GeP;G=yVPM8Jl^IHamPiQ|s31#ecLOc+?>%F==^zjrX28iPW z(~(>&Y);3V)7r;%oaySBd&8W}(z{zyv&jaei`A0Mcx@VvOxkJNgSs?$MO5XEKfmp# zk-R31iY8AEh%rmDokk4c_!Z4DTo}wahOgCo?`sGQwOT~IqaOveCw-ejf(2o)8=7HI zA%RT#1scAoVbPxPP71rQ1r>_Ahi(}@^xT%u)x9OWr%lT;8-@6%VS8Vi?X((S`|~|h zgXJ+Yk&cGc_A7+WPv%!5@mXrC-fK`~_v}AY-<|TM%o$K8GvvGPJ$x@`Vz;TGSg;}7 zEou#8kwO-p%=%)pmLrL(Jq~=WN8cI6p=tFDH&62ys@tN3CW21xhIBXrkB(TPSWve_ zA!gjbt_OQCA-}>Afv`MLRY`uIF2>Rr5^kZVK82uOw^*8!dY5Es_vcG=nss&3kI$kz z!M>mrDi3|djkWwFJ*g*GpyBkll;KrvO9};YPANqovFt!h@1eL{ER-cwpZbi*lk}rm zH*Kv~O1eyZ&^$LR)-)bIEh{byQ}noeZMAmB78H6@WI^NFz;c61s&v=#axJwMfRFI&p+@_O(!;fjI>=q-{0@R(o}y~*_mt}a2kb<(jbK+Kf>NG z5lFl_>fS&lX?8Gg`~=+RxffjPI@^6Z%{Kq;4?V!xeH2{UyC|bi0qU2{{Dkogy&CUj zNiEEb*LMF=b&;PWqVb$@?*mFsNJW8XgV+4Hxm|R32F^qeu|YT0PgSA@+nd zsdQJn`TPeVMSG=UepPJpK0P;PrcHA^i5vj`JZcR#QqCv2r9`&J& zEA_AtzCUN`XoCrGmh+RYKP1Y8B%gnp61$G`kpKBIQ?8b|?)Eg<^lgt#;Zbs@xG`dx zw|&`I;tohJ$v;Ev!1bM^xJm}Ql2av#wb!Sg62Z=82Rw>1H7T`2hBhumg);*hu4{aE zeJf!Gbker8^F$f(`ODsCO$M?}2_!@t{(w6hJ4aDg4cW((1D&p7vC2%$I08IUnpBf5oBA#W8F~1R@tf;a?6TdNaBJkF za@n(o>Yx>k*QSaQ)`QiuYFE3AI3$N}&^^R?%u>p@iNyfMvRWUMu|T?6$)|Lgup0`8UqSx>EU#KH2Jbv87pjC_bC& zY&W;HO3|9QSI5#o{rOkqflAv;H}rw|oim4LiF(NAGQc9l)saDfb^BO2WyW`8hsR^` zkuT+Rf<_FjdcyrGW>YXI+ohb)ZaDcXBHiZ2GicpUtDzOlPmxSeeVPCnE3@hy4>l8e zo!HL?iI+=9+C@waa}0J{@(2Ef2GyS{dztTbA>vV-nrp?MwjSjLvSrGS4_QsX9xlS= zSKqT1Sdr`UeEki$aCsXRs(f)C`4X4?2$;RQ*t2CRNbYc0UQ^jMV@VAEb~8KWrSI|Z zn4uxid@Oi;m}B)-X&V|FelN`S-p(d)P6Oe0u}0e>TXl zq%Wv47YRvTAa|psMjK6*lR=B8Fk)wp5c#xCbc7->>P5%yv>k>P9cC8F<3S2@sRXgS zmt`YnVhKu;o4|iCww)x8g|1)Vr!PqsG5TcK)$YS|i27I*{(97%s1|(krxx9fV%oSf zYcQ&k06HEe8{Oy*NivY)CMj)d57zGCY{}(cnj4uCb}CH$Pwvx(ojLa{DR{DJ2OfRM z!&%|yuQD@k@HY13P6ywDdR%?GFQVX0@YiaxStFQl2Jz&54}^KKInt&1`K0eV3Fshk zzgu?671#eeM<6lNI+b6FZ0{BlQ>7>SZSuEV^K&+@k7`@q+L0W5qPJWSN5BfLDmRiQ zn{?ujttuY4nwY92_v(pALW4e#y%jusc^eMgd7P1A@Xq2VM~s4k>W~a;6U%dXBj#T^ zeft|QLM09Pp{nMqGG$J&Z#87g(pKjg_MoTpt-WWg00Eeg(@G2%B zDD+G~WS}J|{{}daDA3S8^p?W{WeX(f>Tx?M$7F>Q$X{Q&TlMvw&!GP8O%=4u?~|Slxd11}o@RD+=!)#UbQLUy z19d%y)S~V0`aJd?Sl-il02$0wuJV*uWu`N-`PWEyGrB9T^(FF z>J3AIZ`kFmoQ)T{Nqx`WCj*%~I1nI>w@b>{J#3f#uMeDmv;PG1?6+NH7ZEH`KVYT4 zO2)H|+LLg_P17{9{xNAYe|sPCTK4yL%+|wfIUF%WHVDF&+CNswOmXi&vYIZ~^~3H^ zfG#s`z~g<>XF=B6vV}G_Hq2;j2bDQ!`gR((2uCKR3$~Xt_GuNy6_U!u5qigz{>pn$ zNC@)cGrmNisE66zG>Bh+o?7<7`81I?+^xV=C{)3*OVEAl@)6J{iO7k=$sHWxNQ#tw`S{2{ZhOShfTFH?8k+ zns}bwYgi`7}nOyS5kgv>B$2V!e+CRkNQ!U^|!kHSC~|+)c=HY zhaE_Z2OB=-ReK{M7D77u_HL?hn0SX2&ctsQZ@*F)p3(RT{Fl=^MZP<#;=VPMLO*q$ zhybsFc{)G!0`QlC&g4zAQ5JV48BbyuTjU4y|66+~XFh0D`0k9p37_ij0sJL@f4o1Y z-AUYp>tq-Msnm}EI4?d3ZgQjG=11xz5s{!$c_DoIwCQ>~{>WgAYGSWAa+FR8XSuQ` z8wC~(0cSzCD!yAw27}0rc1^0N_vE|Ql%s6Fvl>NUlzsWIrHxAQT&B1IM77638*#R* z3fwt}eVy{8m#yYp`3w@0jrR-_XRT(&i#u0)P(*ry5k@ttTRWMbeUM!rFf5-W8Kx6) z3S~>dXF^pxhkRC7N=wTWA%BVpS)b2wrQ~|tVzjE@Lk-n_jT?Kn%Aw^$2E1l1nA=CG zdr%i>j~4ZuKPDi2MGyS@xpmYiBS7uGfBkSh082o$zh&Us_(u#Rsk>?;z>PLUD8NdY zlPx*fYo?T73Sej=FO4>dj{n)>fKn2q~k^uU{|VgU*XHC!f@X1vNMBvx(tam6}UP*Vz!nAGjMBv z@|mD+cZydet7=~FlDv7WlsNPZh%pCfQ!xEH^B^?ssL39|+IHy6lc%vWFQ%5gS5<3wi;&BG0#YiqaOH9zFag0ZlCB?+*aB^cfG&@Xh(#44m_Mh*6k^0A5cKX8r z(>3VlJ!2{O8`cn6sNwqW7IwJd7g!GLDB{ZjX+xbdVfM8>?!2uF$?~_5r0lvVQo%Q& z=*ca7AucUW19bEp?Vx44I3jhA5gJRyaog-jpSQ5bbBm3kJIh(bzFU!*K-OVJzB>d- zq-2EwHW@$UUpx{lpak=aHwvG-FXw5d0vaS2U@+N5D6@5e$#6*T~nVKSPs zYk@I}4s!rrX%oK|!;z6oOd}0NbQND)N1~RK!Q8VeG5%Vye*4>30}Y+O-DDtn=}11M z(7=h>j1Q;{l6?9W7{RZd__AU7-ABooNI(1OTpi1mV#5=BLD{i{W^JOB%h#rf2iZ?~1ua0t zo=j)XqP5_p`k87PXZnR{wqN<%3RC~Ys0??C_1Kvu(};VP6L3*{V?C|onY-svkR#03 zVrk}hyUyzbgI86<(-OuKErlK%;He?E(9w->6^@5_ePrfs2SC}CX9{y9jvw@I zy-esZGxpwo5^-l193c(z|FV}M!Fio;?dn4~)RqrUS}|Bg5}183?&W8t*neK9dypaS zhnpB|yVBo|c_Oxh6tg0%mA?W?Czxfw_%a>m#`Qmj>w*#PhXDZa?{KF(NecTC60_x%>n<9qSSbaSBT#;ID%IgLgbyU}v& z5{>r(E*>NOuJ>OSr2F*#DVSI>PQllZVQi0;jE;|IRON+a(dau0=3pJ{#T+^Mn`r)~ zO!si%-_20VWfUsM;+v63pT5HwQNiYfq$>s}*q8TF{d!Xw#1Q+jXZyw?ybW#!PRW(R#DfUIl z^RYTO-e5&r`UxmV`s8&p)?un79q%YXriAg8lKOVh{!7SPOr=?PWsUjXB?V;+s)CXdZCgpDJ^xNL~qC4oHXs`d$|#g1N9I^-l-^%e(KHHuiE{ywc^6G zGWFcpL^4co%pQeb5VN1^AVEIt`U%-urJJA7?f@u1rC=Eqy(Z znc0K|q)};phQNpF**;0Vj%#{c8XChEbd?BOz|J^jt)FMvLLew&WW|U>izevtEx46H z{#`XqK3VIbDgKQLU;cet2?Rh-=E~I@a>oZh9%!zFO7j1(>gO<1Ao^Pw`bpyMg?!kz zk*o1H;PgsPgCW&iW&jmyi2f<{2Lw6x)>ioCjSPcmSQq0w-{F~K=8n9N+_(maW@>Rm z|6H<>M*V>#YQo)rVe)BU3CVO$b@rP3U*se{3z>VvfNG~r`8*|?(XUH$Brm6(zVJ)n zCzA`h*@wwhgC%NiL}>hQK|kWqUf0E{#^tuXV05Vo@~j~&y05ya#^xL6_fDh7j88Zw z1L=PGW21Qcl?OkFRK7DevqYEdf^n-6wd80LwUEVp&(}8}z@&gCe<7ReI-T-$!i0p; zJMJ0HcNcO3mBIMNCpe7jZH1_a)mM9g*3AdlCCp7EFmiIcdn%P66%M%|?%1`TUT(R2NTTFa&~>mZ6ZqVScDl^h*-`tSx>+(5}M(u>2LA z9=cSnXm%kfUBJ8?2&pL7=q3m6-Tjg4U3e1N*6kPSZV+y`;5IgsGTHBJ5(vsM>0o?r z_;c-nMh!))^@*HorX7hIKJhRFtku#Mh_v!1TyS(?oWT7CowN~4n_?sELRUY|GU_?E42--1*@86@oze4NJVT#PzUYLyF_s}$~4(3HrvdiICe~aZ6a(4r5g9%K_ z@5q?I_-34m5HVcX$T_jMOeKK-;U*LvL)-*{hmk?vRO-4#{zL7$mHgB%5GoLHN*}%z zDaKUt$TOXTcZ5={Tq3H{vn$K*;nl+i%aB_gYuL|msq%iEZ6k)AXff)`bEnuOX`rLL zStYl&&ri^~G!teu210{#)IFB-z4Omn$b&N$@`F%kyR|NU({NUf-2+TjhjyU1?QmLi zivkZ0vd*|-)WwnhWsw=dHvPBQ$MjUii32|&_D4LgwM4R91hIe zV*NSVM_s@Ne^&g0 zyz9o27rY`4goE*SP@4mcI%BRAo%mmgw_1;>x;JVj1;-?+#)DkTDl?{$-pybVBGTp` ze*kdG28#w;oa1GZOvh6`*aZ2zMS^A1mteY%p#3)pJ;Fp*n60K!C&33dq>=L@q=q|`Wm&BqG2<{it5iTxYb zh{?tUrAz*(#K)Za5Aq)i-W8!JTp~P}25Vtwh+UP@2k@~2TL;>2`|q_?I=XO7_>$gp$+oA8Sy^pmlj@nXM#WB08jQk}U=nfVlPx&O%Fz zAQQ*`Rm`c~*>X(c?DYeH#D%EtFt(7!TOMkOZQMpxq2}0@r*>%dXDt>x2jyHNzD7(jK>jQBq7N+5@tI6X!pcHtf0t=vlJKL-H7h{X z`rfIbb82~#|EV0G0{_e)Sy5BRhLE=vGVXq^>^eb7@g;Lk-7$pG5-{HF|2_AigFqa8 zuul1xMgTB3?Kwyev`=~Gbdq5F7}NI4(~1RL)Ho>r{MsTk&Viv-am!c{V%-##$aIP? z1EgeP)CG#QfBU&QkJ!A%S+4zp@w-=9$5R`fd}Q(5d#)Z*_Oh8!FYZm>b23(*>(}GG zMQF`ZnXy`asE6!(r#s1~ikKZGH<8qMNlF2-KM0jSnk3Qd`Vp__u=?*QY!Z--t~>sG zWYom~4zpzk7)2x%*shgKi84juW{Lm#LL`xVY~C|x>d0;93zq9UM8(qIwZVbyK1%=W zBDS)ZI-Fibll2sjNnIbWb@)nn^5}IPgcdnS-4i#7c(RW+4#B@XdO`i`wwM^v@t+*hC6)}#O7~W1pMG?eGWi;|N<9ft z=T^J8|La#)_I2zau+Ifg{0lAUq@C%)W`d=YU0fermfdqcMeLwh1#YkJ_|~I!fZb;x zug;I&K4xtquBQ3#p2i$SM$zM@+G4rv1JIMdO#4fw561s>+o~id-iHs!r#p7U5pfq- zAW_U5Z>|3gQvY<5P%TXQXosSY>Y2g|7u5X5sX_da`*v3xDO8y}^GWDTqW1MH_C2L- z%TGs9(I<0YkhNW`g(@w+0<|J~f--BwSn~Z%2h^iLdeeQ7|@YQlxz2`1jJ0JRF0KU3Q7C3a}!(L;Pzd-wMyRx1cLQhoze zPd(1k@W}#K4>Qks^&x_KK8Yu&xyk)&0EDPd z^1s7`2&->uZrV9|Y>k~AiKWgA+mBK}JUHe5Nl|#e)h61B#MuC{lXVu?&kkQ*F8rJ> z_3Nua$nf{J$cFoKabnh*-xC?=NHc;D65(YJh#Bgx2l2gWr8jGNN6A{Ki_a{fwu&Omd`FTcD(0+>jo=5gBf zm!;N8Fm4k#I4Q)aMa5ff`AC(9O>RGHqrnvH0_~`$#{nv>o@%`RP0SwH8#$c*n+YSe zxkO%k%g%i$^Xu?76pkFayM`t|0J+q<_n2!EA!x?kJ}&DI0UHBVTBV3*aed3rDZp0D zWPhz6szJO?)oSWB*R z6x*ThvGSxpyvwVT`VFA!e@7Snyl$m^5SsZD_kvUpKi>6Y0l%0-`UO_ufNk;R#LE^S zqj&5FGI37Uu}@f*-@ZtLbELHq(`^9(^UF#vT>IUy!ijj6ir%nRLs+8Kim9G z63Dp3fQK>X>c&Fi&2r9nj-W(@2K@>?|%s+Tg55fUtq~V-1EE4aGyv1 zD=s3XB#X3vp0KtJ8Ecy48iJL6dB}d)%uvAJCV+9Q`m z9Fi{WAirn&DfJHnYq*lO*iU+um1`-Sz(hc=IHzyz{K2-9M~w88r_Qt{iaIuT2&F<$ zP8iv`Igs%%Mw-jXt0=JPL^sxHU0**gqPwcK>_X4Sw(DJ?B$ylc&FVZum@w6Y)i`j< zcKet9*%H=#*3p$tErN38%9)ST0&RGiHnD;%^oUlfFi){G(TvWy|2`7q}Y0u z^Jd;OYLf!W#acf15lO)JiSewOk&?8+CcWgsaWDM~-tr&hljR3uraNpxCzC!cj`V7* z1kR(UG^u<)lU9Q$HpvMMCRMimCIA1k6Hb$in)sW6Ml|I#uG3v6m3LWNOVE%sp}IqJ zq$F>RiV0)?G65}J!EMsqYY06}PC@mo$`TYPDu^Ur%C2}t9L9&joAIRp6C91sn1&nP zwKn$ILy1iig`BgjzSq?4*AkVBKaoo~rM`&=wXd+I20A$SDpq6)Gj7tjiR--6T5>aiGi3#hT3|~LA;fWrYtKxQDlLH$D0Q>(rrU64~kIz*9akr zhNDOWmOm2vWmCnd5cA9R4}$+O{F0k8{nL9!%(1prvLOMkOlC%w<@%lw8k-B@Y?E># za~7Bsfp}3BBLmQ{$<>Z)@@BX;S4j}*+ZQ1706y?hDc@<75ZgTVkWSuWO_%@cV?C1l z;_!MJx|jQg-Sk5cf9(V0eS{bd==A2;lN%}kDGwulQ6LL9Fa>0G;OlZv8KF@EYfj*@ z^Iz8~x&N$R$osQr+WtNXzfva9=#AV)hU&rI*;l+s^5V3%pVH%No*_I^_3>jjUGuP%0^=;QVcMXtVF} z=@A*q->o}}zPT77a~#Vzr7z&Q25dBN5iF9kGq?&B^}xk-#A&=1TNR&kWf)*fRlMW2 z4zQK#i|@2VuBWGgu?XqHz&@)DT&j4MmA@P(J85K0oi@R0Wbuv4=1V|-ERce!^Nwem$>pv5M~ zs8StSIr$Jr5FkwVa;$ShyIF|NK6hCZkr*+w zg5w)+6#6B!CGNfSf!DGheOXqG3!b!{nH zfNz=$hbSVx)z2F6j}@*8^1?9Fj~le&RF&Nfn8t955(Dm)clHARd37W-e{;aXD2($D z`ed28d{6Ye^!N~83S4t{(nT?SPW)DwaU`51%l|`>(iQX4=-Lv>3MIXYD84DlUN?lb z*x;6(C=X)K?T=sud}A@&a|5S>Ndy%x)J{mpyc=14$wsT=3xP)XZ_uJi$NOMzkA^6A zd)X~&2iOQN;#tJ#&uxTkCnf2fKXQa9&=n7FbByr5b^8oC7J`)_llk*2BkO|v4$8p` zVAYy5Q=VL+_u`zrPO;#zaiaje4-pvleu{YPnP9VO#EFM>9;z-y?F7d@?NUWTgi_ie z1gF=?fhP1p%S?K#_1_YHf{_fuAr7scH> zqOgIy&j)WGxIDdSio=~Xb%Y_hcuzR!9;1a&9XvEtU38nl2S8pB&z7>WHra3H)p}_; zD!;`*Ti)vWpvoJEuShjB>fWMYpUS#4Jp3A5nFkQkJ~V1OSQs`>UGTE`p*svl-}FvP z>0a;<)VRrE$HZTPU4^WPV7_u;&7^kwu*gGd*R;4X;>JoJPh$Ckx0PaC+?eM*z%t2w ze!$rtd8T&rnquV#yn$fsIzK`4MZ5h4{(T>pL{=?vd+59bdqXnJ2pf$wCA7X{rYSO4 zTT(>fYnL!?3aQz$Z_(UcEyG5WAsjgI5XJ*Or>+7SaguHbAB-FvSPL{|DlO`}Z!YxC zAzf;*jrUb5lI;2Ls#aqA!3FDW&3t%|lt% zP_XxX)8bMFmgqLIQl#s>q<{#2k)B({xzcih1E#PZNsj~6wfPqrwo{(4Pym_toY9|H zOt{t)wKs`3Bt$C7jmMb4M6_7VK&(RycVBj)>b!9Zg?qMSqxKu=sf2XdQ`85F)HAog zlfMWUZjzn8=yVL}=4aMKTDy1W2L{F&Po>id!ihF{C1B8IphR;=rZsg^rIN}Lg2IPV zeh$69o&m|~=HnL93WjbUtpW8UE9|4?>DdW6Qj9FObhl9=deNO1myy`*m;1l1%~LV~l~pmwCQ>UU21c7cOYh@Cq)pe=exh&muc%`m;3*tOeVy zi(X+(@J^u^`M~UR`d?3^UvOMs#{Y;oE*O0fY7jB`F109#I?t=f z6PDGd$L4R1MIf6v^M1lj{v2W6q)KG-J_l>!*}I^S)4xkObhw+Vv6K-(omBtD$7QMt zv9lI6(5Gux{Vg2@Lg#nUWGF2-N-etp3{AN%m&L}T!op>#4CDo07F|1sFC;A*w4JPs z^rb#zu?q2V9#ic}Z2nx&c&Z||C@PHZNdQeI1g`~Nd|u*InIWE@n_=!~CW;hKB@Rmd z8=+$WQEs5(9|bS>Q4?dauK3=v#CJiEJZjB7hZvRjdpKJr}G zJvONP3n658U&KjuygCn!YLQ|%XmDZNkJ2Fb+|>`=337a6cT-KPP-6n5ljZ-$aWtYN=-N|i$Y$$jm+A4YwueS|7v z)=qDQwj45INW*GBQ0|H79k5cO?BY2gyg*-lE|@p25$lelg%+QbG_dNg(NWPw;0SC{D2QbBez) zJ}}vDX%MRo^ywdMToQo4eqp)-9=Ji#+YT*IkmsbgPJFxtca(%hGO4dtxzY>}UCviAlbu{I?uZ1&v z;AM~R=W}}ldCj~1SI@Tkh_Z0@@f-mrGpC<0TpIlD9cnGpFL#-&49d7_w=AE}FQ{tF z1;Feq5%G&aw<~#<2%Q1u+bC-C+LjUCs)xUL)1}US&Dw}#RG&?+m1AP8w~jSS$~;9m zRbcxpyE0?#Gf#D$owF!j{j{V=mlwptrsd&7;B(H2m-m7wD>?C4a9S{3w9u7Y3anQq zVTY|Gz+i9w;t~>EeZj;UFUwbRfBVdD zgISwr)Stnr>bpG71}5yF{^!dJpdp=Y`l;{~6>$$gHWL3H=avo4Mud+&ed~*kUl#pR zv7wTP@fxR~e2)SXn3Q?e$v5*-F>)!Z5n%PfHMzzDv>ybVp;7m}9KxsR(gnsAlfRO5 zEe;@g7=;SF=WC5C%i2eevi%sd=8S8la#s7tlYDSq{c4n(#IH}x{?IV6B;b&jHz|ma zA`@jYKvC#>%(=qQ?v<2hywlq4 zo0ov0>;z~qpmJo3Q4{MJfJz>{CqWA#sz5X&u^2yn_tdlf%C|>6jtu z&|!>MIwBuG@Y?|Ac!AU1&&f$ywEeun=J?6||2Si3uu{nDl{rIcChI2&mepBA=m0CS zI!;`a2v1E0mZL=$wY(h8`)}N>gKQumtvZQX`P1jB(S3A>v`4brEcI#rGfdbD@3+rHS zfXbXUM3r2n4)tNcr-U5wv-*f$aQ*1EoDv)NI_kW($^LHuJxZ?0$Fb;QV+gw}Q#|;p zIPg0w2-hAvzoLfeIVG>sJgu;)%&qll1@V036K(bo2CJ~=IJ!CwI-!*0Z;9&4fDhD!Cj-D8cj>&Q^60TmIgY|O5^-k57@Fjmu~V0u z2q97LiwCiSXNXN1Ik7QUY!4>+@L1#a3PaX?WbJvcuZv+q5trvI=n`ZeR|i&dn}hm9hg#HJ$$80o*E2cs4#wDAi>Sw}-yt7ziHZB7O1 zbs47BTYyO#)id_av&B)l9o*fNln&Clz6mgbKj8}`@|BaQaF=v6n~12mFd0OSS0-Nl zmRDgBFx|W4zi3Q52&72g3>v^T2+wZ4LwToD>hJ?&?eUj05>!J)m&Flm>1TpTPd-|H zn;#tu#<`3OxYcHKTQP1QuyFQL-pCv(WUqqXTwf@;=j&Abp=mYUH%z?KI?mkJ>8;c` zVU$RfzDy`slC7@W4JWMp3kVXt#2#tAllWUuB6Fb+clm?#K9;RVlvV>0v3FI z9>uJ|qi2D}fgTmzu%EVF)ta9{cAo4Pss5!#A@lJO@tGjrQr7lg)8+(RHI7X-aA(sM zA|Nj}%NIv`eqpF7xYmvFhM@KzPlaosgXM61R5eJJqIvsZwPTPwp;2~7l*vOiQDDD@ zN}-)OR6!v=MCLkMS;R?MG4z;A~2UVyNhNzOU zmx^}3-n1~^y_m;sMLt_4cQyNLkWUYp`f1=}CB5x^T~B`qWjy$lT+tHP$O$k~XtPT3 z-!fA;qo}Pya4miahda1@?|X(>xQYusSl87CfMB0!b!o3oY8BgNDMw3UpNwkMA;6Qr zp(hX3YKO12wl?0C+>aJX9z0Fbl8MQUvjmO9(c3@}ey?VUK{AfyXMtwa z3}sHo8n==US59c=Yo2|6hpM!$e>@0$%=}s<+t?2NhwTDTNB;3Z?(qKe+?vsg0! z$3I$$r_ga1<;$wx^20MkJiT1QdRc(3P>b_F3=C`J)Pc^sZhWp495yRA>dm3r;b)_7 zWoUdCh~a{jH#!!qI+VDsc)@X4KasVf{FL&9y{#x(Un_DF6f~ z7KVJ<;&j<_$+rC9NuRw{{gGj}(i0`@6pYJcarJUNTM-%DjK?j8>kHX5Q zWULs2RcTR52o{1YsQRf4SqfgDWH%O$Yjtb&XbH(~I6`s8f20R!=LHm%3{8Z~?LjV- zjBf~@_^p!jw@j3s@CD8%wBKXxJg#+)6)*>gwxRA|EpbFlw;~cIozYn)sv)vFP4uxR z>FR+Ab2iSiXRr0 z>;2MjAKJG5uFZmnNBDT!wFnsB_k0&Oyfmq@lX7YCLG(3)3sd)2vCQ&a7 z48=2%yYMQ-6adGdk2-ot#4rb5{+}*D z$=BRFjd7~lrBhlKQpmr)Ez=VAD@j~)?T{9Rj(!SZI7<*&%(L;EwczQ-W}#k@9Dp_t zRZQlpcw+p63#mlomJ~wqp@gEzJ_(Y0ec?UCwOq71dbBJ4^+D&LoB~=e&}o^}Wji$e z^x4AcQhiX>-rFnZq4za-6Q??oruU!x@$~t+P-~3EyVy)zeAXk5GrwM+$r3|tu}GZK zW)X1>>3P$z!0d)bNATP!(rjmYzXpm$C01XpE2OH5eM51k$zN6Y0Fj=ff*I(wUVN`F zBXOx|G0Iia-_I{Pbl!hv_QE{_$drQ#w%0!uY{YZdm?Rb1fDbyfUKkx%+g0#dD-*nTfF`f z4x3dSqpK@^%muYDT?g<1~MifkL$fX&Hw(P6N=N!4yh!CTlg8EoTdQB~9aLry8L z2t;6}tj-s)ek2ItUJ9CDxH^qL*H+iduU>Kyz6KQ(0+LWenDd=5IDcymOigyW7Gu|= z!I*kORmbm;{=w_pOWhx zxu48hMr5niK0E+LW`jf@%}}4ip{!zctjXf!7Zzyk72jQ$YoS2VVhr)EMm{CTe)nHF zlF6#_U#Je`JrxDVKZ@?Z^&do!_MZB!OSV=gr+x!djn4ys0yX!s14p#vnTe>W4qt@U zBeetQAL>6TDR>~Q2|-H+=$*!or|MdzZv;xPn-8s$K(mXbZSzJizL}HEnI_GQaGu~V z>MgJV22>Yp6}!y`Pwt#H#wI+F*MFY?Vdd1U6?UqH7EE~)2nw1g_1ED*R#Q@)EX@Bw zRUdlJnHQ|!YNIW+KqOMJ8guyJSTKu8Y~bAX4yUtzs3DTNRf z+UoAV(PfW8EI$K3G>p6U!C+f_14-dpSPS%nu@hWyS}??3>Ig;9kNRbl*?TJ6#cvqqHi+aj z`Aqjh&^Kd!`1|{F;Zd|}G$4)v%w9`svHH@!7kREOQ4tgPgEo8(?}bc^wS+B+y-l3VVTH+VxGrVJW*F}!IMcHSTARh$u{**K40w&xg6H8CvXDDQ4g^o+^5f*ZOd2h z^K;4t9hij!5wN z(42b&%|Co1Whue@u*ZV5G(iE(^%@F`yMFEBxH?qDCer=ts%aqTrE5RzL4Q>M>>%`% z>G;vWWfY!>!>^Rka21J0=z7q5(E`LFM^-P$sucLfSacsawzroYhsY>Yy8u&&zS^D! zA&GwRT@6u4rSAv$_!Cm?0{z#xmzTX&~aU9kl~~ z2e)byd$Q-JXmEl9FgfG~Dn7VF#*+Fd+aS!P6fgCr!*Et{s$0%qd>;MtEd;#{K>F*; zNT)72+h4&Kmzn%<6U`zv_9wqyu+br!cIom1Ew0RLeAZ#3vwNHUV(-*= z;XUF=2LK-FEz0>bX{d)phQTB{dii^AjD{+1HP#aQ36DtH%q(bwcD<#nF95Rg8RH69 zh>%qfKM94}+ucg**GQkbtX^F@<9f!Jymk1$E(Ii*SlKdPY=7Vxz5#%4U|`j4>F=!v z|NAs?9SKNX1yHhbH`S_CY?B=229}O;7A)+*oCCG%++%|vo(5{ zQIr2}`)16TfyifGAM1n_FuywD{Vn-kh0b&_Ezyv$rkF9X|G-G~Dq8`Q#Rk@_G&9jg zL5;(xN9Pa;VT9!l&sU{Mo~?Y{|K<=U0EY%!&4TCk`l=3{9%I`a@2V_hF8$1Q>%esn z+iG$)bnGS7a`j$m!FO)PdtQ>o(EXlZZYinX$`(YJyXSs<57m=6(mx?r{NeQaVMgMf z)BlQuNI%JHz6l}NPKSGzt14qkht^iUW2DUri|sd?7=uIBW)cIEcx{{4MzbqKVhm$?^&l zw?+9`{(JvV^L0V$kY`S|Y>C`%Uuguv2D{TdTQ1nKF=D1Py)?kd{WdwvQuGo^p7|^w zDj}nm7^ ze}p1vTWvp$e0G{e`?yocW^d1SKP&K(8BUb=(i zgj9AuB_H5U5j0-dlxxYYG z$&ixr6BnxkU$d%4t~E0b;!Li0<-|!@|J8S?HuW;7E|DPN!xx-{ZKlMKB?5gD~n$gLe((H{;x`>ioXM{{ix)Vyx!=JH~Bpy`%`1?cNg&@e=} zc&y6mlh=YQbT~vIjow`aykOPG%9x64L zxTNy_UZM{~_UBS8P?MWr!o#to*>Bf4fmOlB4$Oqj^-ySDa-31Vf9YGW3;I}x;lIFH zZ0027$JIN!033n@rcsrRUXRt8GFkCGh1I~Lh2AwUUQ!m5yZYZv@PCLboChzBfYHUh z0Xyv(!(=Z*uYFwz34~cZ9-;3Gf1JKoTBN=#d{Clh?H5C|?tG4gHfTX*UG<^7p|Pyb z$5sK;#KxBMOGOHMyokO7BfO%DTp%I5CdpEs2tscCCyDtke)0>TKI^0N?;H>Qi@@uP z9s)vsjZ1sM|6Pds{XMw5=e0#@pT=fzZ8%JyQi}sM{xnH`HXK4GM z44&S$6OMs;zo&ZQw@1M5klZ*z;t4jcUQw_Z+4~R`r*SB$?%gt{L5wC@zq~WofS(S( z*0q6Gvvqj{zS6G1@8I=hy=n6NH*v*gz$ng&lV*#9?yS#kA#UaN`7ySyOUc`CI{~!_!0|8-=>`^zwy+}RcT^!L z2XL+FGuqgix=tfHqg`V?SGg|uYw}IkQz@m9>)s%n0 z49l6*))@eWsxINH!uXB4Z{xPF*YuFgx!iFyC)FUI1)OIh^~#D_mAMgv2BTecG{cTG zZTM3jKr~#3RD#XXe>=gDQ$y!N!b{pwstY~zxd$MnPDx1cO8T@br#WhN;pv-mRt?RY zP3)xUB(g7s?Enxi;6DV?e5mefLS%M8-)joNcVceQ>IK{QHxA=0B+0}cUJXg&;2oNa zGFQ&V{Z2rRgk@^4Z3WeiZ)2@?fj*kCG4w;F<-ej6H14PX?YX|m!SQVhUl8%}2MZrL znZb4q2eIowanCvCI0llH709+6sdA^b#l0@^-uP>=)50LNJdWl*>T_rHg>MW=haY43 z{kGoiOFA)=HUBqxHcqoJ1nQVL4X7LXPW@pUF#k#hzi!Rxk$*DPyf7@9zGTrg#jrG5 z!OR-}eW}r5q2g1rV}Zh0qxp27x6}h!i7HV0_Cx|)Tb_68{N%qtkG6sN2no4Jxr3f0 z9D+N~3%6sO->Jy?u2}C|z1TmQW(%1(LJ-hJ##{-+e{08(?p}eCpf-<90XIG*UsZ4X zgGsXc$*@rl@I)EvJXtR*y|KlD4TtiAj9qiC@olY))AK{}ue@ zHB7mS&(|4e6o}^|W1dyQLn;54-US>y0k#h4^SeTi<5sFZ^1-HyU)k*!+yRo`4)Aro<(%FpuuT&AiXv#*_Uc8k$-NIrg$0X>cX75niKA!e{?Usl0 zYEZv~Yx)hV3)pB%-Z&BJQ8g$2r$pQZ1ur&UiaJ$x>ttpY_h9*((%e4;#*Dpc^gzqd zKXH?N64aDo%Cp5^!dx*YUk&J_tIbA1ey-1g+$*clft!9$UX;!~Iy7Ql9w(PlT@HY~ zJOJ!evLAE@1v*t}6~FtiKUW-gG+pjT!-(<$HySYTs_Lvp(m&_$`b>74+Y_L z(0J3JfnyTbv7TmMn2C;~mdFUCq76y9=PO=qT2t(k4uLBgoK3a)Qo zCb9c_cGhb^aNT@wfV@{7j`$DK%Voi6g;;7%?wPGW(URwKPMZ4ijQwh{)MuA))7s}a zpi(b#67fI`{V1_&V}h|tWV;I%3*^R`c}>%bSRBqKW0=s@Eo$HxY(4kLWt9itJ!Oq6 zIR-}N1N0R)r=PJC-Q5_zvXF!f@H7m3{hLNgDJJx$^pg9993WFh4Q1vPLub zvoLbsu%!U@^AHg2gbg@kz1mC5`!pt`ee5WqEn-@!drN@v_Ayh*v77u~b&hf{9>Dm+-8#E8-@BEuY8L7G` zkxkRpeyfQ8(rw(6j3re4V}i%H15!<+ULNvOtf{OQyZTx6-hZ60Yn3`=cEML8lUlP+ z+VnUDq31ha-g7jpjI@p|Crp+0@n_U7`4zX=f0SCJ_*8>Z4yC!@FwJ6bXY7U)^3J*g zmHpj&ef}+W^GTvaT$%&MBJR{=n?jhn&sszOBsB2N?1RkTKX{(t-6C2@-9UMdMyyGO zc^G!uS7`v>>KKY{(A`1vtlpg_Z8@10i}u*Qum{hthDqn7gKSGidDX%s=@j+=UpXo$ zc^HPUPA^1u^AxOFfd}$SSbzg|<~}x7q0C)CxH04$1&vW1_JDy9*sQE=^swi;I+JoJ zN&^Kk#&ojKwuUd%^%kDkB{!14H)c?qKOLxLFfzVmp=ushSJpnS%1V3U#()BCS8mO{ z6*P7cd-ugU#0s4xNH=EeG1ZVd-h-AgR0-UizI4e45xp0gZUCY{r>cu!))m+?Npl|s z3U1v_&8Bei8Mpx_C&+F_uz>8nED>VP7~| z5%I4dS1D*H0GCitoXsmNxJF=f%I|-nU?NRTq}b19LG8`EAtD`v|JH<5Cwl{f36RpZU7$Y&{=+RmozQ+jQ1)%n`aEnN>%YB2Z;BlH-{%S> zz(?=)JuiMC->Bw7cXe&OY5PS@1BUT+Ud{06qY!a|J-pVG3=g<6Yg+|qZg5Qjm_`{L zy*dDoj<;Kb4+4O9-HL~MYytFdtGtP=feb8a5rRW}#Xkv*#l+`zj7X^uEj5oBmpu4@ z#Sc9tz8|YQS@0JB;~8ocfY=>>@!atss~`b0Ga;7Ta{4{%`Rla+7$aGZ>xU9aY3!na zL?%{XzCG45oZ0Y49uQhj1wAX)9b+bOxLy4sZcJ2$VunoMZFr#5j-DgsdQ+_~t3kG_ z$e$fCaV+#0OTM(tGnikzDDpzlzU7eS0=@}qSCo?fTP;xKm^fHqw62=6eyzK=IQ3`u zb%--tTk(B;Be@2~8_B9iO5|yR=g}TpNX?b}Rbq-QGoO&RsHK?*S2qYiOWd$QP@=4M zt#397)qzRqHq6j9JD-{J&Gw06KZ=*AZ6|(866><51ONKCx3hOuFH%U`5(GwO+R3du zQ1u&B;3?>Q0oMd5`%p7dZMFGq^1z@E8J?AI_9Lu?ovZgWm-xx8Y+O!+J`0p7D`oXW z$+9dL=eEG&6zjtraPV!Bpp;j^R2d2U(I4YM>02y){Pa)FY-L1bUc(l8YF9oz2>h&m z842LjMxSS$z3F5Dejo1cKehF&aafZn4%B($S>FcZZ`UDcrn6b6eFXcBrNr3z$A|KB z$?4t3F0{mjE;oc%lN-e=VMOQLP&?MAY$V`W&F6Ce0c^Dci3>VH3S#5H7>566g4i3P z*z1KyQLQJdYp4VGq=7^>ydKsvxe%Hj97zGB?Jo06o$gUZXY>XQkVE!FV?G$2VlgDp zk^kcM%4!WA`JtjeH;QT*h6&Cyj1qR92JXmE>@Hw~mTe+=YdwM&RZ-Xf7b}tYan34H z&~TF0B-{RzOxG-11(B`cdT7@0>>3Af$JlOAoxN@Gm~ai}O&4dk6en>% z18vpE!H$|xcz+@dX2kLSPI7D%2(@d_zGSUiFluev3#$pR;vS4^pEWSVN@pzzWpZP9 zop19Ng}IAdIauf=Q9VEWG<>%p=BtjV2Z9v zr*nxltTP3d2-fJaEdASq4_Y{4lCAJ`(TQD0qG~+{a64v-j5^}rbybr!l=hy5;IM~T zAkQQoND$kG(hDgkz(|23PW_)ElhyZQXo%Bh@PFtzbmGVG4yR;7(IPKPs*jeqZ^vYP z8^zj)Tw2s3s3ACt%o8j1wmF$uVT+yfDPsM0_P5^g5r|smgR1nh>gAik|7>3+c=Kx% z16|;N)H5(hQ|CZj_M#)omzly%($#E<6UEE&d6_!`D?YzDSHtbPSDwab8%&N~LB#E)n3NhOdAM2k8&Mae$k3JiW}j~X zmOIh56Zeja1og~xWydTr*$cokcxu7g53c1WEf7~)VVLIUzBUfb73-D!hhJw$_to>4 zB%8_N3eB363BZ#YJMMWa&h!5|sM?YQF3P|%8Oea2%^GjZ^J?XJJeqArDn6|D(Y+B$ zCOCzQ$I5+$he1GY@&5qc03rXqI8UK<~BHC!P?*h)^9{=IIMKFsH(#y=~vKfR1jkfE-4Yb1G4mJTIjG4`<%J7a4JNo zyQFV7kP8$_!DDb9l8zePgcVPi9ZmmaRLkg=e2t*ps?IN00000NkvXX Hu0mjf>Euz} literal 0 HcmV?d00001 diff --git a/Assets/noise64.png b/Assets/noise64.png new file mode 100644 index 0000000000000000000000000000000000000000..dc6e898bd1cf62876998d4439e07cc246d5489fa GIT binary patch literal 5766 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 literal 0 HcmV?d00001 diff --git a/Assets/noise8.png b/Assets/noise8.png new file mode 100644 index 0000000000000000000000000000000000000000..aeebd839ce5a0b04f4fbffb4a64e173d1f9cb0b1 GIT binary patch literal 1167 zcmaJ=OK8+k6pdD~R1~dlRD_Va(BkAX&O|46ES<@W9c}739k3Kp)8us;YCa~fowVIZ z{o`*Th#znvxDc@@=tAm;3NBoT3L+?eCL{vWk&$o3txA)m*(R$77}hr(!ltOYdf!pCE`;fJR4}YK^M2?CLmPUW*$`ED#Yv)0 zqp{CLEq%>-t*=Xq=}i49y3Ut~fQhh5`{q{5ll(X{rYn&+NOKH52Eko%=C`R+ofuh=OcjGuioc>;d z=}gF4-uO0=4dZ;(;RKcsmQ(^N%KxFJIf3?Y4*l`_Phl_L>mV+NJiFV~$ia2g1f?7) z=^_=|Zr--HmUc1QX=B^#v>iIxBGQW$RWqz0v1E*)C{o7quxe>2BgYw{!5W4x$q{~e zLozLka##?Al$eO}=~y}$7GmLaf)`>Xu54@FCbDpetN-Dq%5sBdFdb4^My}C=^t5Z6 z^w^>$qr8_yS-o+tUfxTxESDo@xZr62I_gr2^iL2@G%lG;v_7&(zq_Qd=lp0qL4Jxe z*`|C7YGDvy)l*ms_h8zo*8nd8pbfxNM~@cZb^iu3-Sio%A#|EVy#QA~oZEQ|hH`gn z;pFhaHIwj0fZMRE3T6XTZ_dD@i}L|466_Lyn>`T{e7f*q#0OZ|RG2jLJisfczRYJU z$cJ{=2X6sB!gGLGZHdFv5B&gG`RXf%e%J-)pz=tHpa9Me-`P$KXESm`-hVu0c+cbn zMNPR^BX{J#G}iVH0Yu)9_yY@73K<35cZK)@14|jZg puzuszP46D<-u`CY=NCWcZlRc|(=I)$8~8@FsZ6R_9!zW*_yt+Kw}k)z literal 0 HcmV?d00001 diff --git a/Assets/smaa_area.png b/Assets/smaa_area.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7a0fda4ceb3401d68cc8577f01538db67eff3e GIT binary patch literal 22194 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 literal 0 HcmV?d00001 diff --git a/Assets/water_base.png b/Assets/water_base.png new file mode 100644 index 0000000000000000000000000000000000000000..07a03ce3c7cf70284436a232e6e89a29a6c8058f GIT binary patch literal 420235 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 z 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

MP~QtV!CK$ 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

aJZk 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 qOR`@%EbK z&BcB{D*odi6@U5P1No1EYcSF@8u0Z|alI-ZzORXS1jfue>U2~d{t+ac)dg7@!4=CA zJobl8utbqvBY2lhYX>kl=R*;IsADbM1mH}; zDpr7#s9va2|5y2C1K8l0MYLYm1PU%PX3mJ zFUh<#9Bmrjz5?_2gnb1r0Kb|es-qQ-P1U5_7sCv8HH+i$*?|{;S3WV)J&E{jwz%ka z+d%rjJ@T6k3er$@ZOi-axVqp-m6viG^g*|aJL(K(7?{!?TTL7&cT(frfZ75jLVQ~3 zCz77)I~+){uidF?Uf5&^s$?6fXm)XpQYG?p#PfdY;NCrNHbB}MxeGgN>g+EvK6>Q@ zs|teRvYjO!f}y`B?BWDu2my|b#SsOIBBd=0O8J&w+l*kFBaufUXP~Fig|jfoK|5wU z2pbE@)5F#!p?6H<;*P`^ghIW*iC&gR^V25JD59*X&2fp4nb^lwU<^a=Nj3Nej2FG{ z;XFX+G+f>83Bq4~JO27JF+a6)En1C$YtlMs%fmq-ZSPD&WDK?mO}x%Z6aHMe_rVE5 zegmj2f$)UVOJ!+}34BbaZBz+`;ZISq#Fvp2rPa%@naQp3{GJ)#+*Fo$gVRgiSxmHQ z%Gd@AgK5YkFlS;v5-(+`cr)yUE8@Jg1HU-^wQsN|X!^V+AmBzC3B$D)hD;f74OK<+8& z7DQZo28Po3mbSz69i}JFP8%>hQ8yJk$HJnMkQu}AJ{(&Ql;4Ve4r83}C-5T@uQ~;5 zOFGCT>`QR6x^VZ?*u44Bq;UvEKjp|v6?VbG#{g6mR(`Hg%Dt-i7C*hF+NM2SSIQT3 zkWY=*b3W=~UlqAOU(&LdOa)V&j|cX!(I!?e=i;q zH4=76FCdDQ``Z26zo>OX4>f&E3E!4AC~;b#syklRAJ<;7^@8mv_>W(}`jR&6^e(H zZ!qyVBCabD>F7D(QTXzFuyZKw@dx`Q-~V_5dwSzx7O!MipdDb!surQn@iT9sLMCwj zmKVAh3Idy`Un##fHjUqs*i9J!d|UR%LV{Q6iH{@k@q3_u1!hnQ9tO;qM2MO&z5%Ho zR^gQ*QV-k(Q5A^l(}5qh*yqlD@&x9W)=nkw=v-eyK7o7YPLR+VCoWs2uK+4NXWIl` zgKHa~kAEI2aQo&cg(NV@LO3s{IJR>2ateZFVX&HaPWjmwEsK`u#sYfxh5uy1u|>iq ziZoA@*IIYkoi*Q+&rfTKRIFfIOHXUz*+9(Z7iH|DtjDCnJWdz?#LPU zMF!U%b}FT^Q%_SGCm^GUDFxtk!_R53Apnp8x%2aAgGE3u+@-s8E3-TSV~n1as(s`g zv)ToEBd+NzYoWsuffZ-JKyh`xHb}W_u+`s zDUO%&-zCyJERkE{BMey$pFKD+7(n}N`0<%2M^KYi5@X}UAmFII;KK%7pfJxTRTqO( zm0pw(N@0Uvcu=62 zJo*|%-=d~(?0zhP)Pik%+Zkyu2kqQ_GH&=U0c?e#4*lYn@@pDechqiZN1|@DfnevQ z0v&Q=DZBA_xmVR3stj{YD)i66fHKZIi}r`zVVC3WaQv6wj{iD3_sEUZshN%u+*ijx zC)K+Rc-w%}9Ot*-K2u^>#G?kTMmt;yAmvpo8pW-(@09bw^%rG)CA-h4Pe}k6QB?U@oa``*Z;myw6*r%p#;h<>(fvLAK$q6MM%>rKus&$`Sr;c{%xu& z(3tLyMH9mJf?=!kR`IthF^A!RBdVfPot>#Gdel1q{Ht&uUxgd>3AB@nB27HWw#86yW^XdINfs6q^S z9;k+MOYDt+s5}%#wjfoS{JrGNI9{|FW@tAHO=DyWY?tHXpB3-_FtkpM$+oL|ofHa} zL9g{0T7PcbXQ!{f6Zrt9e1%?xPKO#QLA}zrR?MYg&}Tm7%5md*btt ztCbgVM_A8CXBF|}v&=%0XX|8|*j&P9ir|L!&rSS@=8pHCc;5r_G{#(CiB0K=qdH;) zyzsx9i*3IOGI0)Qn>BpBIoht6>&aA7C<~!zl5iMsmBjfXO95v5JDV~3dQx=h8yOp? zCtk_lb7JLKJPN(yj%f-~GTiwpUJItXl3IqF<*(u zP(FuiubbGcpk0nXqu_lgO1LoMd1(DPCvd?rFGoe7WpItC9{#5gkh@;;;orpkp)sLWckwO} zMUg#0wh0Tr0sUc#_p9LF!}0P2J`4^=I~*|+Uy%}_I9?_3(I!5mHTY~!Ci7B4r``L~ z1q|{#R48#lYS@}ciyP-UJ5Y~;?JTIDhBy*`bi=z3*`(bl_j%P8VGMfaO z8AxM=-wVSwD7F-h&4JkX%*W(sKNptPfKxVDpX@5EJY)AU>6tGo5=u90gQIenFEg6#Cy zCSU#31V|CKZd{Y(Mn1zasI`a$KIaqpx?PTsUlsrM$-~#CRN9t!X|Fare{Ro5KeYf* zLv~Cn$}WL|XV%3(w3>2(m>Sm_&`3MW6Gyyu&9C8jTdW0LOjl^NdeKICL}2WioGPGZ zt;JSEH~N~6@ch}GQ!~8ycj=9juz^7Beoyf~1_4_oiFUTtm`wTfgQC)29kcq)I&VelV@HaVsGLf|5wS4o;mYHv>l3Yj$2BzZ91Zz`YW1x_NS zV(Tpy9F1viU;n)Dw@-tAHnjiR@b>3~?}2kLAK^6@*91sWH(kXWPJ;!5n`YyA(%`6v z<8?Ux(g}i!!os0+?&_!O``TG(Z!bRUmI7l9+NnL{=^uRGJ9vM`3?_(vr$&P7k78_4 zyc%k}{C*pKQtj`_9i@+ir>mQ$N4M|XVdvddYs8>&2S-yW3%|zig%9CedD`U}S*@Ur z)fu*4@S{6kot07)gH=QMgL~1?=%-Ka`(K5ZKL7UumiV(TyHmPq-W^9(a8X(yn*@JI z_%Or)*x{&O{DNwy4eIqt+nre=H}3V~RDO8iCbbO!U&(^1Y!vZKOYLO>IRf3Zt4`Yg zplO2K9<&iQjsGS%Ru#YGC2+``gvXdB0dtMCilM%-QeQE$J`>gnB$=Hlj?k_=ekCFt z?FYMoA)!ENle$DA;uZO{Z8`>Ad6?pum9(;b9I?xX8*^gg|?c`494JxhLkPU4I2*sS$Wx zhJH${IS5hNgOiHIsrNJQ_r6J6aJk=Fm7(=khKOkbQAIHQuzs^g6$9Sf@Jn4wfpHg~ z6mmi+(~xiubGdrt@vp{$UY28srLa)9>)g4(_~Q?7iZdLz2AwK%s8EsO-<-umgCkDE z6#;a^o(&(L1s?~{Ce!6GZkz|I2U+Y+h!Vin;FrXXNUoH+?6B+c?1IU-aQ3}&`ZM>u z^*U3<$Q)cp_%K{0P!3WDTUTnJF0x2aNZt8!CK6Z1?Y8POSWpX~REJ@^>=xpA!T&GcS7T)zRN!n*wnnhfqI1Y`OH@svfPfsf4w~K80sRIQFq|y%TPAEW7mIV zz$=5Vpd#?xfOzqonKMx?(@y+Bze70hQh=-TENhd;Mu#GP3j@Pq2EL|YX50w+GXgm| zt{>;Xe_jdnhCgS;>)$6zeF%|Wxt_C)onadW|8#s^&lvs`h-;4m>%bpb@bTUHJ{Zij zCb#auJQJTN*ezgg(C z3z^tD@GejD5WBq~sj3e=%=6?)c0kd(J-E|f)No<;4ey;*Oq4~B z1AI6uV|_la!_hty{+TEj75SG$I~fG%Bn*9b68JXSMR5emKLSMt@VA*buEd$Z%QzaH zuL8UruNkOc^e%;HVYxd>wR=ZxBk(%8SGG}y&# z2YfotPhkJ;_&5{oVquQmu!O)NpFHj&+UK3M`^M^YEic-cd-(~FZbHel?_ zV=uv?cE14iFv&dtw(i)b65$HnPPH0J*SX_jnk?Jg5ZzQ4d`ei>21n;6ky$rBKlsLV zDeAKQlG5{cJ<$lUN-N3PU6HUNx@m9=tmzvy@=bUFJf<4sZe(23!uD zx`1CW9Maaj%qh|gPMS*sW#>g#T?lD~49#G(vMI9u*!10lQ;j(ir4M+Y=n9MTf3hL@ zK*3*r1n?G2(%rb9S>Z|_ojSR2HbEBx@yQhpq$~Mj820&80FVCe+1ZSXK(8|$drk_9 zgGKHY&cHai%5IIbtc|THplsMJyf8&5f5FfyPg+>?_LabX!=nM#T-SM$WzwKe+m0hfskKuqfx7gx^+<_Fso5}AzNYBM8;Kw^`+hf zJFjBwZ1PX)C*mZHEkvoC0nD$2!TE975(Q~QD4UoE#fxt*Cw#_}xZci}sZ1`+do}dU zU@ynJQ|R}aoLtyNVqY0_S7c}p+Z>nilG~D&zA#7(B&Gksw8ory7gknyC=7M6y(EH` zmK%HeqdQ(#-h0gs(#u*|`WJS#E?@PKUwQ#+GV+dK?L~sBLgz`VT&cK9v8{}VH#nIn6LbWaM zW_)fe4X-awP>#znl|ArsUNT=GMQn4i`c<)>AYZ+WM9&8cyk*eUP#hbkaMnDurP94g=@-M#8=kO@tTmKdn{=wCt>+t%L#jhwVY3YYRqs%-VLu$f~jg3DD+wDC>?wa^lcr`$WSva~q6*tO?Y7yu8L;a! z*?=0(N!6)tR!)!9Ik{}p*9<;q=D3Gd_MnH$q;S_xpbX~C&rRAKP1*9chDiq^A*eH# z7rl~bnBO#Hrb6+ObEY>=F}o$QaeCXfgdIv0?B588A}+SViR$9Axa{wKN|5J3yj-6r z*O3_o5d{{3!zZk8wb*|RoJj?0al@-l>|aGzJ0|wkSmS<9fopeue!5)ubbN@W`90ug z7A5^b4Qc^ScXV^S7@Wh^pE3v7*2i5%ueC~wbXYYVBZNt-S*tPrp}iKSY4o&Zz7l0`2c+C3vhX;UFzz+tOCdI2Y~v; zy}eIOA&Mc5Ui2d6XK0VNCeLa4z)L148RqQxdox@P?8XkGL-sA9L7`xWs?N1d9x`Kd zjNN&$ONTMsID`BGf;m%yQN$b?tSD*?#Xl{2Z-z$jzoC>=@Gi=o` ze;CdeyVEg)kP4$R9b(E;7)G4u=81T7;%bwY9tR76ufhwW{^n@ywE+hlzteAqb<;}% zH|(WgX5s)3!U67%*Mw&F#FdF{bCSO-Fx_r!c31I%1a@8c0bc4kOcX}W--i(y5CQ^) zJiSHYGYbh?Re9${t7Nml<&50j7c8VEkVUC~2`_vb%l!6TVo72|D6A{!X<11{COn0O zM#A@m31QnMe7mNhT#nongi`{$@n9aJ(hUijLizCJZS0Rky zE#BEcOnahuLpICS_}))#Y;pxq1on027Vx`h16sdow2 zreH7x_b`V&!SzfIS6)COYM^*g|x-}q~4{^r0{JwVs~$cjXdX#2d~M7H!F((H>FvI-Qi?iYfri( zSfu3ScUDfKxEK5^l~~9X3Yh^zP#wq_wGf_jX%p7V1iI4Bj3Er%n*qGMHvl#jg99v?%RI~*tjSYrv^3SZ5Q@pk6ZCy;86L?R< zFBSF0#TF1tVg^@Ng>tn+^rpldi&- z&Lpb&w{Cc!6FG23K&#J`D#Zx+Ol6`?Ovt zf@+$CEK&QsWYDdG8f`$S&V#&ggl1u=V)?I7K)e@Z%4Yfh=j!ZsBiE4_%qmH4Cv$?m z+4q0RGfCT)RJ9kR>|ifvfFMY>E!7YCQBI9IDQr88FKbN}3myEtn(DS$OkkhqkE%%P1G0nocoU6s)n155ZOCNC<4 z6onlHx9=Q+vJ^EE?MlISCP&SR6tw#wKABFK1o|`VNanXyK5Dg%t5-{BArsypSRO+t}KGJLMVpbXk)Nj zrvaQVg^G!a8fBr#IgvYshn)6A31YM_6s@UnGN*UG=`w9A#A)y@Y1<>d zEMeHrq;fKrNV>obsE4Ed1ZEG6IdOI1`pfU>brh5@7P=;d{a7@*masjsrJCvs*#0{H z>0HCwuecg3`Z<5J4$ta^Qz+jPbp-k-unTAr7{$?x?(sDll|XzNc?~Agqfd$kJ7`m{ zv*GsLHvRIm`JEGMxuRK@0-W7rXeU2|`xqtpDlHDM*Wz~bC8JG+nbom^Pn5s?ISc+9 z1^dh!C{zMyCojfJzsLPK3|tCcid95*+o_Vq01l19ioDEK3S4ahP}hD~Hm)H|o2B;1me{9rg0h4A zPy^y%r+pS`Jfdp->bz8&KHplomN1*_z_JFs-35UMNCgIT->|V^4x;t?M z$z!E3JD%<{UXa~+`41I8%;(l5MlgmBIt8ZSC8l zjLFgI2trzH0KO$fflX*(jmffTVzelsFudX9oA2bFht>!cff7zjp(VZscWUzz_kX<) zNykOV$>R33SaWFY0usU6Ccux1ocSg%;JPZxK2Ta>`qBDH`TJ0mF2hq? z2Z{Waa4=K|*ruhsllw7~(3fcZzGtE8|Ez&o176rImoSvmuw4#69G{Adjt*>>q5Y)} zr$jPvY?B>V&yugpcLc3~<;j^?G2cG80&`CsMbBm9?mwi$*H94wM$P(K553s+B540y zY28Ies?2pc8`rR!Vq(Z)1fyjVTzjM^i-5@~)iw=%9RU_o1`V zk|XUYM~b!&C_!=PPopknHJn4wE{l8?Ga;ysOTRH1@b90*z6Yur$fE?xmdG|yCS6D6 z6vyQ4m|eRW6|xN^dbR?*UPck@C00j$xG`-413GIXF$6)Y)A8Yk`6qG4Kre}5fsb^I zpNW6@pyKP#>agtAGX$Pcy5^jw27uS8cS#+8=Awi?O^$1g8$o8mHJ#6?2VG`mxl5Kv zS=egnT4vGTvO@OMjFn=jvVm9ENHOqKU|l8XyH3H^KaZfJu(3pKHMXqQMhIsImzHc`1<5$ezfOMvwTX~CL7I%^QG&R58K%pXk^N2cpVe(>8OWtt(&<1 znV6tDcUj!}OoTi3#!lKcA#%aztv40~wi%8ZNCUQXRIp?3J@M87=^A2;8M5cnQE1HKFxa{Teu<`BJFqZ4GiiuXNa6c+X~0=Ym+5$2JV5&T zdQMJ`U_^M*%}E!-=8W3Cf)~B310nE;oQ&BWMYO*6bWE$*Bk)-ghbL+cD&G|$YQkT8 z051ea<4$gkb4`=K3Qldl)9*>d68QBYg}YWGP47e3WwnAzDiXa?XO#63Z4QBNvOLci zRFJJK`{_*R97%pj1+yt;QFvJ;@BRjwDVP0f6RjEg#)Laez@{V9unou8>8RfhoQC~O z#09+0M0@j$Fs&a?u;D0$?2J@5S_Vyey(> z1Fn`&kk5)YPihnzfUEKuY>JHTlSiBTR0kJwswv9&wh0W*lWZ6}MD zmE`C|$+0-qN;NbI{2d;L&2Vf5$Ib%!rFG|39stsmu$=$qw80wiZb@=$O~h-$N+Jpm zoSNEkPwJ#TQ4o`BL36Hwd+VG;B^b#HVvQ@YY4YKKl^Xepx}!YJ3kd z5$3$?x4hXUMa`xcHd7=0hTwV6JE7El0p{o|`riQB`YOE*>*#a<0000(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRvikx4{BRCwA%{n?TvNs_LKeay0`s+oIa*3#$93~&Jg@PK$3JQ*%<3(#O@&P<=G z%7}0?Rh5Mq7c7z0L`Bt|9T{$JDk{wU^S{XdNWZP5iehv<7Lw zLxsO*=hKZkjV}`4H{t$f=x!7R>B2r?0B;exflWcwxw7*zXWoK;3AqU$O~?&?4(IKL zD2yH6`^0wRQ4&WL+H)e$!6ONemf2s4{!+foQ5T*SCUCi9fDI^JsFyGcjLos#kST0$ z<$wAC|I^h_oR=lOF6FwMHsD8se-=kK;hQK~g{)ALP{pYo^lM^Yle@!I_?94<^WRba z@l;;F0pA2RNEK=ZN$@z8|MH*k@jqYiqf^Z&>U_EKHbGTTfgdKE!}#ojBmyN-J~D03 zsKReHk>R{h{!pbSApyA?48y0>r}H+a%}K);U1qG-)XR&rJzd*-<(sseB0 zZpH+TEF=}0JFg`8E~H(l4`Iu26lXUl-Ed#<5@CJ_slsNCoRAF2@Xt;7Pca70?^$_l zpvhqfKPJ4kNh$ESmFK^d&$p9@lcpon*>Rp_GK=!R)x!U8hgUU*JBm=dP@CXC9XTCS zNLe_tU`0q7YJtsQIQ#^X!sdX10h*y4ju)^DW5U_sc?HJifPvu)t@&-wTkKNhLNW*#E3P8%xcHvBMU(VtDCcsa6gTS>3V{?i) zatiM+WxJifr^(|nX`3)y=q_AC@kG<{^Oo}UK0bZBQbw-zLi17 z`9DVGKUK){x52C2Sec|QY-eHbnKA_`$Tq-*j+0GD9+x^pXm5coBy$GPO{o)f8pS3m zf-2SP3O(5FChbSU!+v=?N6G#E`e6+X8_+1KID9X%ZV5Zn~UF@cFA zOxzXBIWA#q!uY4d*TkkVc1XVo^8sfPUf-4dRQBXl9PPqD;s8vzyU^5`8FmBS&@V?D zBnQ{{j~e)dvro=C$S(LP*mvdpUyYeF`?FAg13irLw?XPKhR_mZh2v7*--UETIRyW! zGm2qNkhdal;o(Br4gU?;orLr8a`Hcw@86x_FfsH7y^QUejBYd$N>y|ds>1k8oT_*h zk{F(amX$Y?+YEg<)*&4NAMkN2zum&$XXp7h=lrbr?O=|jjBSaOqY%{#xQx<_W-tUC z7}MDS1gHw4xX5~mooyK0lQtYtfh@2UP!KaxcjOv)8(iL*Q%T}zE(pSXPfBx+sz?>? zJ!2AQQ!-c*x;SGx+hxcQ>=}Qyesi`mnMoK)IDt=x#|3soAu*3LsSR|%UBCx?+k~>7 z|9wHV#P3ZwrUM`ua)l|%+??xx_6=sTIOlKJ--`c${N*Ht`4r^8N)+@F1ssp}+yULd zYMl8r;>_Uj8En9-3%Rh~8Ry}`^LDmAsX3aD+cWQ>*gu4=C^g5=t`p7#Uz;)?iRN+6 z_AWdOn#a$2CdVJ}wF!Pfx{ckPcY?EmJcMHxibMYC^wap`Wyn;vO~^SKo9u!PVP@!0 z!9R_j;U&gUXCF$r6gd?oj>Q06;C>tPQpz6*FEEp%Gm%C-FUy7?NQdWuX~uW!P~tuv zAE0iiD|AqG_P-haH|4%F{BZos;E#!qIM0_t`liV5g8UuE6MBm0nZA_cEu0-%g7+=x z01{jk&P&KysVTZ8bz+zsf?h)DAOiI-Fj6#5wF|_@!`JUGTYSHpC#J%e8TCw*?=T%a z!LL`y9EK{rD(Ns(95F{OA@^wH(hb9uRv;J0vXGmze>vl8@HOC76fch4LNnMhc!pL5 z8A9%cPUkTV6}XeYP^5xyf{RerSsD*vY=TbV959>kaT!@0aiJHua7aSh9Ck;Gp$0M` zO-F~2reX~^9lwC-SOOk`KVWWfZiZDyG#cmkaQZat6w(l?D(cQqM~aiXQobwxQ!wCD zg}W(dgZDk~FOVOZM^R)KS^*09$dqb4@_@e>KSqXAC>K;5o^U^uA0LkG&Nm$>1}aQX zWc%9%beK?kr6Ob?6=#}b>Le3tSF|hAgfw7l;J4r%7;(R*0SU+5=~eJ5BpfwoAe==x zi_nrI2z`U1lVfYBI716}5?t4%a|t8GXVK#H%@Kz(1RsuW@I=4`xq;!dBs7JyI3qch zoIW7CkVPm7x*A=bYLGNhhhSISb)H~saBhNDM-|)zTLr~-1L6=dovDIlC%f?pve1jOWza3YzjO=QgghWSG@M?59kha|(8U-S<_@<~ zrl1ar;3r%!=jR`Z?VIAi8}=BKhofCdUD&7KJG`pXJ(?`u%GTq#y2k{5RpGTMAL1Oh zK{sT}wBHlbfMz87g}gXlxA8LPzJ+`#9Lh+Ukz?X-DohpHlXyY8;xA`>P5KS33BP;h z?`=}sL=)r^Mck)g9$mH$*ym)wL7Ov*(2}wxNX@thOn0(4X3!)MDg~;;p21BS#B^`A z&b&iG$O4)ZBEmhvd@`?w*%5Z1ghO3fP@x$m3 z!!ziFtt-13wuOdi|-}Q$k`W0vE z*%beR{1fbSrowx3?!)=5Dtj0DT*w-5OT4RlUsD}IPElMaG5HH13k4V_ z&dI3CcqBZ>`Q+{R?c7NiAId!9Qoo8bESfW&pw%HeTNQQ{iaFlk(V%re1$_!5!;P~W z_)n*L6uGepO(2`oXB3TDoPr=4ui@7vB$Tm%R|kY`#5<4%Op7y<5|kpOCd}!))kq3S zgbkoLQ{tYfZ$U1&rZe5CuC#9a^u$Ai+F^Ugy*W0;4q$^r9H#JYikMN7G9;R!*WZ+H ze>3vt+&KzuN{*I^e-m1hcm@lRafmamPOl&}iWyVLR|1E;Ia=Wo=<2I@3pM7e_|DR= z0m^ZWf`&PdFXh`0r@x#|eAluIb&q$TC%9E9QxTIu6{fRIs8^s&S99(xSdDw_Db7)h z^HWJpFav8&Jsr=&1+WR}c4~7{H)f6jQ`{MMe1FoSi6sRpfkopc1vu6aHRE1)g;9i2 zoR%GmBh`^&9Mwt5qtSjT`BHL^S|ABZjpuNy1mYzNZK;N4$OH-?#YyIfMe{t9QBbDG zee>6m_?*JsoY`XVOBr-me9RaqPG`K0*Bg#qseAm6JtfXv_}C_Uj=QyNFb}wQICtkN zffn{0&8F2sl1ZLN!!evPq9Dx)c{p>#G-^QK1la^H z@Zs?fucVx++(g&a#CgxWpr4L>8h^Mjk$^*92_JC)XAa2DiC-cS@G%n}8iODaE5}??gA&5KdRNX*`DGCiJXaS?Cf?e4bz;bQMI6 zeJm(98L~tBgx96Kvd|{vF5D&H$uw7^>Fq?KCZ<1PUJ86aOV@@mW(z-n$f%QN(-jT(O5Qxw+e4JGJy=-6;y;ijs7-L zh6j^Wz)a8`iaWjkf`J{uQFn?2ME+fcmV~D|HA5pHHgf@wqAucO2etT~DMHGD+{kqB zXr@GkBtohQpMh@Coq;iYG8q)ls0ve{uZvy>2m%6KqbMbbcYzcW;nf}cDWsWUb4CV9 z4hp7@R>x9IqmrFtf_y0FJDSHk$HZ`SA-|R9ccx z-0qAmCZ2aTraQcqvQ~H`Ay0UV$InY~+4!2mJpsRd266FKfqcV$JI6KI`^144gj5W2+rX6(^?BThY`T#gOL_PB$9 zX#5oJk;qJtk#0Qg(_9 z+BBL!4fIq-+%p|;ancmZ6t>kIum+m0aVHj!b=bC^@eiD?kg_vze8!zyy7Jdc__&Qf zdOXk39leBJVtqF&j2w-+r?_hh&UQOGoS1_gdm zLb~8<3LGi!T1$bPOo1yo7cp!WpolP2ps8ddxso~<@qUXM<<5K)_$@hKRrqqaE!J;2 zDaDj=PqaCLaate^pCeJba}D^%hr(}=j;Kv+&bdRX&dknqWhUoc!Hy5ie$pP=ees<)e;QgY1+9PdBCt*QMMuE~`%Luu_Z9C3h5G z%|gwB8fc5@cN+n%z064(%d2|`$rbX9uC4@(){3*Kv87l$%n9YKJWk+>i7z^85WrJ+7|F1(So=6F-JGTXL zkPks0f=t00ynal0!M-Q*rpV^Ja&)_@0oPR=Nzj{+FG24pHpAf*#gZUT=lvAU=b+&H z_AcDtqd@Lr)J=Fi687cDH5tP>vp{!>84k20e3{X2BP9U@d%#%V2{WL!?J_o*w5Cjt zCP8=DsxZ&sNAx7r>;b8tAk7e*S%g%D+I?|A1f7s?L)(mVnFOzjWTj4LxRIKpW?=Rw zG9wb}1&%_ilaU<7@oSJ?&PYltf)|acLY%fKErU$Qe@>b>X%}*hwNY>I8mp5mkO;0v zdnE2pXI5k6czJU&j)$N%!H-0G1uC{(u%Awy(FCyxGYj{jNQ%kOQ@k>1L7ml|Ltq{Y z-5CzIlbhqj{#z4)NG0JehW~1Ea}4~_Vb18POKf8PyeN;CvELTWh``LswIyy%6p5+a zAK(>!Cgq=-&^DuNu^KdqUA{>mi>U`^=;J-m4we$~DQn5RRyYl+2TE28w4$5|S_NPC z6BcVA?{O*bJKm-9Fw&GyG_PBzFS{ESOFWsi?}=JA(R|Y{xt%{)gpl=xAiV>pbue| z#1LWEK$pvaHihmW9;h=p8bE0<8@LLkMibalAc)rDdx)+y&eR~zd`U4IWm{+Ah#I0D zZV!aRt7Geo*>tSM?>0qfb#fQzd#vK}9?#PVBzh}wJd9GE$A>a+8bFXaS^P-F-z=L=7e)fbjk+BJSG)MTqTNj8gPkliy{KM zX1o|K1I?4CD}D@GPSh!+rqBhNU_-doxpv2jaU|G?y}Ul7K{$t^7o zFmg>)0$C_;;XWtVX}mv#LVS+a;W!if+Q*5C+>1099G-$?bN9s{%kbWvpU=VgHmL`cJ5x`m4x?c7sffc= z#cRO72f}?rS0!sSM7t|4MmM4F$|lZ70(m=g2(`~ZBih70ibPk%Gvulq(Gf6*;UkJ3 zB3KeeQbsjq21Tj2LoKmv6veW{glsV>unVO`^LM*22aGEw zCe%qy){b4M4dzdfA4WZm^lN3bDOQxR3GW&xgOr$Ryg13AT+q*$TI3fm0H{;g8t4UX zMhi3_Q{k#{>H2_n#x(6ECVp7ZConpU+u45_&vQUu>#!ZJoVYY&1LT679ColLSP5cl z&xZUWj?HxoccBbnJDvS9^6D_=v8Kxer91N!atH5g;@#qT%3ArH?rd2&fYjsM<*95A zy)2|KIo*v%Qc8_?P!?i1t7A3RE!Ec^IV;S8VQg`RN3)VCy`V4XeYz#3}ZI@tg;U3O)3P+9S`q~_-u@xgp3=$(xL>-w* z1(FA9W$(oE6drFFEhcdZK#Xz;-W=(xPtrhdNzts|7L)of_`Zrzi&Na$reHl*DW-6l zGiM+)!=k|+U8zjcG*pyP6&7I^k7A!rqpxA(^8v+;loKvN4lEs7bA~&wLrK3U`~f;0 zxjeWF3N8}Jn@01h3iY;yxWQdxpo zMTB!2@2ya`sBfoD{?(nb)-zqAaV#dxTbZ^<n=3C)BH<&_h+ z6-t-b2@%kGQb$dwGg$h)I`%MnG0v^y+vLbW^22{Y=}NsbLw>;}klKRv)EL1|<9!&V z!0y7nrfykxPLt^3bghkknv$}!)rqR$L-84ejcpp+Fn05}oJ_eqh;3~L-<|E4SQo|} z%v$SkcCb?@D35HUMRJrHtm!$SkLZwx1&em5gU8NIC+r&Vvbm6Y>><;F?ylyH?o3q( zWSK*io{bZuP7Pw&C737>%|O0EuGmdUic3HyaK~Qn$4DfN-F426N}w-$F?c#!AfL{M zP4c096{VwyIz@yugA36Gda{7wGF)|X>BL`7`O0iP@oWLjJA!|UiB>YC;XJR2PNfgU zdO+Q_3)`pQp9ybq#!5F1pRpJ8P!S{~2!qqPlX7OE7L5)+CnQC2X?@}odKT^~v>K|8 z)SYq~xjQb-xhwM{;Z;c*zoQz=9_LM}Vjqf?AoF0(ZbCKZ$Gw>SS@AuHe*G5i0oy-g zlC{&39{c>-g8epUT*fAG@%m+4yV5)b+Tg1YYBa~Y#cCvJz}(Y<7^?{$o8Udsr!j}4 zS=m&$ORV*TC|VcM*!c!lCE!H@IjEf|()Xm`^c)3nxX>+edHngQfdWbm0+JAn>VC!d zmNg*hF#^@vC*)4>Zc6-K$(-?yy=yI?4<|jNFm(|+ki@CCaNa@Cldc@Ql1i*#WG-YO zK&Rk&bdVv4EHY(~!|6TlkF-<{T7sFK&J@h}HF(UrRwZ5N9Zqp<-FIq>BP8%Pq2KY& z9YxTsvF($dz`Af&7)L0Sbiy1;$<92YF`6b^+40@DjtO=0Fr_r*<#4NWB&Q@pT*>Ne zC~_;uJ9sj4kF`y2i{j#$?n#7bzN zK`5NMhOWGxZI9orOvh^su&qd}7lD1jx-K>F9^}N~(b3P}1o?~c{5Fuo$p7*Gu5ABp@H_`vm9!~a zbGiis_rke`VKa)=jdKp+Awp3{Y;{r#@;ZvsXTaB%gt4z~D+M73Vvl9OdkE=D>@Gn+ zFOJtJV$ZJhuIM6V+?&y=aplQK78jhYh1w#bWC@nBTKxa+wU+SlYe^OpYfHu<%I3l$ zPP2fYJUOE}Qv>Rqz!br+6c<2p@{B26=cG<3!`N)bo_6gB-$^(n*tL5Jn03sM>s{(x z)3IB)L@0Y4RtPdkNdfy$7pw_)7id9v3H_ImMA>@4ZPNl;JT1C>NpabX5zKB^XJ(0g zdsVs{cQ>|~km2ZbW`X_?o{ORS?s0j`jE-7+OgDdYA)iXY(amXF?Bj~WVavl&ank1W zeH9pY@{C<@>?(zkV@jDd2uWffUs1@WIGZcqZo>`klDHqr*oBmX(eEw{i4Z_q7v_(#x&J zKeJJi^9T+VzGQ^Sgua^AvIiN{no-_Cjtpcih={jLbXT+}IY*HY4;a(dZdR(WA?P&D z9=sG$2ZN@n@(}EzqDpxhgLa)M1(G=Ga&~wiV zHR#~4;PIHk%)*vD2u`1hh>$GSJQu>9qtH!r&PBA-wIiLy$wYy-MOMq}mEI=QbG;Wy z7Ci0b?#Kv5MfW(jcEq%z=GYMG0NI>v730yJYXrHm5^I*WSf4`hapWCnm(O5)i^9mU z-q6*!u%Pd!E4?~b4b*}I?0^r1-Na^y=Szia z{r=hFQfK{qS;`0&f1xL5bRoAwxq_>4rPu*Ka>%o7-8uWI(PR>ekzRw)S(aqbpD_2> zx#)zez}IH%IY^K164JMxoJ~S%R?OLw@yNn=NnADLy^rp6Gvsz!cU~%dY)aV@{h^Gy z#7MqQkRX380?%aE*hCyQa5 zF!xZa81YW4#=Vt^x>ahR27Lxc3LTa>`(M%XkxFz(KO#l%jS^W15#$}suzm)AuX;m2?1|C}O{RST)=`hxT zRO8xuFkO2>uBe*gAlX2jUE;y@8jPJwWB-*sF4{RlI+AaViMz%{A->2Z790l_!*{*~ z`7clV@!m$C(5p{ zWm@S>b3QKEz6^U!_S+DPW=e`OcjYd@7_W;6g~qDIw|H)KIH`qfF1yi)374#-D)d9? zB`#kRIH!}pKz}IPJyCb2MA6G0%{j{pKM>53T>r!M~r9YNSi(-|5Z zp9-WBL_=yo`&oqPYrOu&9l2vrDC&4}?h>r(QV3aHi@Ra#u#I5pLy+PMN=XBKIMp1x zg!d56SD=VFM`2c?I9Q2ysv7ty$QiO(A&TFsgo*#62YRrH3hdhu4hvCeOIL&P4*=2pQh?c1DBm4a_tD!^7DW{!rmwJN2xN{wfrg zt|mO1k}Er{1-f~h!HPd0&fJai&ZJB68TVHv3U$iHy@#wg_aIf~87SxY0NaA2As$>B zZ^2{m2lzfQi?gzw!ZSPi1pg*z5?IXj0@JmKw2wO4o4sVpp zV(pO?uA+<&=lEunYOzY`8tMoSL|BNudtE4GPw_oW*5`MpWP=^f5_h(%As%Pir(j>9 zCiCpD8}cwjqfqONVPSMZ7W}W-D2H)r3>#{(YM2t|WUQLuY0}fc)DSmvgV{A!2|(`3 z{?0tl%-)u2!vbL&a}A;eXc1mI@r`GkOE9;I7N<-hoeHl&M_!BBpFJ8K8S(E83HC54y34H# zO8A+&XB3h zBD4*9adwYM!28-8o)RV=-ALVdr_8G+N_Em_qgvy?Orzz(DZ(F3_?et@7+zx<$|Kex z=Tz=G(OdYB6OMB*7Mu5;L~&KHO&CRy5>v}m*t@a!Ku)>^n!#`|!#vap_a@vqcDZTo ztaBB%r2Lr9E5&a7u{67+`2B}EWhmR1aLoxgc@ykVe0Ho&@?|gvEyrcjKb7$bS(N<4 z`Su0nYtqhvdhDWmQgEj&6+%|{I)w5RK9l@fds8&dV4gRmmwP>$KqWI-EJ8h!1s!ggh}5s|i;}a>(rR<$oe*wF+rQgJ>gA zh$2C3ZAqw4s1F06obi2+TNo31TSPd7w;0U>)-DSXaEB^RUJ3~HxTqcIMY(sOGSTe( zyff3HX}@r)3cl16vqXcjuXRFh!h6Kd_*|OzGL&R++(PPc9{XBLCDuToG4=hr`_u(1 zF&Mc-6WgY9+=YF}LN=i`VK%3$(|02g!^Rf%D9B=$^Dp4HnRq;Bkl+^Ktk>X1NOGPI zcIlt;+g_b^3Excl_J$L94@j@r>!+7YiY~R8EC45cceGA8Bu?p5D zp9CIxFiYV*7BaZSpcexI?0d)=_pY>eqQ2u<$|Z=c1|`SNT^83v?pW3IUBd+J;pk?8 zpdoCx2B)dVecE!!N_C$h3%)fZgBH!!3{GrF15*_Gtbrtr6b*yOA}c3nVlnZHlL#~| znDB!B%dDLwWth zxgL}2*=RZ#?tro-XaDHr7AMIp;BH?=7Ab|dK+B<%Ob$8{n+wkkj=vcB3$7v5ckITG z2{M(@gY0vpD5_GUN$Gd!PPyTJ!L=*t!x@LtR5(Qmenvn%RXqF>JU(N7C3G=wO<_L5 z5l%-q)&26K(d4ud1=-tTg}Sh%*z1%xxEog%#v>e{v__Y&4eF&7hapB&=V~*Y4a1p; z(&O_riDFqKp*TOX@KJ?&IPD`ONZp}JOkkOK&N%1|a(cWO>dZsA9?Fjda%LU{{!3|m zD>wIF9N#N)$J*hYCp?T#&3vA54s^h16P<Q@uoy-@ah` zns7Tef^Q1qh&$7umjt`gYhogl7bTiA*0+TwH=c+WJ6;(0gzY%17eyNO+Ehcys5- zYdZd>xCwPC^%TAj=PSiDlXdxLNDv=C1d2FTgpq|S3!ff}i!|eWB!O9Vv5^H4ixtml zNQox=7YSn_Doy_eujJgt*fff4 zHevIH0KLPTm;l!?yPz zV`_qG%6E)^s#3F|+5gn)`yy>UTbLHz0&NRI;o6>(1P%E@_>#Sc_e0 zvFq1OCY`M|KL z=q1=0=k_;@>mmFxqo6UBk36G87|}%JJ2dc9VC=#?7VHk(UkazPEji;MK~U8$q*n}) zU1HTaXR!N^+qqkOUq;lQR-9aeq@7LI%0oCy+=15?I`&PR&6GTq(UrbFm!&}JLY);2 zV6jkojTt-;&r2UkG@T~lzNZ$$)g}oWneCN$eg)@h2GEO)SRZE<-X+Y6t_gAmM=UJ| zz9$}ty&xI0;5f7ibY?T$T4)fCdM<=b->xT`U?Ns@qAbwF)} z-ZVPa0bd%D!Bm{wWijDnhwHMK*dB@n>mU-BSIyx(_}RcN=Pkj_Pz%Cip0M9a9YGH` zH)XOQyDJD=YEHfhb0CTDX)fgARVI&|_Rsd^4&J?4y%Dp?X4Z;r0&M`MWB=C!zJkzeM4V-~GU;$!Tl+ZK{k-dGA8i zFnVD~r~xiRddQJ92}NT6?1c2L6~(ItY&QdLoJp3`qyz%7Buva;f7?>A6baJIKBEZq z&FQ=I$RV*z28>`;(+%dN&B^teY?k@kRymSTpW$>UmKe>$1ECvJjb4PD6Dq=c4D!E> zyDQ}(q!iE5l8`XYuDbL)Z?G0g_PZU z*qV-P55ZsAa*JQ#8u9rY!Tr8$>b&`&7(<2&w8{z3Nl2bhR3VA|FPXF{J z$qE;w+%jpzpF0G$;Cd`zX63cPTb=hwt{TmgKF4z~`KNG128g%sV za+oa!v3Cx5UbfXxN%&V>Nq9ek&lDGQ46lNa)G3E?=7||}3>W{i5mbQ-=q_x6L8o*MZOX7me&`YK(vwvm$rPQ}@%-}Qhr7mQIjbi(Z)tpYp&#;Q8 z3%Ugj}Qv zB>fRPUEkn(1lgh_MMoSUb>Z_A@-_KV<#v{{oQHJEgu^7P5j9xk_KUSYlzzqZxKHO@ zogX6Hi!ikn!#@BL@8bOPn7p5KSDW1X~z-GLlvPI$VHCzi@2`0JBXtsp7T(Pw; z)sW%UDZB{lSL?yh-+Hv|?vQGbQk@_>&E&KZ_Re{+pgo87H$9=wgmvSmg*EfBDVrzc zcI%LUye=-z+Hmo z-Zfm)ra5}THbUz8J%#o5@0O?@maqK_>?@3#WN}|43q=AFzrqqDvK+2hC0K&5#cJPn z!?UfSZ%yiejzA%{8{}5zEnz5=V>L7mqi?pPu0g9B5)UhDyuV|SskSYQZ0==HmOX5V zks#ytm5ES?#`~~X{9AQuHsq;1-ci)vK6y?jpTZm=NxWy|X?(18<<|LgJu+~vxtj1A zeiTDPv6FfrTe?_beaWGdEOpKeU#7^6cgq(sb8gX4&%?q8qOBb?aaW9`NcwOiAIg7L z;U&)RA{^@24zC>NZ?kATYIUwH)~R1PNO@5J6eS!5&m&C4N(#kVn|i4CY+0R z;!~M3h==4T@>&ho$+dhIa{9NTr`{Q^grCae%zS($`p?ksj~o*6s z)MW5heqOQi)a3_f<6ltDn>w{Z{Sos4_?r3FvCdPiFZl>G3}LleA$v>d*Sup=Eu{62HW zdLoEpF(tdeD=VcWq#AvnNOHERe2hdVlWt{bWDn$rQJV00C6YL!Dtnd0 zAQNU6Vhh$Tisa?Or*NrqXGIh29j-ehM3#lQvL(in#HAi~(f5*fxh}YW2*#VmXzbgdF?#QwWoLViZ$^Z<@ic$|ja&ifI2^s&dCv7NBJ z3~kO`lyL~<874tE?5_zkPys(Deobx_{E?8~m9fW)Xdlj3U)d$gWpdQjd<)duDL*Im zoZ;4)6-`PXp>RkO%+`+o!?`~rG39wBwo~ZKRd#HVv7+nFP1)FbjP>4QIFM|l5-y_* zr{0O$ly1vYe+@I9gH&wp_|RXD!W0uq&&ZvySK{LxD?*(i#q3M^ysun~Fu_y4r)YLQ zfa5tibE4mIcIy;=@0t6a_zdEFmiRmW@EElJ3gl@d3DSJ=wc@iS5mzC*E%Z~#mSWmX zw=Ias7@C;Nf1MFBr>4f1l&1*w!x*dMucwgj=;AvdC(Juk0Ot_$9=RN-csh^c5t)QJnX-3W5irQBrse z>;tSR+T-8dvhqkl0P`&&59M7u*S;HbZJs=l;H(^gP70$~^E0wPw0(+3EEW?%p zY3b^J@*PHA$%IAFnK>6(Za3kZ3U&zFjMc&b#%4S{^B)I>&qux9zOG0 zlwOpPLdtnx0jv7~y#|XnIXqE@Ik&;ng=1N5Q{rrI?j*A%f)FMe5Nlq9(Y5Eb{Z|-K zu#t87ubYK5S0$j?_d}S!3HqCbJ6QvN3$HJsE~fDhA{?s;yv=BHNnJG<)4_`hld5`$ z`)0w=5*mGo&+%GZf^R=F+itOfEJjX-C&$YqVdy&NWh(oGH^CvgB}k!ra!L+&xLjZx zq4ua}I3D&M1H;2%-zSG9julci3fvFp+Kd?z7tEsRQjeziNRhJkpAPS7%NbSJZ=w95 z+&??gogv2SNZ6s+Q_xL9+G#<)OU-$73pjhg>yN?vR?1K1@mr2u4heLHmr%}th~FQH z{uhU*QC2}drtuF~Uc;$HNY%scl}OMl4*MmPA25HeT#FQE)Uxy8LVXE*!Q(O>;{5ZQ z@~;oz={yxaE@5l2!W}6R1}!avx5eu&*(iG$CpCk2&UA_V6TN{S!KunCLt?A~Ybn+k zN#TieWT)n^JAYZCRVAe*w6L)9zBEwek;2sL*_F??pznCMdl9-iX9*RTT4t-U(w#Uu z6~C2R680QWCx;5pJDSSbB2OhySHWh;t+fY|+cF#y_qO*Hg6Fc(A;`~=pr1YN6_FLd z^c2pIr66lUy9M1nxDF`ydkr2Mp8h|}q`C2s&=>#MCRL3?6L~XuDBEzJKb`wWB$hHA zv8eIABrGX8)$nreFQ2o)dQhOPA^dq<`Aqa z`=+QG#Uwu8TuxCE^2kZp4mTl^^OgyVTIizZB4?BiTNj$gfw9ezCBsPu^_}>2Kwp z8ete2b~7X?TV6ZD6vfjdisMK@FpAKI<@#k3seJAvN%REYk8n)fmoud2c;SkG+@b#9 z4*0rwq=>cU_9Z>MCwv5nawOxCQ$z@hujw)xq@xQTIWEedo6+}Jk<2}G|67WwvkZ97 z%+{lUIEpg2V8Lq&j?DaX4<0FqXH8rmD=T5Z1L5jnUAlMYn9f{eZJh?&rj0vR|>Yojn@gltZPn zNpN*i3(MsGa+@nj#2oXedd1gwKA;E2Zv++QW&?X29J)l5;1gCm81Z_R2h7 zv4$eU`u4ex&!nwoi>GUn!{OUV#w8&>coLq#jC{;n|?t;G) zQeoR-B~qG_hVV*J3nT^6`kMs1|DOq7J*1;$CNxI_UuLY_=E{%BVbsCy!m&G+jq4W~ z_fq6bNN1eIdxP@`4%owk)9{60cW__J@`$)u0Y(SBu2}Vr5-Z29!KX>+@~07sOukj) zPKEAZJqqtA%3Tt@3c2nH8_v55Vv4Ep4rUmSQb?Q7bTV*ii(rDj2vCn1@}E9E64aWb zOCr^l(Z~E{EMZFD0=Awtx*ios4`X`>^A=tfe%?)$b`_>XlHInT?7AGm>iW`;rj#E- z>#>KDS3+uy|8m2lPt1kaLwJ3LqgGz# zEn!IF%qCntqB!S|%t!qtbtKG4oPRrZtSkZ~jAgK}W@JS3mEX#ID`|R2X;ipU?Dh5; z&?uL%-|%ryUYF8|XiYhV$56@*CXtp_4UXkT_dcDk*Fc*=g7>rVeTyP++zES6ybZbv zbt}Q1b?4_j*zdxBeZuy;^RKSFSE5z!;5WR_uq!=k_`+Xf`M&3XUT2BoVPW=wxUfxO zzcdU))VV!uTK|3){=y8BiTw<&z<4Ux zCtQ!EL;j_qZqfZ`5B>b!lw5`TBQEYd1w8|qk|lrYTbNaiElhJRRs7c^y~QHA^myLO zXDV38!%%fvQf#ddhQ_7QHqgBI91Ug4rNZoEY{a08KuMy{+57Y7g-;g^6@i~zQ-EiRTBCU@3xP)rzAUdch2>m_Yv{m z&oi@~%7sA@o;6%ck7c@4Es#4-3txpz(ccrr6kX1YJw-B|F1hn@IgiWPUz5~AZkb9n z#>K&#G4C*0W5!Nhh)f8}8VP#cd7qQ{PmphkAODfa|JBesd2}KDP{suh)9|||wB$8| z{SN(`AYJi0I4f-!75Lo4c<8+l(JqlTTF=MqU!V- zM2C>fg)1N_Rz&@mIaY_eId(>qCIkX7_Z7vt%$!)Z<*~vF&xn=+naN$*+A7M+m!W$2 zikxc9Yczy|*oVN_OT}}&%A56^v)z)s!{y+E#R>!g5?!mJ7KtSj>%rrA~CEQc02hDXi5~M z2)Txil*Qg0#Lm#y-`AEGs9RWmng^7_BCJSq%ENi5B`X|4dZloq%TS(zmSrD&D)my* zB|J|^uLyiNe?VOXBqs<~`2+Zfh{iI6<0U-b;fp>d^qP1dfN`5u_on>Z zoU{_Dhhr zaGuM^r>wo45WDXAOSd1;H!JcH1baCjEeJP5qkGm7=~??_Xfqzoksf2*&!l|M&U@MX ze$Iqg;x#9I3~qzHW+=kAjIS}Mmig=NPJKFGGyD*;N381QD+YDO`I#2ZV}&?uU6*Oi zN)MA2lew#LKZI677W(*7{yKw6jt6n&?TNQ54>L9_yts4hq?1NY03JK;+9!Hh?187Q z{i?zNU*ClHF#~SC<2&veyMAU!Zdl&(5s0%c6$zd9-h=Z(g`O4dwiE~9dNw5{=ORkc zNSOTN3D+}t4%%ZP(iTpLGJF}gM*`#6oc@^9LwSy{T-LcFL4kLL{0MoRjF>!pE=u%5 zF}CgBwzb)Vivxikj1rftN!(ADz`Hp6GP>EC&`u$rLK%TFP{1Cs>S{Givn)9ucjEa2 zj_;ER{P<8FDMG(oBIBUqJeG6jc|$W}F4@$jA+h8cMlPv3wHno(FA}fpY!72EMqVZ> zZUN_74erHUFP5cvpSElvPxeSWEE>|f*B!FjHp4ZuOySvu-%dwv_+iSI!lNr6DWa9% z8Tw&p`em*K+LUq$A9q4VnD*ERUxjV?5=<8!9;?Kgm;`TkGznXde2DMU=$pqm>rl>! ztKGYC^zc%-M9CUFt1Sn!UC)XBO5)SSFrSXk$ylBrqHw!0zl?v0Fv#@ZJod-UN$n)4`!Fnpp_?ryK3hB;>B4r!v)WcT z-C4pbK+0MtS+MaPr%HGR)VCsQh59N^%N7ZTcbvUmAss;;+)eOXX?NUb!v%jBDQQS& zdl2jD6!K=_4U@uA@#w4+!?!pzFo znDlInVr0*h5sjB8u&NZF9J^7T3X%dzozOgt)^cS*^f^-Gya;7v<~11SVBWu`*D&_k zHrOh(Q^~`)Co9nmwyJ1%?nRuEBy?GO+Tf637vvUQ>#S?4@G#|>+eAfpUJ6CIPvM#t zn*BoClL^#vh(|_n0Cjd5}mqWy|5Ls4YlYdo%O1_s@`*O~7jj87@#=ThtGiCUTD%oT0gT+7VDM+dE64WfVXPXcOC|(3+)YZPEwyV71~p2cP5WSdPZR;J9mn^NZVrMdysRZ z!3x4~TBcy*VWfTHc~S@;lF=0#OVYL02O_cK9?M|oL!A9#*b%9I)+0-yq*!5%8dIbT z92&di?vP!nvQ}yyLyEU$y}LUcu{%C)C7sH6Id^yFIr+UNv;>)12m_$HeCLn(?^H%p zEqH<&si>c!YBtMc6m+E;Ntam~uz6(EBo`Gicuh|H_mU)3=L6 zGgCc~#y_R<5hj(hAzL)c%upii4*TYO7vX1)_ht_}n~*yXp?YxILDHYo z`DhvvznaNM5JcUe*rc{dk1SWHFs4MbeU>FtLPF-`11gGZV%o%-F)_Tv^whV^?NXS`U4344Qj#h=e>`jp_?(RvWKo_Co2 z1oW~XP=%Mr%d<`REi3agau`^_O4TBi!%IjStA#gwPl+!L^NBoG2#vs&lr3lG=)veV zql|#zzN+(M-s~gse=*_tjOOlL!XNu_3Hpku;$B{;NeO7b!}d1nd&X2So%WwjRps7< z*QQtvO?nwI{kcS7kSp98E0$rxl@(uF;38oVBGbZ>c{P@rLtP;*T%yb!t{uM0#FMX( z6WCf5e(6z&&4l`nrpZUVa7Lg%r90;gbnMR{jimUJW;SRfZuZYZa)iPDak?D`RgQFIO8`d?lgD`%t3FfwWumth&f z+AnH|I4K44lL>xKt`;(33w1#@VM~TQWBpZ!v%NFDhxfT>;j94*r-fF1c*5_5xvT`! zc;Dp~WHp7Y1 zUYD@(P9x558L@uNC3rDMTdEfVH4v30BVCcuGtT;tlE|C$xraSC3Rkr@^_H7n?J?EUW<17w=v6k7(X!CPFf z3CBVy@`U*jN|Cv~@7hB?JAxM;gz5zH2-2&#vrXt1@FjdUC8fbTC$<`ho>fQ~TbX=} zAlJQS>+jK7Ga#Mk0DVPp z#vF?*5@|V%cQ{hz4D!Vw&2Z?_l|Gj&!c3}31RRafgCj~W!kLYEPQL%M z^AE4={}I!XtY8p-I_VjtF)Rp%^@otYJN0E8_asl__Lwx%J@LEqa_6-r%4b2IDfHvZ zmCf>+B2ru?Zw>djw1?_f6$TIG@Bk$ZK(3a*fnlBFxV=nYXmQC%*!q*0W z9FtQmxCTEJ8S%b}M2eoA(PUh}xf#~sn*?L|y~CN|QH94!&g(af9^9YmLdt>kxr7?w zA&x(l`%$^X7{$3|M#zjh&EOX0B%V`puxj+;__kc7R``+<&5{kl?!@@Y;l!wpmf+QB zp5!B>S86FxJ%U)97iVOyX{3m=KPEQf?6dZ;BSizH2H%L^>*taxBd~QP-Gn^Oq8aWv z?xwKtji&S^!7l%DLrp`elwzWM&Oi(`CoIAJP-cpEM6Yl-)Dg|p_Ehqt@{a+RTM$}* zOvIu3ByBlXh07+i5gret8sB&4^9diDb2Nvya9)ddnV3%AR>a!h2zo%VAj zVQwBApzqar<%whb)`GXgT5}zDqP!Ew8y;qyIk-r&o_CL=>2O#4SP7_S5bwqX_YvGR zdxQLuu$h^wqVksU({$IdyIq*Mc`XlJZ$^0HZWLWy7+9F$F=4kasM%uj}Dl#dsp@%Vv=^rSiES$LL@2>cEmvIf+6 zX?Q4nlgKr=i13obosA&%`Rb$*l$2cHMeJ9FU}-(pv>?7#E+lNBMeic?4Bs^Ju9HQM zy;sP|m}odMR>60J(oq9?C=Ej79|j$wVGfhG@JRJ9ffn+wtw*fwxe^HN*QAxhHLNeF zG7)5-Yje(rVS9v{XbZio-_dyPQzJ5Vkq0Hk&m1^yGu}E-fqi7QuS9u8!z>vdZlqhW z)u`Gdb`Hr5pW(jt^2k0&F*F6&W(8!Dmor_8&(MqVeV?3eAw%nl{ggj_3`!0HU%7or zrsDTb_e$*42+L_Xthc>5hW@#>k1Q$HlVsFfFU=j&X}jc}BR_i8yUfj3Wreo*d6}?Z}tR z?ua4bOv)5NilY}CH*7OvXm5nRX~ZAbAa&+j(Vvrg8BFJ{%K1#(5C5|m`k(RSlbx7@u4tyt74MiVqOGq3zVF71$@ZoAqAt36 zD6Fn1$mI_EUCnDKk_b|pi-_zimnI>6s8Vt?wAU8M^L8g5UrK&O1L3>y*rRbgRk*YB zB;L87IlTQnM(q`36@mjuK62x)`#nC4ZS_ZCI%{1H%DJ`zRO&V2}F zMmM(qA>@Be@)a^Nsg5sKB-@1hlLg;9`Bw4>XtQV#Rb+Loe#gr=JRqH)XE>wnM?{2j z3#sWMkvcrXU2o=qSFdE@n)apG$U&M|Zei}fWKpX~c>J8P;Ah0+5VU0lIsH1=kFTS;pRs?HGr7QujnMHq``FrML>^&Ihc zy@xPP!-j?D_8#`KIsY=Uhz_y&Am7mxq-NaL&5xg*M^0!7Bc1oI997vQG6VKzRAPrI zC4z)T4Jhmwfocp_rWrnr{x;6$^dm<=hdBLmt`-c}z6n1LII?h5c-A0OHVJLGEIH6b zxmMbWiBPgJ6q0HL*i6A^>>FjqI^xf&;Tz1oG5KLVInK zR)SnshcL`T9%SJj$C{{Zh8^=P&<>}68h4#52~9aX@$Hew9{J`n7q?&!b;P-*gx}Vg zNjY@t<&I=Cd_n(-Kbs;J*_K5fwG>)z?zo(E1geC@|Gizy5J=$KgljXlp-6|(jq9Pb zwF_@Eq=Z(&Y%0~I)$T!R9Lq_qe1w8v5-x$hEL-)WlV#)qG0#Mk&zx*?%HFm^p%DA*8=~i4g0$VoI#*35bW=igD9pLIzpD~w$^FG zk)iO8_gnHL@5Zr5xDKbF??8VZ;?y1{T@VhNV$t}&1>DdvsnhV>drFioK*Dfp`9mMC=~X z(I;VqqZxK_?jgL^bkFecD3ECE&1168Bc4rj=N`uRfb9JXCp&fyih(jGQ#P%)7fhmUsJ*@yK zPcbZ~8QvY`bbN*}Tze|7Bkt!>1S|HZr1sZ&7w6CQ;CnaZfZuD>n(`3zRO*-TenERV z$51|7L*Ajdx;5?&Go@$ceu9=k665(+(w8IOojhjf-HQu5ktXzW!4v92?aCOFHjLSg zA2RrZ!e4a6MO`BA`{=PZw+UYadE!Vx*oo}!U{i*63EEG=|45Ye;v|VLWdCx0|K8y; zem23@+&7wHOwng zT?Qf&FHn9fd(7w}f@5h-KRqX_BpQP7nqke5R7t3{JGi_=P=0V`lCQr;}~O1XoWvnyO8{EP5$ z&ohvWB7xqR{EBmqZ1g4)0?puTH?8IjtDS}?^Eq42=P);THMQ$bb;k1bQtc31Y zC;X_!_s2j!C#n(&mX}g`MEdF`Y!*bN=7Q9)EgeMD^5_5L*o{_kvMOVG>{=x*j4LbU zu-IW2N45oX-XZ1FzT&v|kitG%6jPlnE=G`lA6Kl3MuvN{@HHnI(-RHIb`O7k%^qqP z6|O1V8SHSjJz_lT1@nqR@*A0C(8PG}hE@3Mop}DIbN|b@dnAM^Q8ZGIfSOlTd<%r4 zehvH#Dt{T1*=D4#sc`?y}^lzX`U)ovt@HQ3yQQbLwITEsUGiN;fM*ueV; zQlziDy16iBpqn`dIy?aT5)yH-Sq(OP8WFW@3U^(k*CFIfaSIFL=R^2So!!G+O_o}x zYoIA=z!g6`FH@s=8uOY=ab6D?k0{nfVUr;8)`w6laE9#gW#RBTmnVq-lH-*X0j%Jg zu~{Ou7_jmPvTzmH+(OeY!s&3C$J(e1Rx}h~t3lNS{t=w9cb(j2P>KfEBs&G?*p$zl zdHo^uYkAUmAWQB-Cpa5zD(^edUZGOh)!5&TUXyz&S#%LAT0D@HSJIhVnCh5vTBLoY z>3m%`|0{)ORnqR9rieSw79lfV5)+W0LA(-)u!*^?FpeCA&~!t63EP)5?&!)&4=dme zV?2aZ6Eb5Rq4^h-;>c8vDavE*sxE)bB z4M)7?Yfjh5==LzE8YSYZk8F$-mzni|=R|*f57CXAu_RFex1Gtj(qxEn%*>%dntLrc zx~_|Fq-X~AELerFZSta1Udoe1nS!+yY}19@;IP;M-bWPZOvs0Hg1lFXqC0b`Nved+ z_Nd{Np2AWb`L#0Yg0%=C+T7_F^BH^>X^G>tcFcf3qIqp=a*>Z^gtXZH+T;7Xs`37e zV5(m!k7=#@QjN9PW|^J}1bJO#f0IE9>Ai;aw)hh0hayJ10NKVt8-LmW5*) z^Nwb7m?J-p{hm~hh9IR#Skszl>(WbYGJjqnF;r#lh4W+M`7EU03ig-G^ zjqTCdtMM8sh?-M*c7djx7Ac>ik@~iK1c@dwDfLQh`&3tfX4~2!7JYv6@ry zATz&$GAwL$si0_ahkNB^Z_T2k%BG4S6EU;A`5C@mBLW5DUoXe$?D2Ipwx zuutv}V4pnRiu_c@E0E!hP^Rg!2K-S&9kG8;9G*zFP(*bs3-u7%0q^9X>A8%#*y|Di_@WVorzp}J- z1{rRySg@JmMcWp-6ahOyE|SH%O({^Cekgrvg!3erDN@w(t=v7NO}bp@?mQV9@u2%1 zW<@fEZG`)qkCm6Z{PJBwV%IS2XF%U#vC2s+u9+45GIoJ&$tXRLpyZ2_u_nW@NU>Am zeY>{^aVbaaR8n^?qPTGWjVc&jn!yrQ@Dl8jrU=c z2RxTf*)3pF@zo@01T?*YmquvLvDxw;2=kL+;kbF5@$D}By-%tb>08DLHA`tmH484$ zg#A4!ua5S?T~Y*xsW3~zvyvT-b6{Hl-)>o0}le|qxq zKMv$C9j&2qDTI3>;@l>@no=~3roK&!WWGbub-(OQxPC^)!CQq_b!Lf%>M4mfjCK#Y z8Rr&|ec7UT&O4^HQJk~~G46SWi2h#O6ZeQ2_8f_QR$)HEsOioY3XK)@+?z4qhDj*2 zQi|ttU&g;AagcaEW~@io{Z)E2`D1winEpwV301@vJS6kDf3^<&%~HW6?CHoh5f{=a z%U8g#OF3O4e5XX>912Y$2yR>4veyVhvo_*g9bFi^GoR3&^G|11Y2h+y-NT-J`QGaa zI=XTM4%r^N^v^QbbK-}GEekuA)?9ElafCqQa*g-GG|bnK7!-L6W6ADXiAi0OQCDFfB}njThCT4{l{tPS z<_+%+_{)k!AHv_R%{o8Kiv=&tP1yLdcVYad)aS}JN4U2z?A~)oapc;_*>>WSz-k+SL|!g@xoMJ|Gve~owCd%BzPP)0udNB z;Pbl2&z>_F`;vaS?=DDe_=-3cY|GdV?>+J3n^GU)10#KDom1?T|E2Jc9qLp0%R@L) z5be5$X~>=?Wx`MLpHO|7!q$X7)*jN92H2pLMB9yRj}?{?R-BIfNi>oYj)3L8tXtLA zi!S7;;m=PrRa;V;1;iI5sAxWhIpe*8c1$TXJy4a^`W_$36NB~vcatHGLUyS5A=d!y7WQ6-3Y*7AK z7KzIiX(?0z$nbFYZKlIo-}=V5#;@c}OqR6B;AyaQsF&lXXnFMt>@rpldE1MiA8doM zh?(ENJA^Ayu7us1qO=fH%byGTW%QxvJU<4$cUsW_wlH{g;~7HvZbPe$B@eDe?uqWY zZ2!2OH8t2pp4$&t{_NzJyuIN`wmX8HjYi^}9bX=U!$oN{gJsK*9oq}NHeRD~xW-H+ zJJ}(w#`(Ib|1^I7*TVV#UitCA?_9qHeG>+_j4|Vf1--FbnH}4xj1Pl91~FOezkL~- zJM}c4m~tM5nu=vJKFBdm-yI&kGaC@RqKVl$@`zq;I$=elU^lGJzI6M1-c6f zd#+u$YFrsB2yAs@va*FHPqt|i^<{%UaeRaNCdb8o1s+LW`iZHv&{=*JBZ{oZK82kO z;kJ_-&v2e*cnsce1pwtQc)Pk#$ys~(w#x1Dn~Okqe`)gfZ3|9W=j7;6vvF-Q8q|tn za91@DIaFx&s&l-w>4Kg`%9V|CAkElqB)VSPVmzBV@^MRbkB6S`8l+nQvAP+Somh+* zq4;qN?o~}$P0qar=ia#2%7TQb{mXg(S=gQv$(kf6*Yh5nlQWUg#CC1MPuDuF=|dp5 zK4eE_3a(4Q-(JrC---AC)p>mlKC^MYIqQ$Yqr-1!=l$yvaLlw{ttD|D@b_!5jy{F1 zQ>W9#!|2%r`5CJ&hdvqQ!z8ftNow-*knote;s-gsw{p~Ri<>#yOEbzNK+e2fi*1(<;sP&?v=C`+jVO*>BW9Je?4)$U6 zMf0m1#^X};z1;dTn+3KSBUD*-peCbkvPqKFv3~j_5ElDo&9WIwnz}4gjS%?5RWVkR zj^J(R!Mw8$r*f;f2_~<+h=t&*KsiitQy^>RBRdg-MT}b)`Z80Wx`{bWQV@&QayDH8 zgFaM*`Sc5q2wszmpBJ$Ol&v+H4ryXV8}zT>YC%PN%vhWw!_@}cC-~(GJ}JVG+7F3$ zj&-o_@=9-JL>>yDGHEaJ6gx!M0g!K-G0^JvEv68s8Fw>|$Yh4_xkX}6kLl}^37NeF zmJ)y7e`SCBFm_z8VPbfX`=M1K?Ia>?_16R@HE z;WZ;+d-)dA!dPa$gd6#im2nI>w_tgb7q};NuiFi4B>vqbq9Zg3xEE6aybrD=h!V85 z({d+lu=T|Ia7J~B%ksc2c>gSX{FhVy**Uv>=lQpU{YdnWF-2uv6M);`l0SR%U}w-b ztsc?{{`jes&2dNEF{g8;ZWG59PEm?_mJ=3jm zdLy~hhvU^q?p#wJagPT70Q)m>EJphyar`Z@zZv6gaC%UfsMS}{Q=-02S4AJB0gu$l zKY~B^tU}M!xs4(}_**tcN#bDuwTVaYqXg@FP)`@m_Rm47ox3E?A5zLansHy!Q%&9r z*ACBRP_w3`Yn6MVTj0CY>Gv5t`;gwGr~v2&pAGhZCTdNTZLp_-ZG#dM-gRniaAo7U zJBx+rf2Wf9#xRNxM%MK6>QMf4oFUvV(tl8t@Yzw~VGQH9A-2GDVvGG!Q~=~l=hBfh z_8!d3|2F)o$xE38*tNp@%PDh$R2SL$y3GmP=U_Vr)r@#EwoTUMYZ&Q_Us2~Jj1q!L z^)@(e2p=pXNiaKOi?1|TlA@!(EVOcHrVyW=c)S*V|DT!de{})BveDj*~$@jUu674z18!2cH**S732fKv6B!0m^2D6fv-`X!(u559}3T!S0zy;$N zv^spCHe!<8_#Rb>+}n5<_wm~p1ly%94FE4!@no{eis99a!B* zL2rr;9p+qHVhra!rn-v`NpABQSW!JO_UY);PiT}7{34RXacy;0o z%0G?BhOLQZ%Xpdm_Zj;0dH{b6#)tCh-+v~4|8Hmi?9>obVe3tP@m&|@)eO6b95Ms$ z;@Iju7AIHH&3Z~i7UtI5pxm8nz`Z*4k#Td%YfR?6VOyf-#Be!IMpr1+Ga2nm7iuO6 z+SCM{`S_CS;38rFmT=c@kv8Ma=zqZZN5_^SK)?;#oyad)47V|z9us8NZ>?5~2e}RU zVU#A|&yf?)HZvZwzIW+uUWg?)H@QV3g7wHerl#X(QdIA|scERg7k zI_r^WheCwRl-jryf#J>sYnBaqEjk~s*??L{?3KArY9{#H5}Lp+6=ey`aIi6?FDpGL zp#g=kaHv_mFRGk zgsqw$Kd**88<_Mx_an$}#{HHVS&6sy3cQ6HqsdKqJrec~%R_g4?hlPX1d&m|MLvx9 za;|&OCU$YXWX&13GTEaJ{@JU(Icp*#DppBDFS0vv8qXxk+OC(EQP6y<`rokx@L@_smuHt;_yeCIR?XspUlwdAZ@bT)G0(;Q74(F z*i3g_pjm=aoPPV$IQ|`svvU-Ub;bwyK4C~%cA0$ZVaP354qXPvo>)FQeO2Xco00Yj z!nx5ucP4tDfikoE9}`tg!E1)IRma;LjTK{C5^tw*y*hq&O6|W? z=^%m8)uo6oXq{3`wkEkNB%Vu~_T|xJg^4sRbtaol%xd>sjQY^nJ{F@5<8XwBy@R*W zu^QAN6?AGyfO-e7=9=c9QKJu4fWuXs1#mT4xGr=++X6WS>(F~%yX*qV1?xz+QINoA zO>zSa3*3V}LpzLd&Pjs@El?|UADo+3UCV8xqZ41UKt=i9NlB1rGv)6HsOG%CjJKyz{tVvk z4laS&GJ?Hz#ueOMmQByoKkf=Nx{A_9w;^lR+0f2(F&+pg=2^nct3b_-r8~RL5Si@Q zo~WxPSn2GH0bi>;`8MGAYW%Cg+fS$N>cn>Mqy+6Y-d>GzG+x#D+6z59hXW)0I#U#rR)~Qz5Z;f59fRkYxr#my z6e+L0{d79@sjIpk#{Q=0<&OjMe`oHezSq}GN$oS$bpjZ%BGr8J=}ehGv{7gDZ__IE z`{#NRFeyyHIeQ9qoNhIn_bR84*WiwzK6IwqLq)j_Ie#9P5od5+18a%sjfy~2PnV7% z-(tVKVvv#|yMO<=@VEa3{*TTGS@`%v>(HL{l4yTNa9@Mg$1e~~d2{L`SPmy%gY)kE<=?V<#)mhI(xpWYA1h5$;*veG0C%HPT8z(Vse~w2;z%^}6xf%B< z-a#%ieXgs>zLQZF2>{0|*t|=jeGjRrbAmO=qiyKmW*Ox4R}MuVMnhQ|>pFP+m_aj_ zASF2@PA$V^qkZZB3HVnT+(+<#eieTF&%yPxQj2l^X2gfHxzP_Je>(LX(7&?>RJ2_M z(nHV19YOzB@E+P5uhUX?F3z?DYY!fuo!$oZWpt!(NQ>c%cKvnc8F|trkLmpA0oDe2 zb?$e0_QIzmT8OhCmzON;KgD0xki)5rG(LA@Om|@XE5h}7Xmh+@96&M*kX)S zcrJ;bzsrw32-aJ{W84a9S{A|1QWZY^>VrQ<;*o;Yb*Tq%Z_fRI^gb9_B)lWTvkV^j zn}e?@Q*`M~_GjY#oWfd@m9sZ>&;1AFKY|Cw@lzUraPd*jqVc&8<#^E z)U4NFjgA$imG%bDPr08`k^`)VCNjef4vG7EhIMOvEK;SvPG!$uCh>F6f-?Ec(daG# zsL$!FJ8a7=N^j|=zOm6tjqKtJK(gKWXeoX^mjZxlJt%!IA}xQ*8k zi7>5>N78C#5rnIB$34^2;Euua<-8vY|M4fhy#{qN{9DZj)lyL53TuGrX*Cm^AdO-Ow z%pJSsI=Q>7wisMV-9WStns!F@&wO4FE& zDuD<|Auc{?Po;`7CBHZ9LlL`i53alOvpV#_vS(6AeR{es<gBtX-6Fu#?gOS zjDB_nJJf5C)}-l0hSv@IY5X42E3_d(VhkY~ZGs=7>f+z-!O{l3IB7{-McwvDK||wM z@6&R3O#sOBduP|K=L+ouAHx-|87jDZj`s;zU21%$Z~92inH7z^MzA0Y-cpS1aK36# zLnFwN9GeFM?jk3KD$pNk;aU>;8qfxJGNQPEK~;dH7tWEKwkn7uWfd@eTG1@Y1Z=J% zBDHBN>)$jyL4uB=;xq<4Tj%Y{{Ox9ne!iFaF2GL&o`NVM(P84a+aW&t6;kvrk|WTP z-s{nfe!J!uMV#}ob@`xY#Q0-4Hk@(`jT=ZSQ0HhcyL{VipSHq)&yW!A-4fZ1vkh+7 z7<=mpp26tq(4tIqbk#u_o`^+<|2Nm5Rr$N?I_Rr-8rg)ot}bPAMb^fxcaD)bT;u$N z)@z;z&oItw28UpzF!CkjX>ahEgD*O7uf+fH=U_b>|06j+-<@xW3YNB_TVD(tcf!@+nhc!v&}80<#rLyunZxq zEej=k6|Emua~8|oF*p`ToBp?S>X~UN7@NAKR1Lp$nUDm2`dovNJF90R3`a$k&k0zY zg55vYP94GinMt?*^6`5Z$rY-#pqS<{^*Gs>`&vPPYSN{cyZp(UWlRWwf05dj!8(BU_{F@FekS#jLY_uwp0H=RXgB)(GP zYpJxYlaevAv`zG&yb^!^xA8bS-Ic=Ei+=gIrz^+|9)_*Do7#->?l0rK6{Vi1Mcum! z8h1@>mNr<|!B~K+g; z=kaOmFA1M|Hf~dFQR%7xEh{p$XQNCp+cDt?r$IfX4qjfc9)s%e+KlwBFMs6eaUX%b zz+Z#@)k*h|C27kbR==4F|`m8Kr_hs1(C1^$BN+s^6W3$mvSC!K-|joA&9=J?zt-1?nNvhH7Fz%W#G@5?McYgh4lGZ;CsyI?5wkd2WEW$_#8-8hy+ev^&t zO8VZX3TX*^lMi^ztPx$B`kqD>=%-Kj!zj)u(<6w|f6|dCxA6_iF9FMti$Mpk(6dow zB|Eo4i_Y&9{&;WPn`0yMLaU?w3EMenv)E2UE{$58mn%@IMsWAcaE&5a#5Nd1PMPS? zk04*2kp|<0$R#= zmYeCcptjEY$htTq=Hx`e$~q^COs_g!I-Tm$$lMcL)*JZCh2Cw``s>mcsFSrBgyYiP)UqcbK#bk?42CD+r`pi!#p)>yd6P7;cAvacfgY<`f$GC<5&I?5HUufIY zCH@`_#%E%?JNeS+UG{=+`yz8SCK6>`g6t9iM%Gzt1AI}GxHW|-af(2gZmmH^R#o11 zmj!b9UJJ~XuTKY$sbL!^)_a>lakgtsH5^jcH1Nuex;4_Iuhvk6@K!Vbc^BUAq3P-3 zj3!Qo&0K@=FkWwPz74qwW<>oy{x%N^ zYp`5Ksbp}Gcpt`gYUJ6AnDCZWu`E>&Y{cYx zPu4oOF4gW5M5Gh8%P7xae|3%`o63*b(cVcF&zkk`=c7@-Dt8Ue?4&(YmaIdhHu}{W zNn>FTurBp5r*_0GhPr?y(I;V0b6K&y-6Ra?w+`Z7pgk2>(|$L$H{oJ?hjB{Kv$Vm{ z9EvP-{cT$R=7D-;d8^k&@R!HSD5vxV-ev!&7+<^ddZ-}ixAFyO>Utw|kuI|kYDoOJ zE<>o@IhiOE0erhMtNW_UVrv?Gf7Q;B1`kiXg>hYCe_tcGRuSm--Plj?x5Pa)A=b3R zWcr2s5T>kWbxgKF?8dRm8u%FUYu`;7*Iq=5v!UXnn|}Wk#$Fx!3f6x&q7UvRus0_S z7w0y>Lnrd295kImi-5k>HBp z&O+0!INK$WRt!ySm~_D5atTx){IJY!j$J{!2MqY$sUwjD~qVcks&REEd^ww{pl<-<*;SAx8VG3 z&`M{&6o(y)#K8L*d_AGwI+WhV;JgKXv25f;@6%m^HkFI_=iu0!$D(Wnl0FZcx`r(z zynBU{dnTnuT4qq#Z9>0x=^u_hAzP+Ve^hnS?kWPtm~4-U+K&02(2U+XXLNj#Mj+7i zFdg#s-yR%qf*a+x*~&KUoTD+vufbUoM{(M!5$;l*<<`{1yCTM`&1PiGMDz}Cm(fn= z?t{;8{+^Xc`t3j&AVqqg1x(og}BE!bbi^Ci7c3q-G+ zbVjno({0gczjP%c+L#g72IGQrmDa|KI`ar2+&#%fHq5y$S^WBID2rj2aMo4{Bkxta zbu!##;4Wc!%(TIBIps=7C7;J`Oj5GGR{@6iOB|7W2gvR60sQ9C4ym} z2=87r365mH074fEs~S~;^d${!m&WQfjCx7P&*oCflC;}Bheq0csxj8C-A^r@@guRn zCsF~%pwz@T1O$rbz(0fSXjs>d>$SkXWgc6C&3@UHa@9kT(u|v&GWbSNkTU(=^aV{J z)w(C2afOL(37%6O`}?WURBfu zaFs+PBe%iT261e=<-yYsaNuSk5D^YIb*s;YJi;W2{=BHCfu z6(gJ`p{{zvljd)~H|NKpDMrk6EeagF^XG@su-+o*HECqcBq$b*&(kfvf#R}*xD>8I zlFB&tpshhGIzao-h-$-lB#FI_R`Heu-zY-Z9u}+!UAd%S*#sTRn`1XDr*qd|rV$}4 z(Kwb(X@%8gqv^;Fnf8H?Df}B658HQ|anqlj;G?fFZqrylmY~mw&bLdS zl~=G(p<;7YaF_Z1E}>Ds!EeneJN)dyI0pN(@pcA38GIFE8R~4#&3L*F&85qQGECEv zH3fT~Ipk?zOJLhzZ%Vd|3(ha+dKLEE)x|8q_!!)atbmVU{6?Y`h*L%AS%e{OSw8ST z?m{_@!xGsBA3k{I#5wgeYZ~FNEErG;H}OSI|F|>9)JBDOg`;?4;7Yg4sTjfg^RElCgr~W0q2mgIE86^Z$=(T z`i)!r^kZ#eV05P>CnaTWL{oRzlJVS};{kP(tK<#WWiq?p_{M6_v2`9IP;O>)bAHnK zdUZ|`kB15XXF!<0$&5sm73pynV0TR~t|DH$xq`~LAU$U`MwY_-%sPko7wlbtmyxBp z7+C>i*)`rCf}t{A^E0>y>*wHgiBj4fR}tYaOr+EC-A+a{?Sg#X!rQ(wzV3iQjB+ei(N$ zCAjWd8`sHG=_xpKhi2@#~n1^#?E*#trBF+;MtcrW$_ z^IdxCH# zf|Xf0d(qUWRO3BKRQJ5}0S=qMf;Lt7Rir=OG~O#cu^mBs$wIgUJlv^C1oe1bAbB+Q#y%){ur%ks6Ztku4(tP#6_zh2AL^cMnN@EJb;n~y2)n*~TN3NrlM)OE>hgT@ z;a^1~<#n6ZInxNPSHzLF<$cs6&8(reDWZys)D9P|mxg?j3$zl)? zDWs627fj=i+U~a|FXAwDO_%#(=a!*<9Aw+c*)4ZCk`_2kqcN z6N4or$h{W5w%jzS%6E|JpaFjt=k@ONRqh9W>TK<+QGWz!(`(3=_z-syw_tl39FtJE zYW)8C)3~0E_-fjpZ~9#78QjO9{t4Iaq*YVfQl--ggc$zTSJ@3-HOcw&Ei;OsL^-4Z zt4$)?n4RzU7pb-&o>N%68Ou6|$;n@5R!ex0--7$if8}&^L^w>B(k@jdO!(IjnmNsU z5i@1DP-GXsO-*f29nWIYyj_L%gw}(xcWgBtt?)67yiS_w{ETcCQW^pT_iQM0h2@g7 zZMiiiZjBBX0&$8)^kVa#z!EMG^WHpOE0YNjOa&&!&k)Zuequk;1OFIjJLaSx$NJ8Fe7XH9}yZWE8sXs>XN+!EHleprPL<-Etme z>(~$3HOIWfKmMKh?Vm<|4z4h+nL$uCP37J$Bj1C|joznU zedcP!rQ_F=;k)Z}tT3JeuZ$u@?Z^_!j%4Ja!z(IsT-@_}IfCwq&!Qcn)>;~^4^v=Q z6r-o$nM}4v*Ca@rC+&LsWHT_C6@-ehnAsbiX8K_nq>=D$Ff-f3;Pc_6--CUX(5nrJ zLpK+VF+a3UU{0vospiQ-U4LI@c-R_jW7@~6ak@s`DFC>**#dB?4@UM`sR!s@`v;OkY?!B)gbt(HOPAkmKJg?R24Gx5Rr9N zbJwomdcw6hr6jhlwOn)wm)b+!Z)}5Y80|3bQ(6#fA`5-Mc0NW}3jH zE=9hrQ)e~p-`(Nz!5Jb!{t^PBDDp_M8;BTo!(%k9_?dZUuKIKw%L(cd~_8FQ4R(=(bb`#gg-mMLsgGpteN zRkW)bCiQ$>hGL78vs`n(eH$NlrvqonqFklf8YUg^HS1@Ot{B-jhpi~Kw_h4P`}7ch zcWpK=>on$T^hu6vT04asb@5+TqgDg^=M)buhJ85sZ;8iyW3$eAB+~RYd)Ej5SrVTX z5?g%(jgo%e#=JC;BG*g@!_tktJC8$XAxpMRS4}dO zI`{JTbaC)kfqRaio`Y&i&)bqBj&18aOmr_t?C>#jIY<(bonJ=()bBICz`q9Ll!NEz zADQwWiT*>*f!yH7r}M|ZjQ49$$9$ijgUdzp^Q;kK^x!HIt7X@7mW!+CZ}DsGlaq4F zorwnyxeMyln$*qNt@Czttu@Xm3Y!th_nJ6Y*?qTrXf$r7k@sY=C)Wf_LL9-V$ciX# zF&Xwl*w!B3@4t4dcaj??%D|hko|=Gp*GQW>c)cK6qWz~aKKdNX>g=?l_il|KFHReU zUX?QulW%daL7QTWfp@-z!;U1# z*&X`6jwB7!Bf)+dufuo+q*G^kpJ1~!YjX1~6g_fqroo02S8yL)I;lCaOa$j@jCbcV zJF$1RZLke}CNxbG3^=EC+s&w1E4+_@wSPA)gD9P4t{gxb1Ru`!%5XSaA8aLYMWY?@ zi*D;lxbjG{QkSM*Jawn$;Fx&)r5SHc;@N8k^~@oc)=hJL$J9W^)J4_rxcGTLrCY1u zdvTU!rpe9;;8|h>2`p^iKOP7NkgK`fn25n2UA5KXc8|NlK3yV{JH!Kp@ zt*Y0rAfE$kN=&mFB!<=iySfb@K^lYQ8l)K19$W>k8IqEoolyf;#ck%*SWaO)kJ_;2 z{Ar6O1tvS-GGt|Wt-)(zkaO2w&No$(Uv9KB^D%&3)=8TbMJ}g=7}7OsT>BBQ6p4vO z)_6EAothG_BDH@2J|he*1tkV~s6bi893eu16ibyY?kWmR>K-&x_Ygzyilq;AQUpsH z94XPW6wf2(7;}-fz`x_1=V7p|hONQ2cBsbvM0|uZu+wDwbAevI9SLQQ>fNA)Lovpx zsy3%-sk!6VIIuRuzLLhJZoF5c9Puk8sTbqtyYbV9G!sMbZFKG3mQeTIrpV7G>wO7h zzr}t?r&dlr;S#OrfX;KJP^yt$hFuyN_EotV*7eZeMS(5bV*D)E%_TbHe)*N0=`GljYSjG*mz)o1c2Fuw=w+@VJNU&J{S$X`w5@UBU zH#Xy8#@h&4s7ByN&@KT*&f6%fQ#K=}uo6?QdNb{Hw@rafR~z_TLF6K;A!z(LuEAa9 za5<8*ZJljbe0Lg(y{;)y-xIHg%dydeV!9NMu7b8sS5lpUSg{UlQrk+`%EIPEaTKkm zH1gG|FHO1{Y2w8H%xql&SE-A`UOF82;LjQyms9??3_ldX-R4f3uHAlOC^*Lx>~M$N zD$K$s1roT%6vM9I?@Ih!EO^(#}XQre`@=cKDG7zKLAe7BLWhI|ivsnSNVhv(7R_tUimuK*Y{#amHd)gXi7h zW$c@?TF^G*sMCcT`rI2vBG_jGcV^>S0Hkm{W6J8FUnFUZ+_W z`C~QeGR2oe^xJ&Ha*HWpSrd?+Cpaysn9Wt^bxlULT@}H-7Xl}#87U7cQ^-CIoM z3LS5i>bJ|$(vAhK!qlV@j{F5|6dRv%RFe8!;F!CIZ)lJP!*G{rv&?E%9C!?}`k zq>0Q;N(`fD*`800I_eY-77LaV+$nMF!fqDiaZE`cQZ3V>HN`Xg;cS1-`tKT~WaLW; zc;Q+H_#CtKI#`=7-?KPhi#)0}74V}EM$;kXF5Y$o9HaAk-TaR=QQs2F+u#jVvzP=W zzxKgxe)#j^v=hds^UMqBw@!NNS`TVACp@P1LAw&VI}^tCpoe9`j;N zLp#^%!4=XwEF~yQ@Rp7K6qPE?5Ruaq8N4vL1@o`loGjQ(aJ-Ycqmm7u|{R`8;oziePioLm1)JZ^|r za9)BIMOMeLx0#Dj0^7k|&>VWOT!Z}zUbS<+8sp_$UryN)JQ8hJ0k`;|Sn%nZWVAku zt0G(3Ry0+!F_GD2rlxHI(WGj$K4pI;))rEQR={--yIz|qY!?E8D9tvXWGmdp(P+aS#zQ~c_O$EY`d5Dsw&OT!XFbx%mW$yIt z4Ay&4CrbKn5x-SA)R_(eCQd?HYg??(wJvX zxr6tslNelk7U|DnZG**h!Lq3ExUwrh0_a_uDo({hw_Pm$YFe9FC?lrN4%9Yhh?9%qizffwkAv2)nz$L4YsOI`7)zkna7^kmZ3|$ zPe?~h5!s(cGyxw*amesm^yN8CT}mP*T*A0-anq%yY$Q`i$$kJYfjVr0@O?%T$1#Od za|Pt%FSX0JzTjKIJIdq?1n^Td9Shl9n4JCdrL1sv?K?{emUn?jtQ)U6q9vvpcJAoY zswm_zV;w=S8rknI`=Q5F(q0OtYA(F+8tP`(WiZ_MjKt5?_$-|}2B|w`=(XlKw!hKE z6QRRqJstZLBVmk5zjRUUews$0dGal4(zIdkuE3_E@f)Vkw|~Pqs*2g1jeQK_!+0(Q zyMuOD#V||wlyoYwdz3b1}k3m1R=Wb{>%M`?oEOBK1(6iEp6HSNw$LF9QiB<Izdma zpp#yqouX>?cjNvLk#JPR^*B|?-E)lTGkN=--|49OIlK}-dSbtgKP@=lJNJwxA(eeD#CV}1rL4<#_p#_cMK zxfZR_vC}9`-tv6uuk)$%(QbjTMuMIVudvm@(mQ=j1%K1pB0r4f&G9aaThCg_rAr)p zW)9Z42WNvXOFS0Xn=sp>2~eY=U7ukqYIMBnOj$B4Z?G&rH3p)b)(t*4S!LHI$XuGp z-!_w8Z5DPZv<_{b;z^_-cTBt0^&c%LoBzt%wmvl&O$mNAYeH4hAiu*}67}xfUCxTt z|8l{NRqx%%Vt+^*CYimBpxnw@`F9Pzj$k!o-3Q}MyZ?R}t%@UIu7uNN*E96>CW}!{ zoxxy)&gc9Rgv(BGNzzbr?gr}^tPFZn!B~;a_bRf0uU&;rO4{w_XHt6IwMYc9bSU9O z7mm}1_OLyqVD&joT(Yqy_SL0ZMuaC@c z@7fs-(BAgDBAWkOR9P0hY6RcYUzRaf0wQ+-U%^d#v*?rgU8IC`)WJ7wCgRC!yiOV( zIp~)F9*fI9k_Nv<&S_p`cQyrTTsrM)1a%CBL5yIy%X*UMPALmgQm@A6f1RxVD#YJ1 z%ilBocPYQ}PzRDH`d1>iiKFeo-QoCRTH>6=gE9tpc9E&p;H??C2Vc$fPytVjS4;G& zKPy~g+_kG4Zl^A*wn-0BhIXnP!EqljUkG&57sGY9wg{7Sk#@kZz={DhkI; z`Ab8e-Q9!hdrZ2(TAk5?D@pFU%nN=29wmqeR98q*x@W-NRMOwce(kcn>9w^MT$<<# z{TeC~S}0C4PW`)))UjIDLvs<_X#!5VvE2rjY+GGJ68%|oGP7OksOwOt-6I4R*{Aw& zX3qn(A;8dTDrma}t%!^bNu6m9!%ES9+rg=pQW5?vV^8so`Tk{#B! zVC=?uN|@)jM$n@xO4=TV?RpkU1Xdwdc-x%%O8j{aI6J8gatrOa@4-*YnIn7)iO*bhI9fWSl)yO;eS{hBzbk z4NX!fITlScuwV=Umb|JtSm*Em+Jm3kJCHU_b@nqc{xn{%L7lI^WKC#(iMT>jkFU9M zEQ=st%W5no)7qd#Q0_s!hQfk&=byhP{C60?D@}BDO#;rvxbsjt;30iX4|vRRZhY!J z7Xo^))UK)FA|dLXG+8+jS<}m);nX~6OC@rX>mo~Ywbsn`N?28)x16p?#;wjU-@$rx zybn&3creNkZac;dqH@v<1iRd2?cStKX`wyyE@2A)4rwX2E}QsigRk4!!+5m8>xQ$d zap(arRJb$romyu@VhD|$%hXCZ)X5$hKBzb|CICqVLk?#hM!yQ_GJY0FtFbI9cG_axJ8}q>fq5feGurqy zSD{&Mg$lebg&$GW=$Oe`3;KD>B={X}w8tQ4t%71Al(*KE82fK{9WyOAbVij`&&#h! z?-?cEknM4|sjJII4ryWXKQHYY$EJU8!Fh!C_U#&skR6P-5EAlGHR;v((Tq0>d@+V= z6g80gw->|S6*w>ff$Uv)%{wJ}H0rDIv&}c_q1{f~XJcN!y`b8YEw?(&n8iV_Gq~o~ z^6X4Te^2;(u+P0;A5Pp7LePy^;QolIxP|2Elv=4A8>Wzl8wqNNzFg>&-Xq^HHfHuX7Rn(BeVU}sUNdYSTvs8hsH&{@ykY& zCr{vcX;ita@=2ek66B(2zaKwjd9o}KPn(#_IGwU6@Pe3D%NX3@v8BH4ZH?n<5y75wK-SF8$agmEOpr!j{?`K{o`JpwHZ%>GY0u| z+9i+ElGRC*gJqLfHg^eGN>fFBHl;u=*%%-4NatJDrF5vk=t)z6W!n5klV~VCU_6|( z4cI$294^R*^Y%<^pFuwcH*jpmco^I4P|_rjtwX}pbxy@zMR1#x71i6m#XRA(dq_xH zv^Kys4ai-gG@W2)idPBJqL#4xM1KCwahx_fc1T|^BIreXeare;OCPKw@pND{)?s)H zj`CMQwxq!}?M7!6n|1l!*lLm{;Zt4T-8%=tzwf}l6jdBM)ZMtYVB0k6rm4H?O=76A zDBpmob66%zd`03nlF@d<7`#mJbGpAx!A z9HY0Hhj1xgd_0Z+zB9LHwuh2OHy5V&PSVS~4|pttdr0WJHx)Op&56ZHqqALNmfY^V zikKny7UUIPZhXwiQC;Osu@LES1f)A*RlDkKir`$p!U+;H)-l9cSd6}8?&(jrHd83w zxYo?rC(kTgQxdcfk4gMYz#bZ5+hSNZ@*PBl*WGEg%knn07blZmm{p9|9<*UBMJ_I; zMYSSIUn`+-v~A2w-B@4FdduP$E(_$f$i)z*0)0D;c-jOzNh{n6fr$ITqSrU z1!<*Ilf$+Kmkm;p;C$`Q`lqq~UqSg<`9~hq|L4Zv{~yNq(Z#CB3N8x+I}>j&=Z`-# z<*7+kU-`9ajt2E<#Hk>)G?Qtk<>*(K(dp2B3TyosV_KeJ9hK$2xsy{ok&Cn+Bhq7i53L;&pO8mLqeMOJF!%!0i`I(=uR@*zg?N_>G+>Pt`ds5 zsjDAz$CH}GPgim68VW4UX{P|ryBo_Rab_dV!F>nE8CY_ji+0Rq((Jx-pZ8&;TbcIP z+$-m-sd>&b(>dTiW~$K=EPE$cS#|tYaKp92u}KmC-lWc530BwPa;-_Ci@CRU`$a|- zU#EdHs04LlP;G?#uAaC?=O_wjDM?b@+?-M;DtB~FGunhRw3@KfSc`K#lq&Z3G}s7U z6Db@>wO$5%j^L}q4^?TzEUM}x(=m6S{_VdWf|Il=(NoWc+mJ{Pg^DnY+Kv0=EL8T^ zd9DVJZx~2W#~e!)qio7*C_g*Y;7Ue+cYHIh)p=!o7tGbk)AZWvqWQg_vN@$O_vsZl zgnMlcbv5cHCi-f!jM}_p^U!{ zRh+}Q4Vn$28|BQT%V}-ENUSZf-!4~)1u-Widy?xSC&QXnjeS+)P{zPd6%j{v`g`EJ z#=}b=DlYnTHuPy%8}Unoo6zpPBqQ8e1N<1Y(7MgDi9(o-z8mesXsalA=EhssGu%vG z_j?QOU9rWMWLJE@oLZHrN`q@P?iK2;Y1wugk3Ws&IX%n;_8c5t>6PALal_0RBRDU3 z4@L1_A;-m;f{TH7V-4rybe1m|hjX;fzpe81)8Mi3V{<;<+C-=uxf*X@ne;ODrzW=7 zfM-#bz>4;{dDf3=d?e?sIX4gwSpvcat#)#E-df_ro#x{gp{uTn8fU~zrRR5A-xMLXZH@qQ zNDput*FPOLd5<>+wK#Xt*lau%9+A5QeEZ!geQ>!mHu)*B*=^q1>2oV~woD=+Z;9se z8+%N?^+%#v;`Szv`cxH`VfZ#^Z4kE#hi2Ll*D54q_GmKo@6_R#jiM1kRu| zsHYx&F8Fa8k4yWn$cbZSE95t*-xuF^u7D{VrxjTyx9a5NWY;vt#g_Hh{7 z4U5C}HRy-@zu%CfQM9XzV^BZUO~eBB&;%)UV_b9h`a|Q&mS!$^lGth)_RCuMXtG)@ z|0%#oo2>obG;XW6a?8`S_l!9W^4WP0w_j}b^V_$M=yl8T*IwJc3w`@bksW}q3T|qr z!5PFE)CKNsNJQn3&B$-Y_)SxRVIsn<-@oUgjEF{%?XSJKT~K}o{*o}Yy(#iJyLjpS zD_BNQjzP>KX$@yNgQIr7Ok~7f=0!dX{-x>OV`ir}H(pIeE!UjTb^VUv!5T_Ddp0pgLND6}Y;?>$n+OLwD{)^k!A-zH`9n!>jQRH!*TCpm7po%8OB z^nR(B9gC56V^2neVb6ih>7TnSx#uD~^0y`+D=eMJvPB_9jhOy@0EvZqspK^4pXbm-Hg!p(@aM+mc8@~;Ei8HYb<1L{5=fW!` zT2$^j_{Tph>^v$)&U7yv`gR5x`kcQSK1}v@ycigkg$VOXT?x>w zr5q;qd@eERV5%n1#bk*}Vg|3(S#PZx&LRs*cqjK@YmN=$eyLM_ggUvGsnfd`O<{JV zT0hp{-PQeFIp~j0%Y)KI16=YH$)EzT4mk+YBxa>kmqH8JQ5M1#u8Y4YJuD{IVF?vz zFEw`T-KHQ>KInVeXtpI-n+l>=g0e{=oI)h7GI6iu8Pd}S%Wdp0qkRT^DX$>U@!p+G zYqBKw$P&8PkASl(S#o4OA3j|Oy*cgH@79ab;Mx+uv<_>M3c8^`<7EP{4EPsI82wNmUufM#!EzfgjsYv1~CNAlD%Ep`eu69d5<>3g@{1 z`w++fUrwnSbt}dh`kTO2muU)6nkF+9{hnz~B9^Wg+59C-Z3{iGB|3L;KKIPm!#VfB z))`_IdJlXIyrEq3xmrk7;yrU1i$YY&5S@}mdX7TyekS+{>nl-C?MJT(y(-BlE9BF# z89QDV=eZll8+bTt{SK^AXwf=5-;s=aNK6&Q;LXW6Z72xm3^;u7I29Y4rq1|sW4(>! zT200pq=)vgE$jIcrcp_QkHfH2E}(mk?pvMu1cI77UW{zgf>_t6y_*#1t*N6X&thx_ z=#)F?mrwt_zQ>U!Z@)>57OwAh3rz?2K6tyQ3oHa_%UODzSd6br%!H+Yy*n+zRpj1q z6Tm2{u->H^dz-H@$?|$;tpm(w$W{|2@9omG@oBmX`V7Qr9rzSX=k2QGrq>!B>ZNH* z$%6dUTZLaq#+secgA?hf#)on%)=woq@{dj&jh>w2?}hvC(B5FV6Y+xhg0EM{{woo` zCF~*5X+dWj?~}c<4t&7x|1|#gv(bNc+Ay{@xTo8=tv=QF!8m1c=L9dVwO<6KYFc-@ z;W=?z;#wt&e+>O>&tmzX*@VoWV&b*v^6zsBe3~M(?|lN(n#D{r8eDH0r+HHsS!$v* zqaYONHYO!$6IcA0X>j&X65uj4Ul2?pU~;KDzor~aZ3@c?`Kz;hbw&#!>!Q32C5Oed zHo4H2F%!KEsoL+Dm&m0~t0YA`rZh*%w{fG`!_owlaKtoDGqJAi4i6nD-)@FH^ab9F zzSQkCySifBn?~+wn$3Nvg719WXWD@JQsUlP&}Te_zIQ_O zzX5MYqP>j%5`E{pqkn7?tZg;OHE08l)wy?5$2S98dVD8RO5rct>$Juu?X}%RvpgBX zXV34!G7+R>fsZt(ZoGzbraFrv6|F~Zm{E(^9Qc0+H9dX8^( zdpCLxuE(G)ovjP)z4Rp9X%&98U0|Pcm1V|~4Bwm!yqgjwM^%{8+2%<1 zdveslu4$`mVsx}D9g!EUmQvDpw9ZlRM+$NptVwW~Tc8FUQ5r)YI0mY+Qz z+w~B{&6*qyY55LT3hqfZbrd+}mjsmGp-!afX6mAMo@jO1uj#FwXa3UQahqUk)9G>U za>e9D#k=3|cnwWK<^;@io+~byTIx2 z5Z*UCpXTI6;SY{}f4XUMvB>{^MdG!_dTN6UD03fVv&0{f z_-$xfb0pvt8o3z6WvtH-@S|(ewv;KFy|ltQXVJ0Gh}e5E9A(j8vn%m2!;g;O?GQQa zN-D&b>@2H3*EZ%fZP9o(U@A@$j%9ZvE~Ib?7f2^gLuq&c^$axZXo9kg79_Hsge|#?pCrj+mol zf^iOfIO~v8rT6J(NOL9WGxm3WM)@THAxVWC8I~cAe={-i`^3EF=^XmPi7?)KXNlnK z&KOAo=jlDKw=AdET$9{G7+L$F42|_N+SjBZVvHgkPFW|V^JOfb!8l}NyFVQNEm(gW zL^4o2ubYpaBO z`BK8+_6*v$)@SMKU5g1vV|YOi@)tht&c~0Dz>&WJl<_lzYQMhbCoPx&m#mI9s>Lr9eyp3WS8EFAOBiFNG zHK-@_mpt`tn;G>HYyt0wF`mJ{&%tYz&$7(%(DIvcr9^X~cH1P@rK#wrPJj3^p)}t+ z>n4JeHSM#?9kiM897TH}Hwo=xd{btP+G2K;!3&4ecnqaTd76n&O+N8JRHm{77XAww z;|Q&oTtbi0gtJuS?SMu40e(mTV|P$a?T|;F0=6&)gE~4r;Qey;6XF~k2KM3D-<{Zo zI);$KH68=AIofV2RL%rt>6Fst>FtJ%i4e{D!qbdr7ZYG_Cfn6HNEKcrs%uH=CXwWJ z!mAp07fV{u52wGY+aIn&A|WeYZQ3tdlArc!dPuYBu3OHTsiTz4Yp z!De7Z`iFCm2mmNNW)Lpy$q= zjolzC^Z%^{KIe>DM<24$B#&LxQ;61y-u2@SD<%$Cj{FS`S>)>UKiB zG_LfikoT?(iC&EHZsZ@v(w)yRUQ@4AaGGytAT+BUmPgYebMPT|P$<#yPerFm1-(b%FW3=th?^ z)w9+p-hy=)>vTG`fV&0vsTEYYTo%ltV5y!ZC`?(g*OVdsz`QfkOU|#_wO-8|Ebp+s zjQa}iX(wI@acVZlTf%NRg_I59Y*p6WP}d1RL|^Qa;5Z0cQ?vSU1V36QdC+#pLK?7_ zD`0F)zVEt7Z2M=EW@nkh)oRAEB$mxsmT9M+#YKdswO%G>dL|&|kaHtf*lr2)@1cal zco<_>0d|%kTMlvCTuoJEa)tQ#L;?>}^sXc!nj`hvHtD*QTwt4gEM%iNthT&v@2ux8m=?;$IgR{?>_2=2wW zK7h5eUrC+TJ=6P25M~@h$lr&lz_~hn*5Gczk=3o;S<2*uFsk1gnKH+7&#cSem(G65 zPd&zj^vuqu_u$8wcyIc-Zm=FA<<&8G193L$pt^Ie%5Hd1!CULRvU8x~&-?7E+-Ob8 z>qr7M<*u%-H+>c(IoGbDwiR9GKXb5c2D^%ebQzDSU%qDe2~De(9B^i%CQ&YP)@9d* zzBBhMs^2{*)rc=xUz*-MC-v~%2TQ;|-1tm`r38^46 zdC2nRuWyQ@i5}`RrL@oS{+MW5Q*QcKRx$U;)1h&}IHad(-$KqR(OIa%$BJ_{aYOd$ z61cMVx0}IR4f1D@Zn>^HdVga#`lcQ7cM@k56>E14+67BEThhqA4dcAEn&O7_Hr_vj zkE3yA3Cg-dYr(&MHf&_VGRxcG!C<+Ae^0ln!$ z(#Zn=YaC)J)NItpbU}o%T#0@Ly(#M; zSHXOaJ!p@>-(2a7w^6ToDDn+y8A zHIg`-#}n)d?wJYxt(!#SIj{HaS}&zMTyOLVt%3Awa(Rj-uwg zo)zT(?f=U3Z{A5?ELQEp)fqVZ(@0P8w_^l38&>4|Zywkfl$VP%n1Z@N`%rh4x~wmi zfzK$)9-0bxkvG$hLH${YuGExkb^7XT${(gK1`lIAAgxLTGlNH0G4q^zv>+{kl6G`%THRo73O698B77E@`S2G~oa zN5AC)xaXd7Op99h4AAMJ?CjmimsUx47q9(tg}OYy+3btNq^nJ}eQ58wRCWJ1FuJ<) z$4r;$9gaKbx7Gzw;I(Q^R6KZ0jB%N-Ym5ow*`zFfCir2&+cKz|3(eb2kd74Oq0Tc+ zq%Lp9v#A^SGJOY{OHgQI=6)}-RC&_=@+&KNgr)QGFwP(H(IQ0( zb{C+M))cX7^T4^X>%DmnA_i9UoH2L8Ycc!?zN+3c+l;%YXkC%O&XUoxqP|;^6^pq? z{xZ2yN|iQZ?ZFbk&m@3T$#dLWg#AuFGsI65)4o8g5*m7k`b;FWV(@!tVuQxe_43$T!`_QDr-`qGn-qBdU>4!(W;#wgT4&Vy{Q={wcs#vZ))% zy7(h0IOwv$`X%&i>T^fF3gWaj?ep)D|2{_0PZ^d+a$0d>cbzFMSj_kloz1$&_FcQ% z6Q5&W6$14UTwAC3WZJ)+dIj||)-orbb2X4U>`!BP25aq<6dWZnlB=6Of`6Z#|8mHm znUCRY` z>ueKR!zDYSv=?*oa%&8&hJ<&k*SF64QY#_L{!uOch%Rt-0u2j{e3rzY)N zHqlncs-mJTf{U#w*f8$l)UNf_w!wRs*SGhk)PB28>#OH+QF2c;)jQl?V+x`XT1<<#Eb-nA=F zGr{PaHSMNLqD?ewWblvR@d(aU`S5irBW(uzjH0aRJB2Dl0-bs{p1ZS#V=IiNotGsE_Da$YsCJ4f}EVKWKn)Q92wsA@<=1N44y@T z#o&5H9`BS$kew*A{b#mBV-Up2s9JCXrRrqUYebG-40h+JAj_ zYwEBL@&av@v!WEq_G@8jg?@>C_?{$AOiiBU`wH@Xx<7Py2JmWw{3R$@X;SAKnwaEi z`#Lk)KTl9nbm6|Qt^>9gbu0OD*Dn!i?%6&dw+Vf9NMVY8>CRgG9-sA6@I_9R3 zD4(&Tb*3HGFh&io)3~y8&sFtXbKak(lt{D=9&>85$M-5`YE4!N(_f3|cQN$~ONSmV zS|3_XpA70fNT(4)iGKZk+VAER{PE3;aEA@9tlv3bdZxB)+z+GQaEyR2c$=v9*Ww)W z`?{D`s?X}|1#D9w8(D?ZC=&Xb2WwZ%bn(H%JF#lbb0?*~_W7Q*3O0;fPA95_B5j?2 zn9bbcEYaKYnAP(rrSweRwvAnHdW@R z;D;z=HSTy|PBh%3=*qGd&{HXktN#0DLl>B#Vv*`68eCf~IgTOZ1}=QSK!Nes#8s_ORcU7N&1E?G4%Rk2=$y| zMq^iPfk%RSw_jS2acX5WJ~cHQ3&e-fegxP1pe|woFV*?Dw97mY!+!f{vQjcHlrFtN zHE9WQ7aOF`_b7Kofk&4xR@x!4=|`3Y@FRD&kd5=|&V^h><$%XCu?}635r0wZ{0Po_ zaG#o7q@-BmJ1OoGw*=C`m3M4M(IPTbK2+K{}1?X|!kHNB^aN zDA`C!YfVLiYY&~~D7)V}`^tBALKFH@TbKEdaNQXdBOxRsSVjsrcWeN2=SWlogzKWAtf?ZV6 z#0;m(-;*2Yd+>-sIp8{7dbpWGa~#S!ozusor2r?a*p5ZWV159Pyj zpPxxnDA_vcw=Ki@1KKhKH*3>bgx-j};e4S`h=fFXBTYdN66=hlOMPD&b~`_3gymL^ z^repBT=k`*34faVL<$XAM$S_yM7zUgdSi}I)l5>iWX=A{nNNC1mM$8fF%hv7LFqto zm(RJ13(oY2j-t-)n?-;drD#t-#-#I!oT61ln1oYFfh3@=zK7A@g1cy~@C}@Z5Cb|d zL3$hH+gVP-pUQ=P;q9mrYg>1GISG=n*6bP)mBRE@+PJ&I^5ZwQ8rh*ihQ$&RRFFssrhI`Y0TArGbOL~Ou zblz|HiS#;O+q7ow4aZq;!w=!6X;PIR_LqQl{-1t!?B_>w=R>Rk!Sj7H}ccO<)4`e`wLzXyi(^GgZC@(<96O_kdwr&_pK?* zHk5?-=VH7z*m@Fs{?)h!q^D8;D>(i?PW_Jp|3~J>e`HqIrCw^_mjZ}X2@0y!lGbMl z3I9emHV=No@k>JC(FF`k^W5@)mur2rEV^KA2%r>Ic;Ix-*z^ch0z)Lt>>{e(NZ*p$ z6yUl9#A8VOnNDHVA$YzU>&~QQN+}&#j;FB&v1lEZyYV=j^wX)gF5h5IGtF2NESoqh z<L!sRxGGQvisZIg;x1rV@%P@ zNZ73dBWB`CvccZw59|u2V8E+E4eff$BCq`#5;;|LvHNVx@})zvdGPgSJoAus;>$_* z^pjR~yM(ba5tBnCr!x1--lQa6Oe0tjBDQAXFUNQHft2TwNj%#g4AMejuPuTeX|DNe`<_P0@Nj7uB`A3@!W^Pw9jcgJqkG$X0& zs<-xoBZ=|eQRK4a!ETAQIqMX$K3#bXxf|(Hs-YMRns`MRYd7+jk-ilB+A=J3IU-W# zv`iZsyXIHN>fl_!SEqOoRT11_ModjWGZlI3o3s6ZdSz-2$`M5C95I7yie9JRD!lSJ z^VcWnrcEKRe-5n9_9E+hb?@l1-6Q}^|A1~Pa5m=Wkkpl)+u-R&PEIh`B82F!3b<E6`;#+@*fMv%x-DD}8d}A48M7 zc@~$U6;8gVZ#}{CBk@__v&fG2ek6Wt!4^Yqry`oFH&Z0}ZFHQ=J0H#Xz<%rmX4e=dItZ&rq4MY3^aRvRBJ|^7AZ%*{!x+myq&f!JPRI*eXN@?sNB(7C`rX|!dml^_yp@S<{xVGRZ z!MZ_T2Y1%&KTloMuA_oG66Xcu`YR!HnP8=4#-5Vc5n%LXs2KcIsNFYhf3-5)@~{{r!;k?xal}f-&h)MO~#mAZ>~j^{x?k&mnlww`gm2$ge$kPB+#zCJ5s( z#M1}aWIT8a_&p7NmPFg2TB3zBE(O=Ld;GE+mLZ2hw-68|2Mfm9Okad0SO#1;H^yu2 z@(eSz2&OSr&L*xwiD`FI6zMYt%kFFw{BVXc*NbT#Qa#wKyruE{E|`S3t(ztwmS_6f z)Ot8tyD)`W!_XLW)qAqez@46F8rGN($q2zVm`dPc8lh+L5@L>%M-!qoWq6s!*l|xc z!J=_}Z9?oag;*tP*LN+ZAdsm&Nn?`N)X%<&= z(*edcq)*w3D(aYC;g%H|a^8m5#1tYEE;hJ#Rqj{UrH>iiC1SJc?2##+^<8t(*8GC>xk;j9x1^|>V;xv`AF?ar|ZkmWNrI~$b8;Ng0Hu4AtKQgD}`GX%)$x-3#?E!$VU z-X(&3i>S&hLiX(JAIAMqXpjAxKy=R3`S;Jt|11kRWghRB#HtgLWiwNP36oJA@DSY1 zoYY*?o!^}LFpk5?mk!K3Yj@v@#E|7S(j^gJib>9#;y>$Z@@fc}whT3BU%QHm(i0=> z*9F>#n!qxk6cwHoJ-f-&u-*)g7`(ScGgs1}4cyQ6g%Zw^jmMDn z^3{T$SErs%TH)O^6;2@nVs6G(JK2W7O^f#ZXEpkz@5Apl_^`pt5DTtPZim1b1ceOuqg^kApiP1h;~VqJ`LQ$_wrgRKPjsyN4PQ*~UOa!MGt ztg<|`t`Sk0(VXR_N=}rmAs^$dUB4_|>KMt!YfF5-8N3@?vA@#Mre*3icO7dpc2{cD zy-6^UhC0JIjq0WZx&(e0{G&*yl!AB*e3N*~OsxF13uSh$CK6tnR<>r0bI4!bW))v0 zxT#6#U_%;)+@z!}y=$DlWqN{|+b;o1%qsX^jb)j@qCPvLta!`Xg69>yO#$QGL^1PC zuYI{R3|QLC;GO|DcNnj5yup_A_lB#3w{Oea6dT46?>{vsl^~bFvcSDaaJ5X1eF=r% z2zRCjTiOtf=q*nU|8xQPGHsM&B4V$JsC4*kS1jddDgfMNIk;yhl3Wi|RCNP~>%!Sa zQt(I$`kMr*YctAe*r(Bsz&>RpZlaMOmy7DM;3@is{aoMMmKry*#H+XG9 zGGm#OgfTS>BV_N1dF#c1o)dMGuet#Ho#2oUIok}6sZb6henS3KisEo!bA?Sc6%!Ex z*hEtHZ5i<9#`!25sHpK2q?Cx9XL_EXPVR6ejhMG)EM3;mzB_62`C&`+*NgU_+wD9Y z@*!77++u~`M%$gV822P~x(9v@?xRC?VPD_Xy_W8*U;2!XsYz&0P3rENu!pg(omdBL zk!`Cq!wz|nm$#t3%em9}ONcmzcF+5jSgH!ieHPEhmSGcxG7pq;ICU{LebMPuW6@Tn z5Z({vnQYy7)XBgPMXgeUdlm68nV>dzhqDCZ0d5LoO1E>5PQMi2+}(Jc&gYp}H;L=+ zU6|z3wC=fvgtU7K_B^wBX3 zM3_(Vq1T|CgL*gq$0uyt%6~3S+d8dCbm>h3xb$niVvJ41z-8(1Tc^KeG6TDvA74)T zw{x8$`9&a?RMQ7FiZQ0$`+Krf4!xEYqmRHYC1d_vg4YA~EwM~ga0}5X2`TY&*NX1W z_DkQ;rc62W^m&egdhXx}Rkmo6tP@mxADZ@kyUo{TF8 zk5J>7&zTVu#w?SEjKrI`1nauyHWTZZ(_dSfSRdJASYbOV6HXu2*CT`>f ze!PtOrNZHOXfL?VY;JGHsK&i?mTmCZf{_!)>0B{H_*$L%75Gbpv|NMBf-wfAgPmgr z2r1ifCym59G-kS+!aZoPHdsRs zNLqEtM>B?L2W53CvKObUq9ArR7MFte{AM&)6%FGu;=`36;2O!7qHf8zIW`NU#Y_aN z8td0C!3#;9PA&5C`?T4$`j?{5%3~HKNpiQv)LfG1apNH@7mp` z6uixxhw&XOatH0zxq5JHPO}6*m}7MvNL)s9SL)gx+&0z16Y_Mqpi4^*HW!8GRch0U3txL7Zq4G!zmb%#~+mgwuJ$>9`o~S)7Cv@OT>I zr3q2S?c#VjIcP&M}mB&zD(ChGqG&+3;Rs%{~X8E&R} z1{t^?wwNaxL?8+RBHYbX&k^Ba@3mW%E-AIS=IhcOSq-`kzD8%W#E(^s{Ifs{n|hkT zmpOOW^U#*y-Uj0#3)gZx+b@$?6s;C*ikfX+K=~U*oD{ONBzP9*B52RxM>h6uyf004 zo|BdEAAWP^n}1OFio^qHIMS_Mu}w=j35GlMB|rbNHrNJm9yJ@CEIfooiVKd{Hi4QKC?$t8&O+`EYx@eNK zR6QH#YOB}U^|Vb_Kz*uc>otUV7JZ*bHug4DP@t*SUr*=P zBd}$#ABp2D(O%M4`Hw;S(THV`?Z=H-P^$BMCt@T%-iiHFqfv7mX7_4bZoF2bm!SB+ zDnUEt&-5W{-W712Mm5sK5_>A;-`j+7~09QgnX!AgcDK(R2yjDYVDCji{OMAlYE?&JoUex&(7M zsQ+CS$Yugv*h`{}5_&0L1=zaMlRS7+xL!J3i^0A-cM8gySQ@;>ARX}NQY6JauqMTH zUEs{liIGMK$=ZTCI%&$Kblb-%eVkl(lIOESag`SNGvh ztpx5Wk7;hw2wXGnw7+H)a8ZZqO*A>{6Bk^J<*j||;uDz~Y8YFwIc5lQ1i9*Ee$N8$ z`;g)`{Z3h9{lb^jP39JTuZq9g^f`5kWGh0*G$u(C3)df!4LTK6371EO$)YEAW*``119U?k&Q zHGa+k>sy?K>omrvM(DV@qC12seYjq#z6$l*vJ}L&8DGsAuZCSgJ{zS?r$&-0K9NXh zeLw^sLlc#Ih-JU+Vg($nQAcO_rTu#|!ISO}Wl`ud))?ADq`%MP#7K+|KVtB_2X6*r zBw9#gvrfPD(T!`&DPEIaD94B9at*du$6k$mb!r&rqUq&du6X08 zIg3GgDx!Sc!FI&#Cci;Q2wvv07BBw^b_?u37U^a@qr+XaW;S0^hdDB3l;;*rzL5K~L3rUOI2? zmu#SqE|}BJTx+(FU>Y>l{LWRc(z)r`)c5huov{YRoYG|t9X3}irbdSWS8-VtNAOWQ z-Hci^C9tkWs?SWEeP+;f6wY*I<6Vra1S>9W-U!}R4@w%E^nsWfS9U&4XY6`AkGVqb zS?;k~jd*2xbaJ)nn`iDBBJuMUK^;9L9=!L|<0IYR6+@TOakZzJA9MVf15 zJJz5cy41JSDLHs7gLWrgL;wAfi)}4}Lwgy-2q~@|~Ez$@iq@`Hy;N=n$t*cOxTbkA-W`1`U z`RYoLR#`6BkXY_}yjrIR&O>O-l=NNNyd+yU%2fQ9c}aQ`VQ+0l+!99irYb#6Oq+)i zALEv==uI#VBOk)1_M)&OT^j0+B7|MGEK1y3#9w%Aja-5UiG9q}vuCjFke|>l3BvAS zymQc(U^RGl)0AZ5JKtS?cqYc!rh0$46u*xyMf8=Oy6X^m+=!Wh$t@qo+2H+vff7Ztb=I@fho)JVYx1+rCZtLc#(K|D zXbs97=cW-{U293ZCzPgWN4{U3$6ckaP7))&v;6j7KDg%PpHLT=(a-Bit{v-o3ERv) z$ZhaA9RKRn+GXoKv=&&Z3Xa}oO?`cJuCwsf3V;61*s|WgZ_4S0CPMAhq>*ME`=Eae z$~O22VX8GzQ>f31=!X^p2GzwRzAoY~c5oMwF33`EP&9(spt82MHQ`qeZ4wkadN zC)s9~HfWK!n{#zd?luovINmifw-i_r@MGCvYeBx8aj8pQiVD|y7~?IcR+_@-YvO>- zw9czZP1SZM;y|lCn+b+9&bUp1d7p)dkAJy2m=|;Du$|xTFm>_dnN=|J417pOkd|+8 z?rbiQ5ZxzHG$M#lqqQW7GJUR}V$u5V;i4_RhqE2Qqjr`yc%@9ND(>wqom&stt=AbR zf6N5i=W1bk<+aAMZgNaNWjr!|^a|2hYLs61V&u8jEg8{&bt&gm*OsZNB_*8t-njW-NEG z-LRcq5SB&A)>!1X-p=6h?(hoMtCRYmh1BR~Mk?@dXjMp0FHkqxhB8gwHx>C2U(5`t&L9DV(;6b>=LY9N{L%Z71T;E+s&Vby$8Jp?`eGtpBH3t-bvKOwsf+?pUuVR zuNpVz5P;+!lhf`fd2NPtd|}qsvZ95;J*|g z)5jc}?@lcu$%Vu5BSi2@gB<~Tjx3)35=nx=sj(y{?I6KEb&{oSgJ;RUo)G$l)*ISc&NXaD<>u{cn^BTgjjxj^BriiLc zF&iLZ(Wa4p3MC<4ZzUMo(EHIM@mey+Pa_ts+R|HMs`3sVtx?9nw#@J(8{c;%>U@9e zM4x&QK~XAFfr~x@_|th)xZhfdMV82|RH>e0au6EI+!#NlX2*g$73zDEi)ddJMf{b5 zYJ+lVWa=j2(s~-D1)te?-$DLQ=Ycbd5ff(PS)X^@3LmPOXCnOPJz9H+Eiey7=KHddo?~C z>geQK`3R|NWqK#^{l})U)%Dp(mdQV%tjeCSQ+s+l1qIXJsitgHCr6U+e`;*nFyfSs z$6d?{aPIyu@mox+?RU!Q;m(o++hm7z29KBC6&uDmMA*EOr1T}}>_|fe=GC0^()zHi zA%afQr4=qBYNWw320xF?F)~KbmSA5YyYs#^0j(&~`1%NZncjPY_ugrH=Vv$KFQAOsxW2@qN-n1%rt}=^5E`d_ zM)4_RLLWY%DC!v8Aa4T0 z#1t@nU_^30XAV5Y@UF49;lJs3jgXCMis|xV)S<)I2AnQ2isB`Mv6=!w4DK;h+zUO3Re+y?kPYd28wsDARS3{#|bB&#Y@70V(m$w;<3IF<0 zjdto%=vf*7W);8G84Q%r;e9k34- zcS|2qDc_x?b**C-b%rSfMY_5QzqJ^3h3h+0!{m^NIhK4zkQU&lGj`)05~{AJrg(M@ z-bIs#bBFuciD&S0clI!ael=L9%6)xmv{32Xrtf~8!liVDY+9?-%vK?N1zd2fhW$~< zKLt19Hh2f43QCkaJg&ely%USg5%JLKs%F8kD0)V-&x1J!X*HTTEhX9enlUa}O6``V z>sn0^r&&~=3-n0_d=}^WQqt(7>pT7DOgCL-cNzwLb12Te1n+%Nc4Kp=-@#v>9rwgX zk(l(+<^=W@MKr#{)EcxiX707ZY`%rVsZJJtb}Y}rh3Q+L@0%wTD0XP-IvzF&lPdNe zo>)DxcjxNCXLfdTN>ZeAS_ZE8A?%)xo^WbNTTvKz8eHzQG>E;CY5XzaVD~E#Gb(!v z1?e1S97*xzy=dK9CUN$2ZCDxnSrTc$uL(XXT?OTNi-PFQC?gk_znIV zNg9bTG4D^)1oSZr-g|cX$nnhK3Z8npy7Mn7X;>Syx08=VI>n=~*Wf%A@OC}4dfWOS zRsH3GXs%;~)~{LbIEatolcSNV1ix(zYVY)p1lOC<_kf3f$hrn!izd)M@uADqLgfb6 zaLzuY*PCfk(VXo{`sa@`bv;-?hcpXq4c2Vj zFJrvnTD5j)yYvasW&PM!?SYoi-km9+&bRj8`4%iEW{m7;dVvF7wD%eui}LIDSCadp z59+Fpa2fD8^*M|*tBWCbgx#usYc}pYQxv-#1y>W3dvoI}JGBM*rOzcNSZwgs66YQC zLzmTEbZ|x%zrIAU+)&TpUEy5BqaLevk9qFY@1lz~7F{IU2wv6r-y^u|BoQJp;u3HW zG-aNWZ;k#-2n$NqLEfAh%PPJ>o%!7PO9G+3DgV6^EXi0yVN=%-2`*JlE~;nB-bi_{ zZH=G3u)kq%>KJUUH+mPOrfEI{wgP;^SDbx06mGLPwG= zAaxa&F@5GfoisL`5x0v09u8X>_-2$b=^JE`}+CC{5{seP|N1yHSe%|23eW!G6iwOAGE(anal1?EBF8xf!pyrrNH6*Wld+ zped`KnXAct(5D@%y*09Bk}Z2yfL9bFokN_Hrgg+vVChoqAIBi!@WZ&D!Ptjf3jupJ z(&~(V!1hb_yJg~qn`t!OGSuDiHBr|=vmt?A3kuFP2LE^&$(^*DeA(#IdS7t7G+|s! zg5~a7Ica$^ZGk!HL&=9`qAFUR8XVVUx($hhyTO^JMef$Ew^gl`>aC!v=)qqd{`hiT zCfV>G((){QXb){FB5yNjdRBc>U(ar&8=iF5F|fUApZ=M6yaGiTv;IgIDv zdUl2@AHi|9KP&gsXcMfqJr&J+-NyPdK1R^1aW=V3%FKhA$+-AEe{@{2@pW*I zBv9>SPB87zOp*O+69|@6XjmWA36i>dC;l+@4`bcF!+qeRIm-cYI^&Y2;6z3AaT&2> zK8o>pJKH~<{QA`uU$_|ygR)G&c2_H0i^4OuCAg|_W>?oZj5_yLcbiFNY3?i~n&+=# ze3j0g6or`Y;6mB;V{-Jjp@TCIjg!VC&qZ?i@6&_=G(BA1^`WdKc!i0CwGHl~FEF2p z+WH)$2$@+6jMb=@9xAh$MmU7ywA;9c@sGLdS(c&EJgT^}1&ardJa<4{EQ3E99JfaM zpL6wLu4KJ@8>7$6gCP~THyx;DqJWo|f>>JC#h+VXm#9uxk;3y{g1QVgOFY*syVn`K zEZ7`AvhhIaXKlu5madrBI0xM{8HrW6;iXC7caU(Ks{c8q86&7Rlc%PjlUqgH-4%kd zqGINDG+etfW)v>pt_q(5GN1OO0Uu=$yDWIUjo+(_@6mU-1aD;?{1+OP*$mtQX6IFc!-F$vj~^Y*Lzl>s68X~Vt9Bu*8&W;5bA8ofdijG&r5iMfyl?JB= zEvuVrbF`R|7yEfPQigxJ)7rF`&P2hy>ASx@_}Us*?rf3x#)j7dW7j^n42@wul*_OV zA$_kH)X=%}m}IU|Az#J@6q9~vw4i+r##`_IvQEHP1gV8AwRXoYhWiC;$Uf*f5=o5HmX<#42- z$;z6b4`bD4hx{<&QkU(;IFlx-Ilx;YU!B%<#$F$?^X^yT=t26>gxNFvun;=!!`kBZt=X?j}p-heXG-99Jk=f{$JaNQB&F?~RwV-yBjkwQ`J_RwsQEQqB zsYpm{LwbTcg8T0Dzu@b3wnt`LedboEu!ts|NC%EGAxpQ`f~QGC=2v8 z>GC23muNGVvCkdF4^@^i=J;t!DgBz3rU;ngt~lX9f9XAf?GyG}IrLwf@%WHG9E1IY zB?bFXp7W7i;y!}Ai}#+E$w{6}{7hi`6fVp$N*wZ?mpkZV;)-XY+uNl6U$Ea7-9089 z?jf<%Sb|T482)c(!d!^cF%#EH$p4;}fnCmZ8QUo}`BA3b=M(%&{McYOQMJaUZo^#N zvQN0RcP8gwc-fRhyt*F7k)3PP{(2zIK&o;Vq*VpM-KP^E1$8&ZpZ|&~wXRX#aOck2 z@%T*LO32m{%fFH}wgbu)T-9mAS(N^v!+%!sy~Yd$k_xqFV_gm3L%Xdk*TMJ5?0ahR zy4Xug%Cckv@$_426fZxt7|D*Dk~}>!40IL$2xsyMygoC;7A+ zGFL-^_K)M!u#Wquvq6NQNv+{{FvB<=F{@(+^KUh;8RTb<7sS9*ge?4Np~wOUa(7- zu!rb<-85|)1b6GSUpgGTh-ZE-+R7oCvLmQd*ht_H9K|5?*q zzu9!HWGAoA-~OnizbR<#o~rZQO(NMb#m{r9N}hDTQ0~D!=HDOFSu^)>F?A`%JU5QX zxeuL1-9lk7B`9e|3x@>EcNp6%QAT5}N`>rWP8jzfFTuJ@0p=8)rd!inpRAar2fhvN zKb(87#N-=96&$O%Y^3XFX6Z(pPP!Cd=`kCYNd{ep)*3!9QD4NuuzBvbq=|vu=ZfD)(K=KKr`nt5{^?wu|Qzw%LtV>Y-G$JRCYj7Td^wbnQ z({u;nq$ThXM1ze^9mcEAESjmGc%${3&B*a_!}1Q+VVpKKMnyWab~pN0rzVHh$;F99 z6kA`kraRrZ%Ajvu;a^{gfBWCTkH0G84Dyu~Px;0;cgOdE`yg4cUQW6aHALT9vtn~= z)sDP$?clGjN`{!fh#S|Yk@XVBdZ=GjIKTO?UT748mYJL3dD#pec!=)3w&^mVDTv$tb=lEGI)pF5z+`O||`D)VGogAIo5$R-`&74Id_iq)k#>=|OF9ceQ=% z(`Y~7e01un(dpyM;hl+7m-xOpCe?}*zGjRa+H>&eMm;t1QW-o0fQAmn?c0i(oUzGT zm0Peyo66v<1F~u2flGXnvpnF`@5oDmhOQ;(o4Px9At$Y=z)>cIF37>W+#7RJBW=Kz zV4I!E(v!0gfa;8u4Iw(y7t7^(XPjBt$ zOLUqgUJX9pIyk$@+Lp?o?Z($;e0>DI_uo^UG9Vo~#Lgk{P&E7PV#4?i!w*%q~QiaE@42v-&g!G9k_F*%K2ihj0+(5g4zcDmo6#G z{I|WVfJx+l7&{#D~^;eNOJujElWZx+D*u$$rx0 z)Kll0Ijk|*FXx{X4i^$Prbzf+WL;fa7ucwo4uq@_x$EEH*GJIz&ej~CE0VB5bSPJ% zocdk!t$V~SO(-qfZ%0X8$Jy9oa4b&tpL$_mXYjQYYVnS|g|xeie1UmQ{fC zqVboRbDd7QghWk!rhj$tOUO=H&NQk#3GNoO_e2#pDUNM8){Ltdu{!J8DQMiEy73r; z`c2eB3fa586yd<|-w!o|fn7q}mTb5jD`j`876Hse?B zoOxhHa@}>)B<3<%@p8|ON5*GO8kM38H+_A%40RFUU4P^>$JZ0P$cD_C@XBNU<*v$lM#65>ZfJ9_yxfkx2kGjQFYlzJ1j-n5322HP9W(Cp zUX|dtP72}6MV&KuSjV&x!uqqx*7KE!Psc7Dgf{V_QG?d>Y~t{>bUv-Jp3dV+lsoa` za2{XsV^60T3j)&g3N{8;QmWpPgkY|~8Q}HX?O?wfHuZF3|)nPb@6cb;Gnz_wuhM=e+0p;aUTiA#hL2#=SaL zv@Thyny+yN^{w2IyUc*30c8w&*UI<~6(#2|{^}jOm2uJXkj-|$@=hYa4m~%;WJwk4BlQ?ukD>Sfjh&rUfG4ma*q6OHTEMCM0< z?MJXL&UGk%r;ownnRRoD8;?)OpPlx3^ZCm;-i7>sB$(^s^_DJedpkcZFJPt=^r z|F&g*wZ^X`>%@I${_PC*QDvv`pk$3u$J|dW{Of>c?(VEg!1rF9uNlr_m%N6L8Dzsu zeY<9oQ(|)Zo5CnsmME?b<82yKnn|D0hLP8SJ%a9&w?6a**rc764D?L<8Z4i|pwqHb zW*qJ}4bUb-x;nf^=X}cpZb_D^nzaV#h&0!ROA-9dC}`Rhc1zgBJjrI{WK&1*S1~@e zAm_j<_?JoSF@imJ#v_T7-h>nWYgMMN$9IRL1i_C`Oe%{i-fK!$%jU$)X7@|-_3kro zAdGgY6O$iRpLLoSa1La*yVY%F6r7aWMA^E?S=N-H;7z#*W0DiwnEHe+Ex>yhQu%Ks zcytv>b#{bf1}K#Yt*9me&BkD_!4)h#HK5pl61g)d}3vzbqxoTc@rhwurMZ<&*4%Vzhg_3lLwyhBY}CLmC%p~7)a8%-+lZoZ?2JiH9rgqI72GzsemJ|s zkAUYf@T1{%@VE!ZHMq=aB{7OKZxTiwP=s5ptJorwHzeeEQuE;dMF8w&YC^g?uO$sS zHZ>k-@)@(J594qr#dR@LRg||-obc-vtpBs{x4ZDiPepdeH`uBQK;vWDHM8_oBg-}7b-Hr0&fe6f zgP}?9lyCj4chc{gX6Aj0U_FidGW>?+6n$|d7`t)pI)nB{r+8z(VE-#&U()yV`SpCq zY(|+}i83Wz34V3s^-65jS=Yh64n`V0YQjR=lWxMyuT5u3M;5nni{8_*sU?8cgBps| z_Q`yxx6)Pt?gX|@K{N29sf%eQTk%@;`!<5J!LsUnSgK;?uf;ep;+9f2LXMwSox8yM zDY0uB#>bWTqYp|QTtn-ZW!lMWk!vOwg$jJSnn+`=*@rj;ZD!}JTkv=o`R&}ND(B+P zwiwUV*xtd>J4dZt9@tV?OJX^l73i0MR()4-QWrSOApbkgk(`6UyE=S1A77a@86zdq zH!*4^;KeN)OgU9W*O{c`S!1TJ4GCMOm~K53A~PXpvYr7Pu-%>IYIF~3ky+ zJWLnm)f0Duu|nRbdZnAx2{~!>v&1)H+C0U|41Uol56 z!C!wG{^!86s$)?AVH?4E27c*~U#1;zOqKT<@+Tjz;>VNXi=V?thwO8AapD8g4}}Yr zQ(pMPwC`Tt`jRcAluoAojfbpcEwnePCvAztH1odTpH*@-uZ;iI= z&;@=!41d7(P=sx*F$1FXtZj3u(z7ND*9w;_cq$FUZXq<|b4KYly^vRzhqW(GdOB^H z8Sf|LSCA$&=w(TPHQ6{^UG&ctmNGbs(@Nm0mWYQB$U~VE)yc292M&#auPvsXQ}2h{ z{5=VcS54ECZ!+>$jJ*bV{V#;CsJG{?gmnFK{DiG_dK%PwgjNaVng^Z-*CIk!8BUsMc_+q^5@|@k(z@Q8jIrD^&!7bN zW6+a6qY}o7yS*N_Kq%~$@Ssqj3EBJJ70IO6|n_257O?GRruq6XNlaz zgdw+JuM<5Si8~7bmfw@h?rI9Yt(_&!jz3BG`(Z)xXu&&>6aqyo3W z9fMXI{>$)@^?A%mVhOF(YKO04tjSr5Y*;z|wZ>ph0>{PgNmDXuE6zu7JqA3a85rMx zZgt|*dH%yF=TKh5Pow=%9PX8sR_Q+2AA>))!asi`?n~#hNmMU%xC@_q&V~q=PM~>E z5w7+cMruicOu)OqSJUb^XNm88z}}Qd+TPNC3>QQyrW2{Vn$(2Op5En(=xLJtifo`c zrcE#u3iS+GXWvJs_aQJ_sKJWjxI@`w_f!I-I{)_$?`?3jF6|l4&*A(?9V~6MH`^kJwShckN1z%SD*a zI!9`)PfOWp21g&X1^!{WXg`v20dU|n zwrfzPn$~A7c`16p#!Lew8&@72c~HXna0MIXRhY$JYm(?|A8giHYhpR4Xy{a>9D|7t zhOgCrN0^T9M6D)&eGj!!UM3BzNs&Lg1ex9DKWllcaqxhxiul$CT#=~mlyyKBpFLv~ zf|P?;RXnv{A~zmw@G-l~mc%zG4brbpZ-ZZ3;{H=Jbt^ig))8!VP$v|qF2<`!1h;&T z=bA)Q-ZYZ5(9XA+ipeWU2v;tV|9r@=&vZ8y;M(R+F$QN2(jx!%ekg2cY1+xwGE?X# zGvM6e+6HMgo{N$e?`ioVYszD{+L?Sg`>Eo$6==_(KeQ5f+=gAwtBSIR4XjGA=}n!Y zn}nbFDZ7xrg0;g}o6*zj{66!mqjCL-@wW#ZIpCfP+L!yTcpdES2ucTf4+V!)2ZNc@k!`6k*9V#B4Ce6TB zT*y-(ol0ms6Fe7Pu5D)9e<Pqr%QR}#9oW7Up2(wO42?{{Rnkg_nh%QHv? zS{j@#PD$EiWf>l<)hIR5np6JMSk9R(KfCg!2KVOlKb$CP2J+$Lqfu+;csD-7_!!Qf z^%?p%61Tx8y0HH!N~g2gFm(?SgLRXg@49BT15*6I*Uqsu*5YL3(%9Zk`Er*3g!MhR zXXwnaYh+Xm0c2hYUkqCY+q3bp1b@Gs=*Dvi=9_>)qz0YT_tN+wOU*up86WG^U%YO;<^t0>{zyoyRW~c&kZsRAV+3Zagyl zflJWZ(oeq?t!yA>lE9PuJQrd3a;9-e9jJOBoOZfX{qDrO@xstAQ<1y&3!F7s(;uGF>6gYD?jo z9DFn*ZdXOsREgU6sK%#CI)SgSY=gB9+S@tbVj>0--i)j?vwW+gSlXn|&D~h5g74&j zN9%ZkJ164doV(MW`g5+V(Y8WI)RB8YZL((#8wzd6GmpB!whSIi$Ct#iWOY98MEcV2 z)Db-AT6CGTqRzYwV|^rA4i0a$E%bo>m3e+Ti;L`5s_1fKP9@@F(3i&AgXd@B{!8Pr zD~m{VJvt*7dkdH)rI5Z-cE2wj_mUszOYrs)zEL7h5vcgSmx*!|c4XOc;8JMIUpf8HX9Lec6@T4cxCv!a$6cZ$*ug>;2r(B78c3Fvv z#=$<|(Sp^*uzoKJUD`v!Asg}oFJoXOGZu|T`+&y_N}r?Asgjl*M?Y(%$!I{zU+XF~ zyv~HfV%Q=UMVp;gnmqJ26fK)RMM-;hjqT24Y|FGJW?hc8NC0}im0xjhL0>hJ&#%OC zWO(U?bo(CH1r6gqjas#1%PwZaD}SR$ofw@Q&X{WJ;J<)Yd|*!~t1jfxLOcDga2KDE zxOFpZktInnv9Rq*<6J)=pTW3hsF4|UW~TRrS%j!2N~S%1K>(Gp>;K-8TpYQ%aH(?) znz}CMF_cyReXu6i`1f<`^rQ%52?*srDHz*B>i~m5e7_2Y3Tr=t>+YP%u&1-FCS~~* zEMw^N*pn=}S194H$r=~a(w7Q6r&#uG1FxN7;#9mCxFtj~H zT16imlkV0Y#N(Z-Zmx`Pc*wd zIoY#D7iZ_3OqF$Z=;uSC*lNM%&f?ky)fk*CZ!g7R7T_;l`H!TJi8v;Bq!{rX6hNE;rS4y=Q|H_|f1X1N_}PVR%{Huj`Q z^Ry=J>eMkcJ9Cd-x-RwMYTn9iQg)O5hg4s7HrT}E6jzj6R*CHj?n{^WDBAg5+aMKPRG^A$88e%?&LEIJ$GT0Z%4^H?O;d&2 z?b|V<3oT7ms!h?^?DAis9qTewk+~0wD+YL72gL`m4xZ1%{!$Fn_=0{*nA9>{4`C-g zjHOB)I#y^dwR6mzf{MwWG@!&_6coAm5sYVGYiEtl-%{Z-Wv)|!L48`nxCF+)jhN|D zx&%aEx>gr?aPK@8xNpO*Ae}~D;22t$_yh*!iGPd)Jm-G=`)74n7OByPOW)9%DqtpS z-CZ`jUW|KFjAmj;1XiWeFI@pB!M6f?3I>YtEQ*$_J$M(Tgl(6S!)$eaCF5&{)m6p% zj1JCIU^lYT;tXj9@;RW*-1HW0TKZkv1Z z4JhgsbC<}?O=H4=zLVeDmo|l!bN20%(a`UpU79?^gbAHfC7c}moR-RFZ0q2oblGdG zcAa-pWq*ZiL}ps`RHS=ZT%Pf^Pwlt5&PP+B@Rj8lu{pXvlF1@{&lT9WML9WX zB<`+>;%S0j6%z&WN>EpuK$_s!my^n9evJ7pC|K#-G?^`zoEj^dsNBh@vuHdgJSR8P3NNYH z=_Zkyr!gBuXCIRrfL5q0A~M4Ph7Wi&y}c~gwSKAgL)N2iz_%zB}VonUDuU^ zdpa__i{`hk!N;OTp^sTKCkg%xO|U(5mvo0~gZHCzZJjkKuxbrwA+$2+p|$s|^Q>Cp zT#akfsxd91ygptENXw_rnOdP0#n!jEvUwcBbze>EV^(3J-&qbILk-6oE<$Gu+TGzj zaF-V2@4J%@$A3WP|E=nZGRI$CD9tg0Dvn(qRgw|aNkeGIz8dXAD&M*)C)v_e*gpdn z5w?6*ei%Fsqh89KKR4NsBF)64Nm6=8O#PhwM#iQR#IAh_&8W?=k+3qzt}mO=erAoq zm7HaEZXUty&imAh_Fe{k?qCV9sSIVN&eb-BNwe|~R8?ad^Ioo^puIB7kyyPmJV^g0 zHKTPXFYsT9^({v8U6kT9O!)}0NQHdPLw_W#H%fD&PagAB=%q=Ri%F=<<=d_^$l=N> zzqB{Ly98PJBUpZDjBGa){m>HPEXK7PC=u5(j$X%GxlTY`+6cSF@#Yj^yHOIr240fL&A9cDtsVl(d zYYbdvTo1oz@d#;B?1pjc-+7;d;~&AFN8(v@ zz$S;b&%({M<7Mcw_eDu;=`!v*V_COO{zI^hdOq#S=e8)c5X4?2ejQ8hX|*2k}a#BnFZbmtU$6o4C=0MSuEoMj#Nod<-dgp9nb!`$~<94h%5wq!;O8(Sz z297kyK5!4VF>Q#avQunMqRSBNk_mtEnKnC`95ut7uenxm(`4&fVSP%8zy4HQIbY8D z2}>QqtzViTd}ZUErFl71r^R5sWSQ8yvoe_ri}5T0ui)&7D-jZkztliIFIh?Z>iFVvn~dO^dOKTGWN(M2X+G4n$`7mWp*#Fqjr^JTSQEM3 zL3_$dl})1udeG)bA!foDm-cIsgX4#)msm7ryoShD%)_xHbsRRw5F<-ev+qWEI}PJ= zF&;^%(Lf`>nkAIh10YpqV4UupCCGVD0{Sgpe!C>X2(&|LS){NL8+VWx4isog1#B|Em2>- zWivbnbW1E7$hHjL6#bjG)Bb?#^D21^9x zWxPLSN39>4oRunE@i_HpMiz-|J)L~XLN-?8{s`*NLC(S7KAoi+en@<%365-5E3#77 z?4&yQxCfWym%ZZJQGJp6S^rcPGw9y7ZzPOTv~QT|gW3KBy(}s~2LGZEP`V zmQpu%3$|O-#(zep&mAm2yT8!XWpO<(c@0`omVlqeIEA=wMLMFkNSHa2k!ogr71q;P zFTIoZV(^50)0(tbCcj@DUv^DkZZug)-FObiM&jy?cWeB56dsQtCrt{!Ssr;>n4=o$ zmn@F!5Lik~5M!AkHf<3S{ZZkMo|HA8g|WSgaU|zl17FpJVrr&R%%s5v=i8|63b+ac zBfE^H>+kGYli|83W*65*&cr)cpSbn4vjXLnxeq739lHHS-=?8Ge_MllV}=$@{a6`{ z#T0A&B}%G~IprBzebvSAEZg+k6jdL0;BN(o9b3$a$2r)~ z&dX(y++D7*x54d`(i*UL*CeENMjxyp2iJWMss-2LR0perID`B;I=w66a^x8-vxwjp zXrEYa9oVhFXGz#1d}h2gwi%&d4cr)JznSPZ`O&Q;*d*Y~x$_<(Se4Mo=<^Tb+`8f) zPl0OckdnF-xRyb);KPG$mF;32iFS!ez7$Oh4mV*?^TmEA;>`4JBsv9`(CGxbTuSYL z%ZBo&mmog}rONwW?imw0JDQS&x{Gz-tEK`{t#{sArzT~;@6~w>X?y?}u%Ijy)wi$JMt0-7Z z1i*FT6)GxFn86nfJ)aEiE@P2MCk<%{62`a&<5FhBXM$h*wAIZd%i}VRe>lrGl6H&9 zTo{}!*e~PfKG@eG8_%!A&#%J&?eBvhziy(!@ko5F#@C`^d7qP>JP$TZivMKlAg*cQ zTjy@qBRI@ilEiw=_4$}7(zrF}eQVl~P?7kx7=JMsh+MXHV>z9p*>uzll4)X9XN317 z29J;6b4y$VcUEz@H|KFUUoVB}_$39to4ym@b;+I6zq}vDA9t`PP5xdc0iku}M5Li9 z&8P!!!5V|5$OnBl{r^*jniKCOaWA5UY{^M3KVR`DC zX#~IA@wf9w8LVw^5991=l&;>1k|;%m*qvpc+>0|vl$@z`@Q-Wo*qyQkYYAEQW>etP zLP^XvV;sTWE6<-Hi zQ}lE_<@l(Fh@asQ)7dq$QFjSdbHIHM>8pBhdr+^=F-ltf4F<0|(HOe{2ZEz>!ST)&DLE&MpX1%hE3tla=bd z4s0EY7LJ+YKAFx&kV}x0aJKEvykEw<2d|>a?FR|J;60;3M=1EEJ>Z-c$Yn^xV@On( zn#fqA!;9eEzxQzh8g-XgB(0sb%phr-X?!K9h@cV7Loh3w+cErhg7F#{(0PG`-2$pqTcL-$jkb?}(ev>P4&Dv^-Urtu+LoEIL{KlSM((NH zk5&81mO8h0_DJNf4At3hBmXk~{eQ#X{<_Iy@O~6NKQvaoLWgRE3B1(Bm#`U)9jio1 zJS?A;&TN4( zNsYcrlovBb*Q&90lBkl<4(>og&k>VG|d@! z71+mB8sj-(O_xYD<9HO#HR&0jO7i^7_|rIAr-qUoBfGQ{n=?$bH&S40$k)B`+fq4- zKFhkus#{X9WR07w%b%Z4b?oQ^Ef;6hT0qrs9B>4f&5jemS5mj97>{CluHq}!nhwwAu*{E%*{ z4oy0C^m|^e;(d81e!LRTw+U7dwnxFTibo!j^wx)Pz#onzA^Yq>O)uo2|(R7_%OLG7Xu*>K7N%L4Y0kig=5r#RKf z1a&i7mT$99+-$@=EGG3U9Ih;Z#Bw>0GuY;y>ROF>49=xf4RwXfz_*Suh+$mAX-H`eBCRf_K$*|*Zr-t|hxY1+@&J(aRsCyELjy0KQL zRE^qq55|uM`|QYi!<)j9`aA?OvTOAQZ-trhhr3eohWzFGVmu6%N6>cdi}ROJKjF1F zf9~q+@5l^;wKRML^-heLKhVv7cM{cv+I`-^@(S)Rk=}aIzB>KT`er{3{u%_{9h8rmRVf<=$-7!(Kyz`doj*MHn7wU|C*RsgqQV&iIg4KrnJocG}2r74dF~% zZpPOxL~Bvew3NZJil5HzaGRh%AwEF9oH6uS+ec8>p_SQ8im_p=U7yK~Q%vxZr9n(8 zTcFp zgd49l@wFvlvEKwmStvCE?z@0Sh6_O)654)8`F4Y^(XlHjFs1}!K`xaNlrTOUw5~w+ zEeBhjYIM`I37f2mqfiKF`@Q|>#*TEISxyG)?{Gr17uA=^BO$|$Xp8K_xf&n0 z?1EGYKx;GFt}wD^GX9xor~eQbt0ooAElC4pCfXnujMJvz((x^k9)t1GwdY?3waP8A zokIDx1WzOwtW%e>6tK0Ep9(;$m(y-VK1&ZVrw0>P?nx75vw`n0vaq@Lt#xQ*i8o2o zY&;A|PMk3{8OlxRA=}>Lmdfth$EL%62Mb}_A)&&4=?=nO1fsk^a@wwRhJ zeQ+<_SLPLP(`nvWnz0urhg@XYKV0^O9-K{wRT@DI$KCM{BmL&Y&)r_r2YDyB7VSfJ z7wK3R?YnLj1qSjYC)t|T%Zchl7lCj2rQO|NaP0~`+2^Hn zx$uy&t7ESwpKxh2J`*UZNigSHhvWh6R#){k5t)v5)HV^LOXv%GzMb+`Zb8b^U%W{& zpYNa?n#3sbaSAK9xfZB_y(cgxlqhxWgOXiVDbu(ocTF+e1kR~pScR+W@=wdaR?Q!K z*M$~Yu#nUPyB)vad3DM$;E*WuwHmg_SH2F|n}~IFOQdT+8?-J}QU=E6+g*AO*Wt|@v9d1jl>Yymm_7m;C<-!5t#Q=HXh8 zdf>}oG#AT0>~|2BbroEFh;F(h6>_W!+_@HsyziuG#j_jf(sZy)y-;@f>IV{1ThipM z9P^C14Zoc9BT-EU#8|Y{&yP-dK^mElE3-zDQ1kcJZX)f&Cz#E#1RqT~IcY%qaK<7q z+w!G#V!DJLZcS6IWio0Kit#?OKHGAMHzKFR;74+SsdnjhqB&7D9q|zqce$xn+4=K! z%&PP|0OZ>mG^^wYQPB7?h%$6pof4_c0*Q}9-p^5|HLIvwE}_`MK19bVrqT9UY8OM*o`$r>|&PQ{|@4S9%g?3~)5lS_zC7!48ajJ_SNL|dSFr~US9!Vk{v+33U#-pyXB&`?l zB_UkP+O=IaST7g)cOE3E>9PnDQ0NS&E`ko-=B%M;THia(UHQ|Y7d@}=ICWqwCXK>) zz(wM#xQdEPlrp=7U|!Z4A3Bt_$5fwQAxvgjr-0CvPf))Fd6Q@0xBWi!QYZ*#ZE$o2 z0IkEZa|Qx=aAWibEJY&19_oI!w=Vv1t5C&4U1QS($dZ0O%d&C#L)5=_=`@$dVOgD2 zlyq>9P_)2c0DnKrcbna2nHr61EC*yW1tz3#)vBI}5z2KK9lQ?` zofLvgjR(A}a4nh0dUvmB(aNDPoA3Q%%$;uA<)ZmsN7^FAbxng-1|Mh#AD^=Ro>!2w z08HiE-kgIa59-8~iwvlhM{h2=U^nU#l%jRlQk}g`b$Xs>$uKsTmcdLT+X(%=;R9P| z_|5Ko?cxdKNqFp4%Juxx?|3I8tqMru46gU!h|W>8KB*RborAr(Y;IE+9N&fYfBB!@ z{`Jp!;(itREB1d%(=z%Ftcn0wigEPd{hA?GWjd^8zQ=kSKWXQBuN9$9XM`0Te7gmtFA zmAC8#c83IaNtZxWburBR_mR{s{f1@?xGsnx?`(R?UQ?}e-P-MMEwR57k4u&J>A~gl ze6QV=G4O1R#i@t$YC*NZ*__vBAsr5jF_uoOQof`n`c#=@IKD$Gb}0K`X;U~;HHuZV zvD*LBgLmo#!5?3V@=xbX65MP(NQVx;IPNbB3IqU-jsFScQBKs zq&Mg2a5mVoFr(j?<&rdgiJ|e=(}e}C2A}(YtZvc|L~=W{bG&C&TDl6pJHuap79P2i zYEWZBuRq2-aCLYE0>pgyAg%gm+Z9d%E9;Vc#*T2A8HOJ(v;P`V4G#ouwMvt;;1`E$BDq z)xLuYhKfUPfu9{8U779)D2c0N+M;o0H|0ePgB9(Ih3hu@97_)uw%CTwiM&AF27S7X z@-Vgq>JR5Uhj7;dbKl=CIc4EAlhn}*_TWwV`#?c1=f5w9g3P(h!XW&LllY-oaAmc{xjxRq;FC+UAJ9%#@v1lhBMRba)?~(*oOF zGWB(K{L^Y3-hZbo}TT7&&Gj<-U4QWg`!k$5bM&d+rh zTG^}O*sst?nl?DDMz4)>Wd8d1h4SAL@l&0)O%$*X*xt@^fSo!okB6~mc&5%-ovY16 z$WVqyn+Wg`MqxIfF=LF=zpR}3Ej`4w8hxD^4;JLC#-7O2yx3m36<~+gnLi!(x<=<`yeSo8iC+ji8 zJGKra+NHxTHnFP1g4A_5#bkG%rE@dbYvS=M;hzd+$r~(Nmm>86tqd-QBeXY*rL#m} z&56s&w~CN7<{`TUcNw&zE=qtHB@gwkw81FAql*sMAzz7n3t9OfxEA=8;aEGqz~gX! zoX&m)=};=#9ePpn42x+2|9oq5ut(!F3U_kW%^ArUMSfJD4vQ3lZ2}x6*#hvU3B&RU zTarbH84{Gc^dQOSO2?%k>8?vP4pq2UgX^I#H(aCr?-MEySqs-w7SuZV2eRH6%P#M7 z3~~SKtHb5oi!R!21dGpT+oYm`K%9zI`_^sli`xj^3)num_dyx3pW^4o{O^2@iYTX!Kr-{1NVc>fIAM`zy$E3n+odKsT#91$!?Y-$(qmco4N zE*DLm7vZG3MUiFHr(mtm+>4&1D$egjxtv^w)Z8QZ+6VX31Pub(Br5hH``zHbtcj+$ zkFIwhOfj&_8Qhn`QkJ4F+tGBQT|v2m<^GO$*1F^<#(3&|vE3+BtVImHA7d!h5Tlo% zy7JaNP4`irmdv7<$a6R91Un5=I^xo$5I-5(DMw({SuUTcZpv2ZyAIZ#gZ_{X=p(~! zL96l)$K-3YtjWh_L&D$NiMMec@%wi-lZ#>4jHsT)M_*^SO`3nEJNG86?qdxkD^gVJ zC=w(-vQf6dJ!zczE3v&aiq1nZ>T5TSD~Q_3&FLxe+LfLfS^ME_uD8C0A`C{ISc`;4^mEy3VvwRdlE=6M8r5bQvtL76}R8n+oM+ zh>dbK2@ZWXY=wPMx@1rKcPR>eYE`0I60`%Z!^tz`?2!bLYJUu<#>YrJ4+&SVP&l1u z6-jrf`R%Lzj%2Xh;xF{sX!C;cG z@$(gmf;1nBWqmoTf&ckE%2QQ%EzY%Rua%g1URU?}Y>D=nNWT*0OFPx>P#?kjhwNsa zI%@>U2bKl?$#+N%UZ5VU{I!`c&IEhp6UrOPIcQ@F%Qo$`$Exx37)H8=VmD7{cc&hq^^XYD zG@tqFGg|YSzT>_MqndzjGfsKadTMX_^Xqgr_#7|B9-PlkTe^1XO>2oCO&#H1BjfuZ zo?Xb-FrH_kRpapB>(PjK4C@3Nwl_3?URi#$J7_X@7j?5qypEPgzH_M z8~N3*C196uEpAz0GZJpafc6gRIaqMk?sN~{yL2zb)djUQbWt^UxW41m#gp%IBz@zI zqRGZ%qG6-!5~wq?WLb2P3r`C72iRQp*Bqq1>(&`sUsjmn-Iy`En4?~d2OxI6u;^6RVf_@A<3-n+xYv0{`W zvElonuDmp*>@_z^8SH4Hnfv%n@k?#6)LA%&DW$Xp4d=BP{E_g7QCDSGY;PEc);?cF zm#>?#xUoF+nVgGQG3%`z`;jIWwm58RYkv$+H^wuMAR# z(~P^A*0o86@e-Xj1%7-NYuT_o(ZtoT1L6qUIf(9*r;$Hod;OT)oNw+&n__!~p9TIU zy$$z(yYu$JQ3m~IA^+M2wi#R2fxJx4`iQw(&Bh+adIoj`Z?qo_<%hs3|1r0MMsoWm z=FO4N#JqM_4C(bV6l#)WIV%BwbdjU+v_rHKX-_`36H`l%-G+8_P56K zqY@=p(_nM>sxYoZ`f}D+BF-7wQ*=;V&!8=XQepk8u>Egm`xWf9OQpML)K61PZd9WT z?Ym3sEWK$@H)8J2oVFy+f+R09qkB+uBpR$2ek+LGn5scgEM!%m?*w?|3LPo)A0tk%b{^wz~MaJPJTP-gvazk z|9x|;EaYe9UIv~!%SY2VZW#G+{C{txUt<2>&mezvC?PCv6DVXo!4Bi|9GotME6?El zfa5XPW;VIcOUH($X>ESq&nAz4*@E_=wZOd$>hhflp~EfTt|H%u;%Z|iMM^TjI)YK* zxS&^cR#uE$^qJI4lY=&Q!RsxYs$atQwqm^YM149BAl`vpgQa(tA<2GZb;qKLr#h)P(KFasXT;Y(pz-v63|{sudC+t zO$`BAg?;nM)|x4Jn~Pf>)v;!rhkWIB8_&t0_?jt{{21hYaIJ%IeP2r-aznDHz*xkaqp9aTxJ79-DJLR{WJ2@4-DfV>Ik?!ZXRFhq_IZ8K(P% z8R^^lw9?)15z;qZ**RBZtW&2j=IFc{HmLENc~I%_y$>teYoZpt_JcYI9|NmNnRCOo!KJ|44jyW#Q zU3#pi8Ei)FK|X^v@8wc7p^FyS9bCy6e-uU$gXP*GUt`dYti!G$HDre~%@=LR_O!+~ z3tKPVHjFgEFR8(LfZu|L#BPkmlp%jBXSpwmJ!BpHut_#LC2m{t;Qlwkl5>+Iz^6Fk z>QMh0#Fus?eGQ6h!tgu>pUr58SkS9$kJ_`b2EQ$K0Og+ne}{onSBNchKft$O`5_C( zl7O$o{ZEI#q}V-6;`Jd_^0Vp%os?msPI*LDqSP6%{zsymiC)2OeIY+y(`;VnG-dA9 zn4{`e_2pb2iS7R^)c;2&{#(Vj&SpkG5`X^B!q5LTh|f-&UfXMP$`7ag(1c~XVfz)V zF9mY+1hG2HRDTy!{&%@LHY8TcHpQO%LU>_hby%aQz>7l+bzFk3qie^%MP|wJ^DkX$ zdKPI>*dS>ilxUUUg9Okj3RLax(OgSGHWT z{ggSjzNgo^&mzPIT(<;s{W}G+edAtr5uSDM?s^&P(y2DIk}($-U>T%U!Ay2IN0n#V zFYO4|FUMXgjF%=_Zo53+)GtheVto z*Py-NzNPHGi$bru!@EKI^xsylv1)wnGnua^jlfreRE8!cn^r?(LRj*dWSzX6tLrmg zhLdjB^J$YDmZd+i9IRVrT-p!cMf=VDfb)_+JY2>9+9n-u*ZSyQjrTUy>Nk`_Xy0Um zk@fFckz(GL2^m6#XYPrHbRsS3*=ku}7OUSROlf%59gg}h6ttjG0b8ek1ZfQxPZHHm zrUK#YLxiMu!&&uwjxC5~%tKoHXB)81rUsv>m)STbmeqV(>5}%bQH`gGoLJ2%d0iEWb!+r&RNL?Q&3+2fAu6{(YrNP@0y}+^t z#e#0eZQ8$Y)kr-N!}!D)sn7ROtEls8nl>D=e?FSl7Rwo$9ECU`i|dX-%r%MyXLf3K ziVMzm45zv);AtolnbX-PJ#i$xOP+-YuXk`W6y@BV)n{&)A3;2-uIW|D5H zp^tmQF+!m}y9enZ8j=lD_t`|G+U^-}GXySiGjb0F9Q~1d-G4DA{y$Qk9L& zT%*db*Ui`et8@Reu>UOBkA(vK?G69_k4E`yywKJl0QUZ=5<`ER+hA=Pdy zP_itW3K39(*6RvNni$R;@MF+E{w27Hu1@oN&!2C7=lRsWZMeqN_r`C?jdvBMc^5&e z3<+mEIWaUoU538%QG>G&YBO>cDXSG|Q=#8{@NR>CYm7YBfw_^olD_(F>`8xKxJJ3w zgU69%6%9yb`rnz^^>@%d8|{C_#C}Ro<3pXw62Y3J_>USKe^t)Y@pr<13ZAl*pnn+r zyKJ?i$x0So%EUawPOdZ~O_REHKV(TA25gNw{p-y{65~25+@AXjAoe$& zj&~z2gqT*AyoKSHS7x*c6#jj}}vU#}o5UREPLxu7%=>pDOAzn;;aP?pe zCEIOTse@6Sd?s?0=#B<0#3M)qKYHhrL^NYNC%maaTGTf77@WQHR}B8vrT(|6SIJF# z`PPhg2-q{wM7HeC`OveJPbIvzIdb2d@ezZzIb(|Dde-~1g;R3i(~(iTv0fdw;GU7( zJ~bqdtjRzhi7Of2gRwe3si21s;%E#r)~&E3mrkIw$34h*=We>((+c*q-?X{Z2dqX- z&arA_vvuca>WIBTngMS|2-%%K)WE$%p>9hUkK0+h0>h3J>W!F!be##gD{9r@?A2bEv8 zU*g9%6ys~tvgS%BqCJ&h;)o-~2t|p3GmjmHd2glu^b~>66EU{G&C69qmzrT{u)>$QA zIk=|6S}Y1FYTK#(lqJW9(@cc5%_Z7vCXwjgoj=}Ayfn~Bu0kYo;HPN*#jvDZ z%dZk#d#C@==tVXHxwku3o%KqT>PmHHg68Epfb?72ioUi?!jOIF6 zy0RMj6mXrZsDVArSk~&SZz;^9ONiNaO$bsXEC#Hd=-Ty;Q#mP1bxQT=o|0SQ9MhMc zrIqTdu|5Y+cdk>@!?%l$=_zTfUk9&WjmKHo1*o4ZoH;7@NqL*;jh}D0cgJn8IIKOW z^LfkNev72yp{YPhbDBGCp3L;gGBh3Vx4QlsHU(%Jac7eUeJ@Gv>>>WgT|`#2=8QIY z^~%qh+0vXaBzY8;Ymo2BXK=%dgnF4!Z2RK0hjBlIsa8a;M)cNTJ&pAWzHa!sht`BU z#H!JVcOkxgV#4G1XFqM>Xbm%#;L1am&db?MB0MWhxYNK|p`G+_-%qezWf-#I?!NZ)g8lQb|Fn-6Tr&+{B(tp#6avU--r|(7y=V*gF z4?3Eh=6$$cfNc1p>TS8TOFSLoL+Vst1nP+j5{ zIh-gE)(G#vpO3R(WIsecuc{{mQ-Y%vkS18 zyVJiq_t8aeTBgc<6wwS<=yc&H=`frnR674%(lb$xYXU5!@VZKj0VIjXoo8IoP-ap zQ}x;&5|NR$XD$|03+`>E5OxVYFPAPNx8J%Jrc;g74LgkTf^p3>wfPWVd9vTGse)LaSX0|@ZJXXW2n<~#hUgjNS9)ZaTofzOzW3v zT$*2rAD_m@*Wl}(qRqv~JFM#jtlUbdj79jl`|8}alfv1u(}u)5X?0liejP5wdp?6# zfq%|}(0(~TPJ=H2mBy|dfwT;zFAlJ`(&F})_}*vxj=P2U&y4?jV)<{KeQRJkWK&Xc zzja+cTZerey#JI&qF+I6FuwGeEJ^C-G2aWzVy&COj~MLX99Qrw^m(SF%UQNRcwFKv z^eTPNy*lGzT)UF=+9N2d#0G70T!$;_ZLP+ZYtrgM-F9;LS&ii~>X$L@e+8wD9B`|vez~)5gO9ya zTw!IjDY~mhPEMKx%dgfj&+Mjn>Xc>6^kH&wpsC5b2k(C>t08{`#bp;>n+kwtil1$( znkTEuEwLHL+mR435iD!4FgQtC)f5$}<$HSK6MS?V^(*n$2rkz;Wa-Y?^m*RjFqWjB zbv=eArNtOI(cHxxn3rk%kYnh6$oJlTuBC3dmh5x7L?-rIG`~SQ3~NTRq~h*!e2Anf zOJGn>B~_OBcU`NT1?%m^rOD;DJIM)tE)Dw_oMbAAdyxhHjNs~Wl|4)Fj9?jhXPM`G znzq3EHcF9R#gfj*t*XI}pSpcgHI_qn$JwT3*P$-bz*(A>&Sn12#i%*3;Vf?_m+ARx zK7Wa>OZal_uEtapEwlPd29M#SGkC2|`)Q0#dsc?_cCS@}s6G$PWzj#M7DC$YgWH{O z9a1qePo0OV*_$xcdYKjf+N){uFnSLZEfXCOsYEydGri0$6h>w}{Sce{@!u)LJ_Rn|!XY%aS^NjB5ObkJYXsh3tlKID0eIz8<3*`LE9!kwi7FDHG%$6Em{KJl`z zkZby9h9A9gC)tEjl9j1niF9`Ei9JogXoD}{^I_Z{db#s$*fBWXkdv`4nuIL(;66iH z@0*h!a!(ATdDwceoP+(V$?Djkug)3{$jzZD48@=WM6(E|G=3S23JA5EtH7Kz?Oi7Nv(*E4E;ZUKrWUw4M{hhbCf z`JDl+n2Ucu>W#;su zmD}jSsG1Zc*L$S^QK7`|Jd5X{BK?kmn^wuoDaOE^weBK?CQNhJdoMx#m1Lu#_SbN8YreJ-pHp(S|?cGcm-T~v9sO?RY zinSYigUy6jk36H(U2FF5!n-Ep+TnT(qD;og>HP3aUT1Pp?Rued(&+zQWE)5W_zI%U zombMA-hUYXcpB>p`w7R)q#h6BzJ#u<$w|*3eMmIbV{kLXFb*kOdzP}a42{Ec4wgTH zaXEJr0c|YuQr1mf*yA$t(9U{PDKoQ)6t><&;TBDTr|oBAyEFA3w3%znq&T&ZpVd75 zrcI?fv_u;*TY4$w>H@uX;_t!uO#Ijq3s9~B?@p;h`@z-l)u@XLKoU~cmaNXXra7i* zQk;XjpmR62&2bbJ+4~^7Q;RPCv>Bxc#hHdWtJO5l5$3IFedCjN-K9CG$@z3RKZEV< zY`qaF7>h(!QDJ!x)}~~sBRSqB_|s}dK_52x`=te2`xhMP-5kF3Am&Bm&2uKuCA7#}whQ=@zJmYjlmKo(k-ygB50p&ze--#FLb>kMd49oo4|=U!GpTM_1oUXDb0%Yq zApK*I>a^+2X_X@%x^Id?J(IL3zB#`hLATEOP9%f+V-U+=PlF{%2zXoV ztCv{+EzO`RpZ@%&F}^iDYn~x5iL}TD#yiA4pB9U{I%$zE=u8qnrKv$^K&wXYdJiKj za++0m*OUhv3fF3(ooh5*xc919)2nnkAp+cIBBzAAv4!&p|7~%jNl8CyVvt21of9Z3 z-2Zekt}$*36bUd%nkY)uGsGrX&j{IpqpN6r!LVkYJ@Rz28|<6@aX zY3CFCBXr`==h1JyFOH^JuMCeZ=<8qv3hb&l>zmcIrmnb6M_>=WuI zw9Oa~r+rW02EQY!J;NR1oA3rNZ^0j(I&?VHqv{LceD1XEIWbUmfz0Pm*G6CNFr*T~W>^ zfyLb8#+Sfx?v(o3u%=z@y*QUA`p^NA&dz-e>d($M(?viHd<q@6U)PSn`!Ky5@Y6x+Z}MvL_D3xQqg8u=0P83T*dJ+a|OCY zH^-vxs`Ma5aE7ZR&c>s|Iw^i*ahgx@%q@ak-4sqyzEU2WMz>>-Mqj}-n!Tx=~-!AN_{N!}%l09~(edx~`FV}i! zes|>#mU~E*eJHuH<{&;2W0x(s_aL8gf_SKddt0z9LL`fx23~@)=wMzhB`wyb!}}WA zv*+9uPi=AnyPbeM4#!L~|LEbQTm@{VO-d9ZB8vLzOGmf?I>!{+32T!VJc z426)sYnV~j#Qk*Gq^z$E?l$;QgV8en-<y*j&UNVX%~cldbQ$|h`twVB_uG{Y zYoQqG?N*VPPPuaO9h9?k594-t=fT+r#T}Bfg|WDE$DkGCJC*btv~0?MnYs6#b=h~D z>3jpOLqMKfbT;0_7~2F;Z9y!-y(q$cES=_=ix3T3;alQD8nq)>BkW(Ab_ z%gN`QT;|~8a`u;8QH9Qj-SpMCdX|87{{5Q8Bt>fVbj-6j^+9*%4(CcD82TENMUIQR z&M5OmK}x9_A1|Z40w=h3<9HZt*A&bh_EkmspV@eCgOLaMG`7nbE!c6M7N z`Hw2$pUuPinW(=x<8NRe9iK(O%m&s2R~@e^N@9vudX?g``;6I~D&u|v?@n(T-7FZ_ zmgvdm$ZLujCq>KaU~ywz61OWjxsJh2r``>}G{#ltvQ*_?ov9^nqNb)wRk9Wlz`IX;mHL=kJ65Tky@{@zZGk3i#9oW1*G7$QmCnZLrlra(J!I zD?5+LY462pSqysu?NV2`ii^E(!G|jk`Z!$SMOj@B8SbndQAQ$|a;j3T0e5n$8+nS5 zjt$OlLEC3G{jCvgOgK&61AokEUllaxD^L51nO#2xpq^*PSPeU6BYoUKoqOnltTx>> zN^N7Xq{PQ+Un^URVv{!bMpZD_?R(h-`|2t{YRz6L%y{kbT#H}do{*gKU$4>qqjrCh2 zTPKx4Y#kfUV_tTDBGTkKt*iO1pW!WtWZdr5YS0GTt%e~Voza5IoLV@%hTgsW4D2BX zOtx7FX00LWV+J&t#AahSwZOFxV(-eeXog)$>!cd=Ht3gZ$t;6qHPZClr)HEhi8s>J z94x~rGb|;VJo?`MDnip^@^Jf{7KHfzr3_+Ix}{b4_6p9|Tn{CErk5F4sKd-j*&k^! zI2?Pq)Z)#!dXVm!L}&>>6XtZN7qH2;j^MsJz`{V0uYP~IBNB$saC^3I*0tb_b9h%>QX zFg}f+w{tS}NJ;#QwrgSDhg=hFQ3B)nF^GMzJO^nV^xPRa=t+h1*eFfl z(t?_()?w`?v27OkVY`g&pTWncI-ubLHfY~6ZOfSZWN#=0s{2r)W7T=ew5h7|SL)y0 zCB|MO*her<<9+Knv@jm0MD0P)7KtjyrJ$@U8Mf*3j;G>@&!yugs%3L0HRthGdTl;E z$jekp&z)4i;a&x5*$Rr#HMr8?y>{@#cDbMv5%gt-_GAT_j5*@y>!A9;-_Gya#^!A(1HLigF~fP@ zvW;4mAhe%GO~&gZ%fhtlx$L`9cWF{CSBJIy7p8hgW9+?j+#Neb0{pEx<&`O2QOX^N z;^Ne2p=Emye%_s7#@|z-bg}QZMQfAV=IA^dpDqhhs(~fJj%b7BGS*WEPhC`Krw;GW z#y|ewjr3XgKmN~^{_jKL-lBYo;=#IV#c__r^K(#+&Ug=6$Wj$a6T^N5X$+{ab-lBf z!S_(}m%2$%3cKa;_pL=jU8+!`2%c-|>4a3SHEnRDe zIv8|@1tk&|#)h*rpBWD}(Xg7}6*qiWI%1w^)wjvo_1hMFD-zjpxw7o%gwO!Hz$0BcM8^1 zYpV-*k4c~mh|{sl*)FYQHVZr{3G+VH^b`ikf@;oHoVGi676)N%T2D#T?f4aN4E}g^ z%0Ha#-!uL9 zk03U!6r0VsU&V>Zv?QL)U6f{7riP=A;9PVT^?>7sb5jG#{NK2fV$|Q6 z^>$J-oy(!M$XA4I%2LdiTVl*`tu3FQM)}v!%Jgb@@0`i7eW5-xv1=9Aky780YljhR z%~_FVrq5CToki-SLpwBS70^=WS~}$PS(|LTN0wE%RcV!iNoB-XlBOMPK&`r8mMlTC zAI{c|r4I5vg9Xwen?uCZcU?g}RBet;LW8j=mNiaAd|K8nK8;_tUeclu=d9Z0jU?ab z-~T!I_@mKAXeU0?^2)518!)cUJqGPeY(Em?!{M8eAHpGS%~(!(x}(ef=8N&RNmH7+ z0$XdO$)t>&=nsS#))q^;ptapfc9lrZcbSP>#In#N|ha+)JM>%3^L zxf<~->j!Oi`g8rTn)Y5+)e*OuykxtvnDLnhe=Ly6us* z7P?HMyY_{Gy>(&LEai6Ib>W;tmDX57@vk$Jp$&BaYZYLnRta1DR>Gk6F0j=(?)=cM za-=y;m>B|B>x>@sw@6+i&nb{=bS$V(aHXMul-nrDxJ!_WA~e(C@D`Ih)nL2TiQD4j zMQGA!>b8#v>OELjO%kqW(B?s%OZt| zf{O;SmGK!kUGSVm_N`?#q9kQTxa%Dvu=mi2xv6MPP1xI3oIi77*`y^ny7Q)Uk4pZr z@cX*57TrvH>Vgw_@C{gx;DIjMmutOcBYuf4JyfWXz_T+xBr-c!AXn31mKP>7f}rziQLElow#+VJR5rP1(~*{;HRyH;R6 zg>nFs^ z$#b2NzeuMh4T?`l%{XP#@|b_OK&z^3ymqar?lf~7o`dT#sCizPx5T=uNr>pKR8hZR zKLnlsuQ0dVf^-f>?Yut+@3YW;7QSca**fd1a}H7W z+LDOPh@>11Z+{iMX?*Fcg4{(E9m=}Z88ZF((Picwdf+|N{W@Dgr1HHj|oY}_P<3+}^2{*Gp}CTlj2 zm;fsk06tT4V@PD*GK{Ahb&5pJpNm5BJWoB!p$lPe#`A{7)GTMFAAK7^nxQ2l3$5r^ zaJtB9{UekN^fJ;<`5cc&q9k=vqrfPGWk9}+AJ^dS+9m%M&fk|xd^g%i zv2y=nRM)`n!8ix~hhbYHR#iwP1gU^vq<~FD!RkgF^MJf6x!>T>zjDBnMoNo8v;o(^ zn*Ka@{{`%s`a9MLs!5q#NF-*4d7eTUj5|3!J0*(&eud(MuQ^pnA>U^SILEa31(XzA zCW=~5lOnZDMs`+Vm47(PVca%2R_AX^kkcT(CEmA_nhuK3#p#F+jNY=>AA^(oGDdAUSQNUi-YO3va}gy&zjmKuLu{M$@_>!vAD^&sto z1x3%ct{nHpA(laZPD~u1pSK~oucV}#h`|J(=JUajRkIV-v+qcG68o3S11?$my z)L@yF;K>S2)>7+(UIpkGZp7-uV%&>yZpOW-Beo&cwB3`((bVZ(rcrO{L9eou5usIC z3Zm$FF1NE^unf`t#suQD9-JlUF8W;>h5Rl8L^E+ccDy?rB!JKZr525k6>hFp?EiP)k!l4dhUu5U`Y~uI-))eGXm#>FQRY|eL@|E#^5AD%MCunrnSm#Vm z%T8Vrd67Rk4Rz|{rTuNTV0m|VHKNUQEF=#U=V`X0+8flLThJu%kuXg9&Wb1-42>Z`0!#p=xrxH5OWK zBVSIw#iu_{(dgDy8i?dzMUa(yaakQFYoImld?*1Q@e7kXZapm7!Ir5+p4@?2;9T_? z?k1dVnR)4JIQgEW#B9_}7jd?)E?;W;HAI=hWrw*|i7g-fuS1oo>b`@C{8V^Rw!!Y5 z9B^8u?)sc;qKMZMIE>mf9coP8cv69~R)br%(L`{EBAHJVkvw)`j0F@5Y8m(%Q_QEX z-@C+sIT?9z`ul6O0(K2-hL~Ji;#_s`VDOqd^+z~Q(|TdtM)|2Mj8vt6Ns}ZO%Bdlv zw8Qbu1&|pk%x*fhn1XUP2! zQMso0%}I>Iu+d2~J@4~R!~WAa{%s-smiT=;Gy!=BerQKJ7CpZsJ0HCf8IGR0v(Fe+ z6-RGZ4#QPM=`oW=CtpswrJ}YD^%eBDCWgmu{0xoZX~rPoV~{7?A)$jg%{bjoaL;#c z+==ZjO3bXT%PvbyXfxffPC@cojJ$-bcir$)ldLc`5-9`|N{1#h=^CVakW%MNnLk3# zi-!qKUJ88t9@u6apDG~ot;?|7!p26IXz+7RcRrFPb;)2&a~jctW4f+d59--@Era)| zAey}<@|jp)aGg3Y&a88K-v^mc!8b&)^N_!MT_9!S9@6^UNeOUkH&zmDgL};A?pfP+ z7h9!Yo$GaDR}e!wjkYGPB}t3Yg2!7+%>HFN^RGB=3DDXifh3Ejku@7Xo(h-xxCJh2 zR}kJwL+{r|N_^itWTPk7emjc@NeVm$4QVa1iAlhan~I4;9o+>+b}lbeuyih*56P(G$zv5JUz4;755D?0tGUS-_7`##5B*H$8Pl1`QsIQ z9GP!T>gmK3F@&gXj%#%K?u^BWtFv5{bampWQzVfXYo?FDTbdYCRi7uY9L~yMAB|oI zH%`MbGe#KnK}=vqiZS=WbN`(ZXOW0&Ou=I}*_m+pM)Tx(M{!;Qnh(;n(;)%AQmCF9 zN0+W}p#Fsyc4Hxw6qTI3D_O3zQ08;j8o-*oxyz6MB&E5Axq_q4#Kl!3Cb8b<2p&DO zqr=ty0-$7{wa1J&O+y#|q$bn*WL(d^``#RP**ZK8mK^MsSca6|b?)3ZS5+BFe!My~ zVjp?(`b*5QwQ+W1Sq3R93fSh$d43Vv;;ZQBtQr3C&@bfUD-Z*=C1|0DW4l!Vged?= z%n6}#kNJ6cl1=s4q_)1-LB@#N6`V4Hy9bLokCw1o7iG`E1E*vYqrhcpT&M2fb8)Vx z(6Clu&9dHndcz{~(Gts$UweBGmV0ozGd2Z@EO&5D+vB>Z^Bj3jmjd!RlVfs{P_8Q; zcP*}Pk>s2a3dngFyF)*2GI*k!--K(Q*kLd{K-*?vXr6l{HP8nd=f8RpBzljXZz%RH0ltm`*dD zHG6QZ%CpErc-ytl?kZ+r)U=6~xeva_z^hX}6Ml(mlCwsrEkc>;zv^#UN_~dbc+-y6 zZ+X}T%Bo*3qntrLhJtv8^b9?Wt1Has7_v?I%!juncq~vh6{b2hv>KdC;`mm$_c<<~m~aqWhKXrkUmAyA8OB2v zx{`yQjANI|-v&HJq7HbCE&$Z63B=x=#T(b21QGKIut};Sm)pqIIc_~qn~7V0CD8)Y zGNubi4UmPgT+T9^nPZ*lu`?pPdEiyxoN_5n+h^puE3KE08hqF8f~z>GNE6V)Ikv>L z7<(C$L!l|x-XxTjr5moNBD+gfJn(soZrL{#pec!k5K5k+R88~74BARvg$i?0(3^lx z4HaHz56bC0y5fniDWr~~2Ig0M^ML&LO;)b=pBf2QLK)PnRHc^m(k9ex^#u7lY?1ia z)%e*ffX^_c6`gM1N@cAKQrUA(%zLZ zCjPc{ymsxFOvuPDp7Z!20MFQfta19?B$h}n;S3WmNlH^BHuDA+naIZ+O)a63)oqgs zm8Nwl8MbI-6(%I7hvCz%awl=S%R{MA;>{N*u3-2jMrPV#rvf_sK@N)r;zV*I5)wZM$%}^hDNGAhjzM2quG>Vt|Eks zO(W#!v#>_r%F=;NKICh3df_B08IzJ(bCBK1;rP|*CIxx=aP9?a1nuo?Ryk~;=7g{6AeM8?-*LVl zX4scnes|6faqGPsei`>Y@H$shuDo(zW#erIJq@lrcjX?Fyg5DYUvQ`@0brk~+_nvd zcT$D@vygkDck$V|B;Km7DbTh`x00He{kaMXGpDu8RAdzbUEjynby>%vsa{faAcT|N zg2VSu1$teJCUHyX`T2P$-7z=0R(ysVdC{tbJEo|vi#6d8yEAr8AS09>S@Ud2M!-45 z=h)?2UyzpOLac!05a6aV84dp>0ksuPL83@RTTT@xV+eJ;go?>yQ8C|U_nw%6Nn4U8 zqYdso38=NGd1y(|+9T^Tj20}1v=C>&&!I`rbJ2MmA;rGeM6TK$<`6B`Zi#K14@M3( zRk@J*AUw2cO%t=5g1y4|mX4+tBc8!s26cuity5IJ4%v3|L-bcs1cOxcEPg@e@@4Gj zTurr^9N2_GzOwF@ZB;;+h`72ZYu2;IoZ8U&6`J-m*rsRy-Gl2MJVxSC13NW|wxlc# z9wyxIWICkD7)YxmFthWSm-zfME@=Sn9Ne1#R_UDY<2r-W<}(#-M*q5mzuA|;_gNMF zt84eetPJjuIxey*NlzNH#6Bnb~7^zC5a(Ha)Q&buN9lqJnX*)nGTsBCgpjL}p*4czr4c=`11H-VBy2-ve!V|xavNU--|1M5z`jC^R~au;D{dsYQ{xpsWNEmObY zS|inE&~nHkHF@rY4t8r|EqI&e(lS_z_R%SHc(oLqOR(imK}E^#aJ*FjjUl?Ef77&L z+PEcpo4d)>G$OWJfU;n<9IHNG{IGvVswQ>&cM zM`C<;MP<7Prfs5e({PrW7{?%v;8lbA-B|W%slJsdvPb8W#2Zt(oejDrsmYV9gcL|y z@F-eO^|vw3;2y!YnZmoAsX0;Okj?m(bO!!05_eLgu+G&$UEz8JTY`^E@GU=us;-;X zBQVoMhbF9cNt@8$gVzY$I@xAX>YZyHVjfJ9Vs3Mkj|wt|ERVlMP{a9y;7mi=5EJy2 z5WL9d%FWU5&WA?q%-oTJo`Kw){WR7$0)Tyee;tw;Ee5Yex}zlpcU889Ww@Ri59F>f zy_>|ki-#=V>zwpe@c1+tETR|#y^O$SA(kPitb8L&^*_nkxsBkN^pO$T3|fSrD6XI$#s zH4ivnx`fly7$4FB)J2_f%8-V!d>Y^W0!%8~PkeH@0PVZb-%4yF6dnkg{e@L|B3~1t~YK z)re)Vq{^~);x`5I*d=~~Zo;Tle;mn!)Fj@~@Nr`i@4(5taKJYJ+<8&s>zmyK2 z>iA`}pAtQ7O}~Gi35y#>v*49xhhFsk*V&PL_&i__c)l{*?f6?*&N4c8aXu$s+XL=l zoLx8M(u3NJdTIT%miT4sOI;2OFWNnGPx0c!qOK>bpRfb*k<^_vfr?({v=1k~$;A*! zqNgo_@qkxx_MB<&LGK;EV=@>NYIPEvqw(rN$(g^6jiq+($h6HRw7QKlI%UW<7G)Nf zWu`+W34#027`*u4F`PJ^*F9+0z`I19b=SzcBzVp)|2kEqrJ>1>&ox|`Mg6%NKNonH z;2N;jM0q*KbR4Xa7+vXnX=W@~4eCFY_ExfCdBV&>U1w?P)Y3KBT5w;3?t}MYEDslx z;{yNB#M1|h!S}0^Zs(t_!*xt{e_tiyO<$AAi6^$^a%}umap=e32v~~qUY-B^F!&Zc zpR!h`A@~nXjESu$#s#AfJ}ay_;X|G0_7-Kdnh7j51+T*-j%_n^=U(MwUsK?V(GH1N z?{2&zxSzlywAQ^RSj>a9%kCPd$YP_)!PApe_SFXCPzvGmYAo*|Sdi-@07TEz1YGklCm>I>XE4;y^9%#RK)Q$(<)MKiDo6Y_JVls@E2S+l#Em{GcP0WFN4YtQ%%@WRA z)zt3Mg0st!z*7LR_2rB%hRVlNE5A`C4sN%Q=^jR!p<7(Pa+M&@v)~ZqXe8m*%~S;D zPh&q|yQF71QRH-FH4l9X=bORRj6B||25b-FH~Hm!b~Zf*H4O}5HO+MamfPVp)~@_< zvf*#zICTL3W{nn#4?LuzIn& z*0D$uS)nyToz;9@Gv|`nGHlUVyHxnS2UpVG)w6&uy(DZT@%Bx-lWT=*foIm<^&G)@ zcRombyA&_{)3uVQGaGxjA}ON_uJ68-iwnZJ;a?3-1g&yXu_L^IIK5gy`f9>L`o(VaaCYS31>Z> z<%II;SfJ30T^HG&#K}I(pso_o=q=S}){u~FFUB)Dxf%NdG)^2h=|3RipO48LXLvaixpnQ7H^Q+Rio_$0%H(S!*iFxwu2B=248>H5PBH zbRdo3`V!`q;G1>2X;j=#VR z^QHi?0Djm!G8uko{5jGD#|+9a* zQmSXpis@d$ST6-ztr6s+@%Rn_&JcauGtifk?{#%0O*{N6J!g*ghQ_2`@V^Ea? z&6Wo@Sy=82dw~`S-U33kq;R`6Yr6GDI<-6Nj(ZNBDNTM(9iV+WWriGLAWp_|DQ9M> zqT9xF57iOGoJjlpJB%6llg9$r2KUFHZymR;u}V#pd&ch$xQ3uF-;%MLDzwvdU_ZjK z-(b9qZ$FIv9>gQ~xfK4K$!n}e-DFK$Ti`!5%FA85CpwfiC=Bi`!|oFMd^-2f$~bjM zM*=HOE^}>Qs`PH)D8}+gN+mN>q-UrgSdwuKb-n7cwF_}mg-S8fv|5e3Q@e8EOE=CK zRPRJKR-9{f#&jIKTUTZ0M(>^eneZk~{7AugC~w#&OnW^sjBi(oDD%)%HQM@?V)`2J;8f-ZbDvv)5B4U_C8*eR7I9$ zV1%?E>hhh1prlTDcCPPETc9se+Ilnc=|T`!qc-?#N(UQ{p-5H>6_Xg2q`dclaSMj@ znPJ(2eNn_&9trNmcDSNbO_l!WhFwl3`0yd7e|2gTTOsls;b& zZS>s@x_Ig9glV;aeki|wDcX}#jr&mGN^*IrV+!e3B<%Z{j3dEEg0dLLhjH29od&;G zXR*1K7+N(%==s`a66Sk`MO`^Kx1clcz}{H47dIo)6f7^J`_`gChr5ALO@jS9$RF z0uK`)DP7``jB3=YV||e79C3}Ht%*O2vAy8?Bqg?8ow5z-k8W2$mE0I|BGnyy@9i2qImGE>+J%PdejL$lG6sLY2PgspV~O z8nsSq=_>4RT6Ayh!`Ms{kMw}_1pjv<%&9Rr$DnkVwrE1U4s+H=!$0Ke@V8`vRp5NJc!JFpcNi%u!e#!8u&Qy)Nd!$P#$lDQjhF zn(TTq%7@?b?rfa1a%u+WkBj8B{A?bF+Poc%Uf$9GTCYp zK8c3L9ETgvPv^IPbb5#Wk=T;aU&eXfB3W6h$r8uH5IITmrqh>lw+3b&~kz_;kz!sAdu!-oodSGjj-K2Nj(^7S-188~ffF|Uaw39=yIa?!7@703HA?#(_CocQ|_Q$q3F{&xq5#Z z%cfA4Jc8QA78prmKAUl;+n71-u8^(d>e>jYr6o?&LD;6(Id!9U73<5)o{xxMg+r;q z(q=f%N8+$Vi^`e$oMbp|;j(#wp?=qcnHad;FZ zpuJt%l9XXJtxYHs%i7`dGUBtb+!C5bGR`r0ygKbP)-sv()6H`QY6MK(*hq#or;enN z@H2k>^EDgyJ(E5!VUQz2DRPS@8H>f7%ouh$XLRlnjC(L}+Bs;SD)v(`9-(QT^&wZ# zFjAF>(6V!^&apd8K-;LHE;HG$^u6@U`>8wjO2$<*$!cAbk?#>a2K?hTj;0KiM-_bM z{UK3oo#XnE;XGhHjP=i8eGQg66!W~N8C;a~Y|hD_Pch}_Lwc$Y5u7#C|cS;d$~0nn7R0Pw}ojeVZD&WeDcPp_OZ00v5+7<}mgd zN$zs1c~yAdd&Xn09^_ZZk6lma?t^=sv^O(8To2hFG3a#)r<(s|7c=D?Kaa%k|7(!` zakJFHpWicea~^B(Ju6%OJ_3IYd6kQ?Z3=!!ZOGGp$qpB*{F34DxQD1>tC2<_lEKxa zI&}dOmZZwOjNqt2Yn?66B((fjCbq@gd1vhdhs|B^Y8lUYH z0c@^poNExfkwf;Pes%m)YKoc%=XY4X8GTcj&#*ain}H|&E$Y%}+H=`5eL=}66DTyA z(x!&NEsqds`p@f%MruV`hH*)8nzHtsJ;{shMX^?2;xwhd=|OL%y>@EQhH}i8FRSAe z2$m#h&d=4kx6Zo^nklAtAz0F&J{k`a)qF-!2Cvi^eenC`q|0?dtvZ<9gCzwy5AGrO z&vj&0Pi)>%Oc=L&HCJ}sgPMh(3JWXTA0OHl0NHt%F~-er(Vqb zf;??r_q-fp@m~jXoe|KFq7J0&#$l@VDHClx<_^5%7&J8L zX*Ec5o!4hEh#75jMmRMa$7-CrQHq|+E6u=}VQfBuIVN35X~rG!eg^Tkjy=0r@eRIT zK^l!a7ukxd!nkU{A6@XAQU*&I?3cq&XZ#uP(qzku&c(hsn+;Mn;%0)<1Pd!8sKvQ= zV|;_-As&mz9AcbMdg5DTep_W{eZM-#XXXF?{}}yV_}hPU9?wcEMyvz#&X%BGip@>U z*bnD^Y1OzkX{h054Q$C6@t=KN%^V*Gksz!7p-H8|) zwM3Gl^5bXX_kRw4`^U}baIA^X?D)%gw!vn>mQ~g86~kGB*WzR|o{N%=4mX;c67 zI$5wR8m*?}w3H>->A@YweTnH^P$y+WfDtcFT3k>F*SIA$vD=4hau7uo?O2UFYZ_rd zSz&h}pVBHQO;74P2k=@#F_c^27`tauH6jKP%8CCGofz;PrR!N&pg=;dg$>1~9*c3W z&b5B|(Ky0w;M;zQlyCO4q&= zuQRR9l0+mVb?%XkoRxs}E{cb&SE5&4qFZR3Sv`1e+C|2MZ}e!oTxXPOb1Cc7?X;%6 z_O)o*VAZiIDqv3?FAd8b9-UI5woGqM*O4K zREI8%XHQe(Fef}Va|M_kKem)-Bi(gk;nQRBDIYcT1FM}3|H!DYSJ`22;!7}4y z&#x{oV7U{IJCO|zgQGz|TuS&7Y&j^KY*x>rmBaawi0NteGw>p7+_4z38h0KuvRMM& zbGneqEnfePtb1TUna8Z+&(hO;CIw=E?uG_7=W&bDm4ABx6}CDT#C`A`^Bo)*4Vbtyk_HfwSieK(Gn0Iiw0com%s zO9YDz){$fb4Ct%Y2w%2V8-Ia#jKEhF`{!f{>^bJTLrw=vz(b$A6XeTS4v7grv(DRm z8IMb%sC(5~v!G`1%KGfF_7`ogHi#qPqew*MK|#vuWDskJ=O6Ix&&1#U6@0uq>BFgu zOKIO_D;u{|$Hjtsnd2#~(=>iR2Ikty`a??aIQ18I)Fq}+QkWfs`yA9h_n=*v-Z8~q z$&KO8m7Q1)vWio+V?mwly$pbXU!lVEBNJSOO;AY}1xQnwS3Z}~Cn?8K^Rq`Qal zaVO4+x;=ZPUxWJ%?~-{e1?G;XE>3#DzBHD{AhpCW!itf-Y%m~ z-*-aFO4Kj#GJnUvf$!f3ZDR~B{kbU1Rbr(kbzU&av<-nB3O`KsdMPOy9l zn}*R}NjpS?5vGySIvA^R;eCboG9yDnifi+seZ3E?&Em1!1V`!b;U;x=09PbdA7mSh zY^0@=3nG(EZ+f^2l8#Zjz*A>U+!pwv1NuT5fz>2%N#8sE7~G-GcdJspde(@~Cm!C% zWUqTL2NepD%mOM$T&ryQy*a|$q$3SIxg3A?_Z z5kZPXX$j9xZG)euV_ng^Y{9*vc;Rl0e`q{3o`dmhL>}bZ*v_otZPLo>kjuk9GcBaa zcm$MvuoM-B_twb{`{l$-JJfz_hf87tyMlI4@n#rlNF$Jn#C~xE`xV3i`BWF=$W9rd z&m^YW|CqTK+iI*ux$DN%W-L%PXMLz(Dnsb-bi+Hzve(wXI@Y_x`sWnBHCfy&Yjv51 zkq*U#a}Q-VJe;4WaoE5z?9E8RmeS6_I2$os*kv>gTxQDZF}?XM^xgFs63%8}WF0+6 zQq3EzrgtLgvZw^-8VU12E`KV1UfB?u{Ebm&O(Xc1IHq9-;LLKG48?r zUfJ)J^zVtkJsWH4q-`jY{V`M!G`*)j34MO402@t3=TcNO=0nMv2kreKy;-F0~z$9{%g`8hEJiOHZKoN58yZ7zTLqQ#_lE;t$4NVyCyG;wlU-G>hpMMMN1o?U z074mwQk3S$1n$HnMa8a>Sq_oN#+@{Bk2y-~8WHCc2q)5@jD*(#cct}4r*p4~*SAFe zH;oq3?u<1Ur8A1gd@T^%FOkLR#(Mb9_67+-a{DY9ugO(P=M6usRCeGoD{~W#?rM?=?~=5 zpER%-iVORy-hETy*j>WZuU(yDCQQiJiQOz>GcjiTClm0|$Lv(5#)Ft4Ik&vzXs(7DQ_-5+aqCk>1Wb zCH}NQ-EYOqy78Eb|DII5=Pm(KKaAsqNKPyPTc`a#XnW_*gX{u4jhe7mf}e@@)=DG~ zgy8KWnx#4Aut}XP73>mRCSSqbwGS>yQ@`7dvnWE_W^riQ*jEW!W9kWN(OGnx@mgVB zgB1z4XtL;ju0zP&F4Svz2g_>)968k8h=<`N*sI3 zE8{+c=Yj20%-e;GrF8WKUU{>!kUcrlbMMTGwM>i z=lU5uv!-7@jAb|<)%kvDO+nN1#EiwQSL1$F{`Vgj`~S7__*U|$E%SC;LrD=%jsic`+>9lN^TkvydL@CND9W&ef6{jdL7rkMAg63s-E zT#x{-UoZ|eR7^dL%?WIqQx)&bJP(Q1CHT_BU^t9f&^#~Q&(XA+O<8O6a|w|{eZ~&E z!h8}zIgNa1n(1Y7S3F2A`|-VNg__gnU&_$^a2}tD<t&R;v%KNk zXD9VR&&Io`Yqw(xvBreCWSb*)Wx=1PKFplel`U(~@Hx5??lCR%@;ZX$YS0RAE|R%@|$( z4cBPYhtW=XqJ4@s6oa4xb#DrsI7~0mGIG(!s_Q_OC72e6GI& z-d_Nz4qlCYq9Eln9$I^Z#=DmXSA+C6{K(vEP#=R>Wc5r(X1Nn_D`_qzV-yt@tU`$T z?Qk1CrvH8DfI2P7i7}uKvE9quC7)>D*<-N#pp?e=80@{Xe-_41?U~Q563gJ3oTWP4 zljsJ&JixII%7FY%+$S@IYkoeaecvl9BR<+3pH2u*9h&B>leAU(#5GR(*eDaylT<)4 zNB1$cCd^LUnx3WY8afP^DR(C4M(^`W>0)Hm4^{Yird#H@h8Q*z9cSudO}eo1e13&d zg6lReWRHx!ON3e>ok`j9a{{Q4L48iml3XH>plyRR8q1_({v#*;RR-xu*ySw4NL{1y z$l^YDGq$0<>YWrndZwTjNPA~214|PjZhq1;?Gl7tUTbe%De2V@YmL%9HEhU z3)Z1M>6zdz&bCfhfKc}OeOBZ3l>Wd2)(YpM@v0?Qx`K{g^WxjSFuIE-ejSb3OK2az zHce5gDfFpjeQqnp?t^tv3f7yMrf%l}MVzZLFBVO>-_hs*JCZGrD{n>-CJbB>oL-6=HL zSr$Qdjw85k=>yg>n^f2O`52+xk1`Rvr}pkW!D~&L;GBb{4WbDuwI*0Tm5zHSD9a>C z&imerUdju(FP0{eV9Yzj|5O8BwBZoOeUZDS6LO@S3EOq>vm$SWLZG*K)&DhO|VMLWpjZkGil5s3f zPVjMe>Y|i zGvOPv&wU*xH1Ot(-MH34Pnrbu84fX4;Sqh6nL-fJ5IrSFPzo+0~@+|-9DkRpzucWCoLwZs(UMN^* zPYGL9jX&O+0Nlvdcius|rJS#>YTUikic-m*+4*enGFi2JdV*7e$K0!}BVbo|bcA-| zk+t@)VZ2gs=0R+owSrs1ZY9+X*ADBN{206pUpwDMzR0&m!nK&Iqmd>S1&oLbOCXiUFPVHJ#(r`8Y&Ia_)$;wdN6 zh?(j*J@vaYHB7>+f7t}TFx+Q?yh16OV(cYRm!MTy%X%}eyK}|hb0}z|?fkObDfU+R z7fZl1IcL^$I!ra|=Sb8n#ki-S&k5N&<0g`6U7LL72Fy6$FGPDQvIqJm7Qz^4g&Pu5 zw>{`-kh<%FN}6PRO)tJ(M*j@GU(UN5JH_U)tSj7P&_y_cqGoXsGnsB^SXbqAKA%$+bf zTAHaj@kuKk6LG%I6iE*I6%Bo6O)kwfsqQt1%h_&0v~oKpFFOmquw}>AAUfP09KFL^ zRq>zP?1IOoXC znyieigY_&CSPGVHiuO+VoLkd~)Q4g)qt0%*$nH0iz90KS^p{{5c#U7_eHt$tXAvB0 zP0|&Nrh@fR2j>Mf3|}Fp8G6hDCDoZ9{*-d}y~!H6HfPVq@=X`@JtL1P8rAQ&4&7tg z6MGL$;2!XE8{ChMtwR>T=9Cm{PhA${(`b`MiO*-(jUQx=9{l!(eGHlz=k9nJJiGDg z3UeaM)6D$Y+x%TVn{lMjXI(m1)%TEoW^xta%ZAX?7QvMg?IB9$bD2Kzr}QBs2BRsP z{2Sr}^sZ;Re;ul(ymJP`dq}%f6fykngYl=nzZ@FJ%8jG=UZ0)bohQ9;(bU7!STyc}_D|?YMi^8L@q(-c1g+H%&u+?m8Do*2SK$nHXm9 z7=qt$h<=9!_2Jx4tx+CFP%k)dy_dD9DKisNIOe!~Jrem+;e4+Hp3)&bzNVbT72?%J zKh>tr#J~8SXnP{}M7|sGs`weSF&Jx77?w-;S<`g3EY3mDjzRgqobpE^t?Kk+*V>3K z9s~)cY}#b2oRbWhrdZMqI|IMfgte?tra<(=+*Rb=*|`=4lC6EPErT;BZgYGIb?HUT z4s-g@5!)gqeyy3)#6ssU?cAk*?wD{qaZKPnf^)#PKb^;n%N}1pa~5_TG{JcZE$mgv zj~0;Mua19cp4rPh%$A9*Gzk-Ib_JebU$kRy!*p=21`@$M;mZX z&N^F=S3OKtAypT4(sZ%csr1Mld`Q)M730*ojytTb{n0fG9PiArwh_0n#-MZsPXyEA z(-jhOR9BQ~S2ETSlSTf$(|2`&qqO$yb-iYA30q{)2kJ{YT*{w$sF zps$K*e&A%pCm2KPi_&$VoI?{ByEJ_n3;3$kIiF+L_YFrLq++b|QsI(M*Vnm!ty-6K zgVhJt1&N|djFw06a~SVilcB8-(hc?gnre0KNU}&3Bd-(aRKX7=;hkL^dnTeaH|3D0 zuMu{Ey2&}gJh-3Bmp~3UWf^-Xi3oor#qw^ql0Jq@kh^BBHF`3xw+Enqo|muUBw$im%HCHXGK{*^e$_w_bKQsHm7=1j}gBH zIMk4u-KrS)HWSyLH{`eMh+}fnuPg}8gW&hk>0L>FHN%k;@7<|;kk+6u*O54OsEP6& zmPLN_eClBLES`hHa-i2iT@)|g*P!hxNS1Eo#poxzTNik3z9+Xq?y>>Wrz0gZL&g3Q zfWC{8Hn%R+b?I<6b9o;WF%P zj0;|QaAqB(8Tb}@SFQ~4@Lzcr6|L1Ct1@N0J8N^Ee~LA;w8j;|S&XtJ(p<+~4A$(F znK~Ffs8?WbO*L(qQNDTFq;E}P#+|wk}vwdd?pX@w z30j$XINIqY37pE1*V^ht^}7A?Ltm!s@8rs~Z!>Vmx+0QnkEx24`gyB~WeSa2hW^xO zd~HTM`4!%gcXhUey2SlTL=Qf_V@t3k`R}jk11`m>c@pIojQ|xcA$y9H#3pLqDeggA zA+Lk9s#6}9v7e6pn4u}IopcHXR}7c`cOUS+y9$NSNUU~&ItCKd(2Sg%c2D7&$%0lr z_+g2(CH6;U?3EPGb|%W1)S0JXYmQ$@WWucdy|Ph6I$Ym{Z=Xr_x}ih#0OByV+gN`F z+$zxc9sD^t{lHkndmYX2%ULe57tYn`AA|Hw0EfDCRk_1i?u@mB&rlT~bH$W{HJs=g z1-Dt?=4qka=1ud?!BIMoCGi+4h{ljWCw2JNjmI5aA&0}Z!ZPEcO+cjc9sG#k zFntbp4VEhV>%Dg!OcoOQwkeQZjWYtO1WW(O8A``p(aNtUoR3bV!RYG#mLywPT!}Pb zYx<14>wU4LXWo} zM6I&3dJ>Q*?P5NhsL-PZxoREP=6-r9df#%hSpnz_@iw^5;Bi1*gvI{mQjU*H-_-~e zYwxUcXeD?)yH-ER`4P^K#W_vm?G?usy~9t_Vmm$!drenDn-iE-MX80c-V!|4kc~4w zhoZYL*eAiT6C#+-@RqBolg!<)91^L{zoRvE=s|Ej^xu{xdMlT+9$J6&dG1+PNKjgp z`|+8apGDxZr#IF|2TQag@lP~r-WD}RBOsl|eygK?tvXX$9r!iO#bxTvoErB9*eOiF-V5PxrBHu1gRV2@?Xw6lAK0k z3Z7|yMNj9@3M{%nJ>x@>+YGJ5awxDX9mf4ujV6dM&#qI6=TQ9X*!e) z@|g!QI%%jP|DH_ZpfogkSb_(MfB#IBPwoG|RGYcMb~e20;471}eq5@u)=6TEt})Yc z4~hGl2wOGT6nH6X+Q+n{8Jq~hQBWp#T@!uPFTP%IUDG>np$u}D;AyMc|MZ~`?4AFM zhklN~VoWi_+)MV5U)@~$lN5Rpw!>I%iKxz*I<*#M+TRIkRcBLYa75~pB-fo@f*)Nm zs^*E+ov6@meK})};rra}N&hgzT|&DZtGYo>gZIN&+{lY|?df)w8E5%9L?5)Fky0;8 zoXQ!Nr(xaY!~YV7&E&n)gLD|_l-9r&6+aE>A%?>-F_k?T(Vb__cR-<+sl3duKes*UsLZ)||UKd%&oU-`eTMFlq_T6!;?5@;1Y2qB;d7 zCl(9JB4UZ6lg0P`N^3RQ?s}OK*+ZBAxMmBIMT}faP#;jM^Rox{ z5j>l`_>{y}LWDKb%C(|%q0BYmy#m`zI<0V<$em0O()|{Oa$OQ(!6>TX(_+-A-TBgu zO4Dl4YNA|;bTz$K=D)cA zbqwBjC*xQa_$Y_oO;Dm_HQ2s@Z)nZ&wR*j$?R^w!JIbLutInSlp{Y`Y;>{z&)t&n^ zyg7AuVg`26mGzRF2&k=29>ew*wmNsC+|o}q;L(HihATnajIpW#N}-?S-B_ExOHZaV z`XO@Ydc%D>cZI#rL`BnQTuM@7DNF|0Hs_API0s>gN6XxK{-QUO+w@Fw~uV8oi^+5?>1!DG+o>Vp(q5gv|HJt9 zkH&Qb|LMl(0PaND^w}>VC2UTDZ@jO;8cxAwI|SO9uw@Y2Aig!i)tyHg6E-EBr(i~K zm4UgjT#A>@RdLF>D(L8KvL5BRhrhq}`l}JoftAk3%X$1ctJx&@Q?Igiu015oTBQMr zmx7G0Em4b;y;DLOfJfEKTxw@D32&NDZNsIJVQ$)WhiL^cR%0wK>R{Kdk+1x3G!nia zuAOp)cKiiq8m?@EWU?dPYfy`l*fv)z@L4*Q!G0BweZlO>+aBnWx$Ix1jnPSSRps)8DJilwQI35xlkEy*#ro+8YCq*%R|880ve90|t zg8Vj~E!fV%Ke}8#>*BDfG4Rm26fIW~ zHZ91Am$9UvUz#LT(=%&mLa=rz-g`LLZqyxY4VI>Tf3F&omwEYg&~H3{*DVsuJbTAb zk=L8vvvaO1(wxLfzzN}l1qm+WFwzSu&K*g>tLcWdu0Q8OiGz=nw95W342?R|?)okr zk7YF~%W3qVy1V)XTQMG2{1Sin8JLytAf}*q5UaE# zJxn|RhYdML;(#_SdgS2P41Xu`?euM~=wgxv4eTRGAG(`bn^U9$c?*0~U|V?@ewm`J zdk7SlH7U;dn{)Lp@*c)DW)<5-(ps;~|MLaE9gVw(y4SAGahYj#EhWyJh}GDJ(@ycK zTi3|DCFjmk+V7^FV`gw&>YmpzwC81#n1(#h-U2q`Xu>Hj_xSZe_UWO%vi9m@T4A#5 z!!>k3S3*!8^u^hiQFDrXm=?WMN z&%m0E)0`i9*j`gOp|CX{LwJrrP|PIRX0?B4vdhK4M;cD3Z2! zqc%N+wFaLTd~E8RuL-5d)%kW9ztzsR1^1Q|5M`ZQg)UyrFq2r!GvwW-QXSqu^dje} zAihn)IJQn-CNU6$W?Z+EZzoM4k)djOt0q8H1l>cySQc~=waqwbmlEExMwVw9q!j$y znn#mAvuKO`np8bbNIyGv2_h4llb=TWaDFa9&ARx#*)QLCpRHdW z%DZR|Cxewd*Cs|CvKIA4@96!XLH|#q<_v!?q`!66b(X{@%O=r?gP}>}5A{BSqI!hTW=<{pYNj}KkS>Lij z*6De#&V&_dy76#X64TIU5!0@jx;*(FD!%*BN^NP-`%vBjgYU~6BZvOItp}sdXP`r0 zoNG<=O_$jE<&nR&@6Fd2_g?}&y}&}@hHFHePYLvP5AtoaCZL(km~k6$b>W~Fmp+Do z)`OM?DJOC$(_jFn2bN>*I0kE1KvjCFP>cT(eQOacc}yF&CN4cm2ee(mb28dT~_nxDl+Z~y74NpX)Ef$ThUr&`+A05c@L$E z8mcT7DdT%rB5hnIvBxatY7$wmFL150DK5{LQQMlVwr-qQTg^r{I+YoStd$59Ac59$A+5MT#Yg{O(g=m!4AFS$KqO>RcNwY^n)JY~@6Iv=^@v@&Z)F7pkrD$@ z#keGzvE*E?eSRdV<#Po4WxOg9zs-cS2Qw+KPEyj$1|3BYpv`JuzDFGdF_ras2>ug& zMo<>PeEibIzudauN7cUMWL*SgR#TI6Q5V+)fxw#ayt8I6Uivy*EWU} zt}*R1lil9yOhC)Q-3I$lpQz;^9ut~06QI_rm;7$b_UIK@s0_=Q67P++Ik1=bJ|~iS4U%4qBPf3BMp|B-yLIuFlKe1=44~7 z8edjZK#;ASJO=4X*i^-TL5aeGPX*g5I783> zeGJZkWmmjwaVg{%(}9)x3~nmUcMoykL)5P^g%154w6{jp+n7P*RO>xBLc0hl~Yj-JVsr%9t@RY5vE3A9y7 z{QY+&DMpUJD4|V4%N5xS>pgk?Lq(%ai$pbz^RLx7H^s@8w^MGp7E%sQGsW{ZgSXz< zV-wT7V0?5VO+eO{ooPKZ9XP)0=k^@5r1z6DGwO$t*Xbe(aWK*$mgf(@;PMwf=l=I0n`zQ;S_C?r_=3_j}8 zaHQ4Pcc;99V+`UwNLf>%%RBcpq^F{H`0GNtT?%D?IxP*_ggK4s?0*(`8`m{a*Z$Sr z&!D2JNn(s?ugw~)d=)P1HKZcPmzoqm zTqk>fpg-U1v_n^;-2yP>Ey&9t?Gjs+ApvapVem5Q5wtR!x)wZt1mFI=NoV6&lA^Ys z&b6r~NM?NClvQf|B?sdnAziI@uO)Uj;4fgDqsk{B6isdx$E!bbqe-G#Td2n_elC~tSEptDe^a1;- zFV&mI*r~#vI%k3ZpBqPZ_Qh~B)?={sf%QQvE@kak{BG7^k*eX>kai$B&68gCMLy*v z%QCbsgX>Ci7)c)IKd#`|jn8l%(^BTZwhq>0yjyT2VKA4uXI~BYkTvDr3{vpnhQG9D89xW%a4t^T zwDO3d(H7b8Y4tl#Xg|~q?u$lr{eZ1z9&hc?YSPY@qVAJ9j`bMyBG^*QA~4cm+ZvBE z(GCHlz6f|VWVH-;>H@Y&&7S6-e>KG-55%G0YpA1NoK_NJ=A~alQvmZBpKL>z)x`-< zjBts$UY)IW&ZZHnbb!+)J73Vnbs^*lg}Vsuv++@r@Ww-(@4YDw^vddtn+5mv7dF&p zCjI6{a_1i4SMa_&VZq)Fzxs5?XexExL0n^YEl%5&et9vt4r4D)`X+DqKGor^D<(Ry zS@1|dP`)5XsGIIpqR-xR;eVWRP*_(d6{hYwuOZ%iHX6Y@4cwfo8@HP%XK9c-tj!q& z$F9Qv&O?ANpQ?G?|6~V$j9(r92Tt!!Lx-bi#`-&0(nW8{c%0CW;5{^Dd`7UL{E>Tc z2Cna6)8wt0&X12Sb@Jz!L)C`_)4qJ^L^R2?sy%r*RdiKX0^S4otz_;$E9VsNlBBEX zy<-*jnIq7b`5v`-xh_H7IzC`K68WXcNIIv8Fiox2t?w={%8)+gh)m1kWF%7|otuQ< zz36@UJc62?{X3*}@Jw)Qa1w0YC{+SupYXPAIzs9!FyA2zam{cSxiL!Udl)m6t#vhb z$IN9}m+=cC^%0IGr|SL}=6!xYf9vq!kZkWX9eRtAq%&vG3` z-GcU%fX{C|2g3vc$|WXpcw&VG!Bu$CbRM-VYU$ZT09`HcW-O;B`P0ZkLw|n0U$C@7 zzV)HC0{+nW>A#J_e~(I`bFGEIs5x;bvlO)uOC-io$lD zk>QrO=F9gQ5!5h-8R;p?Rqfh&jSpk&vdfg)`1q08nsET5$a>}n*ewtFvIL%W3BI=A zO7hqDrY^cvXE_u{*_i6mNiIt+KXOC9{T%w|B6QJ~qM(dggZE{O>=I_?DQa5>c?9F_ zqIKQV13jr|k(4Tx598Yj>ub>8gV#N{M$oUpdOPRCsGGjOdxl;t$ifsqgY{4C_8St( zEqCCTPSc*@-koKNy4p;(I(~E5XE)G|S`%r}ck+>gV@`1H62vNPiZ>CodNcCRL}|u) z30a+*bRj8%WoSQYU*e!!D;nQsKo!AbW}Mqx1NBw^P0jfBGkEktvcQroPEpm}s;hKt z1k0_6)jkt8gGOBA7vJ6-?(E#V_Q4~I`go;5NBi`cp*AD*ooye+k`nnRd|n;vNRZYC zqYu$Ft3;%y={v7Qqhk`(tTo!_A%F1$X%BwR$<>l&*)*4QIWJJ^#GX!0xtXR;{c`>- zIa?Egv4<(!VJ6hE2wt@SKl|Xc!J5@Eo@=nr;1p3H2W?XzjJOg~kEgl5%S;<2Do#?s zR&^=aK8Vf8*W@e|38?C2oK1X;y};?ld+0ffV*-4>8J@+SI9%^7=$YRSSWjh(TtoQV zd+40Y)3{_6_>>Jd*%Xp1hF(A?55Dildt~ZcRbY(3uAttXaR-A${RnzdhhLg;wtyG1 z**4?*4~O}j<~1;ky9JM`qP4{gDcavSG<`h|pnO|JxE>Y@_Pg&r8zY-}aTcFP$ zv)me6kDpqrEE8>;To>5ovX+j{FHOfX;2m>QxlBD&gX0qD>GzRXJY;*l<#tFi^ikkpF3)E-u@d!SW?Bv`@oRKQsSV;q4M6_h`N8*Q5#+UzJ6M!~9 zd!GXOF*K#IiT_>i;In_(yH%7`Gd`A>X|1wo-bvrv$P&0`|Cf`SYXq2-Yu-Bi_M`Cs z`j_$Tb1t)IWs32eAebi9pbvn5^?; z#yaFNeGhm=;9u*8JfjzzI_Y#8`83v-v7dSo-X*BvJcrgM7{Zg+Q#!}%?1>zVn^Wl^B z)^!zJ-WV*mle^QKCM9n(_C46wz(0-lhR?$&-;D8_iXZF7^K{ZrCw)ph>5DGpYZyz+ zh}+LWKZ0mVy&Fj{as_G)#%WwhyWn%GiMM2=B3o1X4ARd=+$uIK&u4Nmjsf>Q*u&U* z@UKt^N?x7&NbEgWP2>M)qP3(FEWbBm<_t8`)L_k!Po=0V?1$jlfzWX+7H8$982nJ$$}8424th^x>2y<%+NIcP~g zdlXHHJc*(Iafxu&4X#bA4iE4#52FzDq+B79W75Uq$huZuO4s}PPtV54#=YtNA5+9; zNx~~5s6M!hqJE!lJkr3{5Uaux-V_7(+f+_xbL-*p9c4q9>|Icasw zoR)&yI?Dl%cPCY8hv;G|G>7=eynf5v-;MfIv-VM8zk;=Q#(*=s@@_Wm3@gIL`%JoJ z=4)I&@NWE`VNC;n%=fTMpgc@PXYwgjd}-yKFA1~joFMls9h^eyH;SeVda3ui6D_u4?YH1y`S6<~L^~`E760WFcjVlTO!;cysu+&z$Mq zIMC<4BHwIF(~2`E9`_d9HfLTlk#V~c4@2ovE5EFc&EZ^wx(_0aUpA<9M#1_G?}Fm(`K;FI>>3- zl-?6*dO7>2Q9gs^mY$`@d#gtvH--lfkx zfe^CR_o)VM+8b`o$W5fPb94IYv;M!$pSgav(OH33$g^6gK${nG5n#yzz+5j+|^M(16$JKmeH@~uhus6-x3m%l>VzCJ%Qq&{wI{YESYoi9(i>b|`%;?% zqnNuf=@7I7y^B%ZXj|~H1jT1gvugxr-PqsS-@cRVJ#8Bzie>rfFB^0lG#fbj&|{L9 z@=SVKnnqy5X54Dh7@01`ZjSlSUPhZrQ*)^-U+ciLUVw^T;@oGa&=d`O%=E0}I%J2< z)2HhtKli~qcg6w_cXpd%#a)Cczce+tP3XVXG}2VQy}ap{DH?bWXPe{U;qnI+w_hPL z7r}W4OLErRe&uQRSp{DsI8OKtbq259HC?F?pM!MEK)lSj=YQ?a=Zp}2OiZcISk}Iq z)*vJ3U4HYuN*P_=6SUC>wa-d+lGPG&gYhQ{>o zDj`~qp=Y)my*cA@l1)d7Yq|EALPgpas#hIW@4F(1- zpu^`pf_w*gH1Z_^T*Vp1<-2twz5=ag>RE3?mcWvztMnhurdr&MUYsjIEn2r3&`%M) zEWtLM<5rO|Tz1pdowey*eGe51X%T_#QI*r)SK~)Yvb628pjGd*9Eyr|UGNtE%THf7 ztpaQ^#oH87u4CHAW-!sQN@#VRPDw*d{~`fzH+4ec@;BdE1+EW?1K(vZ7VS^(7#vyC zixcB>Bp%jjlMcCzftjze436T7sDzgF6|G)9XuKjKhwUCVPNwFlQGOJFX>a|{;i z#BlDRg8A}!8MzWUmu{?KyvE?OD{tPCCOLPA1iLRodYduhmyw>sv%a6mGiDlHe13{~ zkcV;1Gbk~i)jlKWo8E=q1a8U^TK}ve5K}mxO(aKIb0zHFZsXb2(EKrlmm??Y>Rivv zcsjPt{^^2z4@%P{tZpiX1Fd-LnD)L|y<5!hbKav-wR%2oV?1?ce`&3(DRMYnvYvMf zVuO@abS)ThLA;Ir{^GaD_y4YPuB@R+OidH!GzIrInIkFm95;+MH8?4Z-gR*0LD1_2 zI-E0G9Gm`AMJsa8QBUGm&`V7IF-Yvvp3e65r6-N;#;$yGudoG5< zJS)Mr*su7|+_es9C>4w^A<=MB?u@L1dB4CIq^yVYEWx>`g4pIobLJtx^UPVdI>RSs zj&GUWc`}WyWp9}ye{}FxvtN^fv;})HVoe-ZaMw=10zc&)z30nUswM@op|Nc!mCJvM z9MZEo$exum@J^ac_B2U(M{$={~0lKt{Nm=}lxGSUk~9R8-Yf05ZbeOjRz-A3F~;n}D>0 zqa|iQJ1-*tn-K#{KwhN#EpWeHz|m2efy!L`nfflM2fy1g1X~ATN0(wWY8So_#q;hM z8|jFM1Huazm>yo#T(~RG0iYf?fj#)w<|RmdF*5oR=tTy7s{*?#Mr8pgv588-Vm~fs zxVPo_J_R@wX&0=G!5t$pfv|oPwBiR3bdTrD60jvEcWpNcdVFI?fhN#yuo2_BLHikE z^a%q>H{8BD#&>jwNw~L<4tN=nvUPFPziPtFnTSz1R#rd-d2x+3gQwgUPTN%y3W@1% z5NwSR$^QPl-#{CGTi28fdE?6CyK}O6>x?vB&b_)A1)l&fSdUn3kEMPJ2#gIjCrVct zP}%s*#X@R^=)QXmBgV~d0{y-HTp7M2EirCC)W%!KJ&asK-UQ5_tPL&0)PQZ2v%IRpKE6_w{Lyyp{to&R#0)MYH8RcC1LUF^&ilU0tDX_krqSalAi{Fw$ z?gB_6f>|^2ruf>aG5Jh{Ri#Poi%jZY+U1zSJO<4`uL;lOXoFg|9I**k6gB%2)KhQ) zI6WaPPMmWHxL0)a5-+#C0ADQJtOy=(Na1(3PKPySc8o3Jj>XZdq8Bn;O5$@YiA@WW zA{{Mpy8)LG4w+6TN;&=lCJU>xLeuSDc! z+vq9JQPH6Z@=H)7{Nnw8$qjh`mnKY&V4=c7L%@slJucA(w63I(?S>TgH&M>TnlMIo z8nC|4& z>5e-7j$LhqBh$G@r94@M34o`+)7gOUo8eU^<0ii{1w^~!`##~1f!~y)+0gT? zWe+xfK&Q|Q(pOBwqEK)OQD>rXZcWG7xHdrIMY4%t(}Z4_yWzuOW>}RFA;^0y?yVrg_kh0j#q2c|4~=2yn!yW;r&75Gm+o8x!ItoJJUMr zO#R%1E)ffRSxXM9JOI;|qJDb>=I|o%h~q6g1*JsGGVWGG$mOB_UIOfsfUK^LQw+rt zEuse4#bi)+jDjlwV+p*}-_|)Xu4qiSxybal*) z?R^>wznJHdOTy-YTPJYJ0K7KZ>ZHCA+|Im}LG8@yj(re2J$7YrcBhE&xHXF$O{-3J@Tl*NhyL;C_+VX++v^alZ}xMK@WA(9l$+Q-%n8Mp^-U|UsSPM zVZf0G!a1Ac=G@Jx5%q1GLUO_}f%IIsLJ6N^ZKOr;s&Cq<<1z*T^=c>%$jea%qoV6H zXlQdB)b#M#B|WoyG{_@1)EYS!2k=`_MzLSO_2nop#b>6i*g|Y=PO57avp+y z3%yk}%w6$nTsh5Y&=1{?TV`{q|O*b zbBZFS(YrY^MOE)i>>BL&DXcb@2wqtt|5#D?1*@crE%1d=V|tn?Y3GffnP-_g1aTh?gZ)pfVNvGf=2%&`>q&bmXQyWEOM9&wb%igO7-(^?U8kCUh$&idSoR+31mHSC3HYy){;jb-T zh)jE)bU_^n+QR`ae6Tav7I>Ig;HGpiwCPE?mMr- z&pTIR)m_Lsui!p&+Y}#LSUWpA^S`EH4B#KP1pOhA#OV0Uz{4DR6U) z8(~V)3~4T;#r;1(R|V8C6lfVJn*f{Q+QNe#m{v_3#p@zz6G#|FZiA;FPOcDi7CI`1 zu-X^n6lLvv&i0mJ!(c*-=@oX%a#km}QIsS10 zyE%Sl7Eb*&u!f;N6a1mrH?C&dCXl=1+#D-8J`A`AE8(@2hgaaYkIhiC@#hEI_o-ZA zwZVh_u?l|w=Z4$=t=RtO#2=^Ok3%rO6YQ0-Rl~L~TvafOVH1)7`vwy2OrWRY^8)H= zOiO!lU(1e|zpo+^M3rfMQrHMh@Y0v}NHvkezd(O|I$21Y5v@ITipCJdl#8QIFdcNn zG^GIJ(=iT*X2a1K%PNbF*aiMf$$)dwa`(e19Q!uG$c9nb*d#F2W!()>Ha@o}MX)0- z;%C?kRXL@}!fD*9Jo}_^!gVd+gyN3Gb1pebUeKH4IR&dLMu4MkF)Z=>4Z2Z)TAzY87;j!dGN%oW)MsH6RzmDNZwx*1 z1a##}>R19jGR|(W$(78?EKXbIBsNEI)GWMj%82zUh^OD+4E_lOy=OiXue;)P&}~&- zOch!H)}d&|b*SWonj@&_8=OL9!TJ>3Zy_8$32oDpX_e2S_!&6ny9;ic;g$`h3cg>8 z=jAAkBI-GLHeZE>>t0w4m6*C(;-3vi?A?QgTAd+TcTF6vu0_j|awoNdVrMDhBk|51 zkAq4$x{|>iav#7`1uG>8NE;I;%*JESaeFy#S1A0I8S_^80?m!VKWTDLvV#Lb8Oi~aE5yRn84lyxI*9GcSV21R4I15 zlVN!%omGF05#+rZ@BmUu_?~2Se{BZIg8esOZKTNARBX$!ZH}iX*By%oqX#IX4#7Pc zP7!hp2FK{LQ$(hr6z)b9`?)Lj$}UidO^XB7GZpiDzrMyzg4NG3Q=hdGt$ z6;w#S?WBYdN&Mb)w20ELjzF9$9NRFgCa}(nnV>2z6=V7i1iu1N&8NY=xhBVo_bDgf zUdYjCF9AA*WE305<)0Kw=@xk19IxFl(}JXhy9zc}9K!GCdn(4ca2s5)p=KBi?vAFM z(0L}?o#JIdak@2N+y!ej_=A0vIdC~*y~cnbO~j%aA6~4?0HOO?@%w*O{NsNb9%sjd z;E!9vwI=4D4!jb!|48tgqkk+qft;9H6oGdoX|GYK|FDG-;=wf?FCi!^D?2_!%3;qe ztT@RD#(#f%rX4+|D4S8hD&_!qLHyhRe>jd?tPsHNv#O(jXHnlA@?j{28i;K%hTJPf zb7q7}rC3wdPVDc>0=d!VW=@TWCMGRvWs`BA2|pIK3s>g8dXXOteNhXboogA-1WMV{ zaZSfr7~=ED33`#`@D@BBZg3a0le_!fNJealZh@}s(ma4z&*^x*fPFH!Ng__UgF3J+ z0qI*X8t_uZM>X7IC9z&1WUX8?Ops-<=g4bV3`m=xzQ#2QgUBqv0*+H5-v((8b&7#k zh?$dcJ!pei@nf(c7AJcpWq}p0mR4cJbIZVUqkdx_0!ayNF491|!Hlc)s~CREijV0i zm*dZ7`0EfHS3*(2eHf0*sMlCQ_uK_7(Y5iKf?FlmASb~P!G{TCI#LnLMFIKi$wph8 zjvy<5)Sx*o{vJ8Rl`C;tG2D@FL(!MVPCG~(D}rl6)RL#uTC(E9-30Rhv>IU`PbR{R z#zJ6?gud9#=GZswFUNio?FyrHX&j36U|QZh3<8}+- znl1{ybivJW`_4!|6Rr;A2aq3zbTfEFEXxA!i%fKzEN5*2>je7A@9YDD&$e*AxI6Ea zhfD@{_{&-7ydt&imZ`wbH?A_CaC{2mZ_R+$5S%9y+HP-o$Rn!E#e=fnfv=AQ``|^m zzXWS2AvT3m3p1erusP~wDADoeAAo?x(6Wi2*TVW|LM@5=Vi&j=iW&9@kCND#T8Odq z#*0#tV7~zU67*-nhyo@3%FiZKe3|3JgP<{asb(`;z#Q~k?-e-_~T2AmJWRVU0G`j_C4jBO^|rr_Sg=DG>?#e_-;Ozv_= z6T`^dpWmvYg}QkAgBp=@I9gW7Zvy|FISpCp(?(OYrl{F*FN>*s|4li5`>W!A{9hR# z|93~ZCRaB#aXqC1H%Ge`zW$41LjNma|IHwu6QvrqMK5;-Crq0MU!ef_A^1WjDpnV& z8>}-ZY6`nODg%Qpagk1G;1uhEBNqy(CSz z42QdRLEj9=jceocskjDky975i*r(#$S+Fl-$d800YG(PRlJ^aaRu^d3c?y1*_+ z8iu1!tjV+PIUM5~xI38$TQ$s$!F96`{)NsI%rOA}C&TQdlLqUTU^HXnd~hm%iDANV zL{>lxHpZLdQUvHqI_Om$D=p{}qeL>=-3_}M zRu;S>g0nBCb9Hfcj}55bj^nkEmvOI`1uaw5G!<|dWRfgck|8CI_ElJ^y$|*)fIV|Y za+^D<@DQ=YwSm6{6mhVcJIZ3f!sSNc&ZO1ianas$F2Y`}LNbzT*sucd@S`f)1=!1B zr~SfmdWcVSFnr}`AqdNkR~Fc=s1o+MDma(HmIIAdr%%Je!q1Bx+4gVTuld0p*c9%h zK7SVc+bbb;LV(f>STEAV`r*Gk!vMXvN1=~Y!iBLehn?JUS2MIlWU_^D$c&d5gi)67 zG5A;jZh~^-82U8FT#C=U&@=fC*Xg(k3nIZA!5nr%0bU1S2Zf1`#w-JW0^gI_?s)*R zDAEx%Y{}4|cvVF{C|IdWP@am}4gH@?qx0~cqegyp3RO1PNhYTt5d!>mQ|M#i#KL^BJL__$KZ*ed-5yu0zEY$}GQN}|v*wNpVMjO# zPU;Kj%VGZn@*fE$D|r`2m%tQGUDAib?#2)vTg@7r50==AP32ZOJSb}rsii<0Wu*?l0e-nZ=eX+5+(bAb3LyYp`G%00XcAA+?IEDLW)c2Ii z@v|9@rg$WSCWB3q=%hF~8T*37)DVH{PK377>SsVV!#xX%C_0!Q(lXf&rva}n_(yfz z#UaIU#)?-Ez_vn>9~xq6M`7ri-1{Rg_#_mAk3`|Cfu9F819Vu;JJIGAS=e(JR;=VC zG*}R&zBLKHw}iPXj-6|#DZC?|JBgjgRNPkZ`fCJjC1R%2OR!x)`JTuif5M9ZZJ|%c zPYC|~A&}kCcg1<5z`R64sJhE6V?{9%99-$ zgh4C&O;8^Ry$OEWL`w^~D;{wHnk(VUb#+!cC67j+iwuf%TQ zpw%URIc8&Zjzlc-RN(ax+&;h{ofNA0#@}Hz{tT-K^n;$`@^aYIIX>DH{=vgbT_OyG zyJHo{EeXCm)8j+|+^HMb8pC+j;-#XC)u(L&sobZbF@Jh<3Rs#7>bGEjQ9d*k$Ol(04`E1nrJF1=mKTSTdr~bdbzv!}0YOAT4A2ec|^X z9lvE@{E>k#PB>;TAVj%>DBp(M7A-!Y4q}u9kJ){06sfMvPUvm(Ny)LfVJ1FDk_cgP zO*m_UW})41QU$+4KvuHABa~$Lub~4sRh5V9O3WwlgT)O19E~s+iOPNobuG&{3QeC} zQMm;=J7S{q3$Q1Z$5#fv)52Q7d^(mGWQ&8fhV5-R(iPl;u)n=BT@7PPn44i0rl=JM zEGC7DNSurBMVrH34x9@4&dh>iusQOy%UT3@n68lpB?u;@Y)kvDtPV+ z{ZJ%jV9DszC{ZaFxaxC2|I^WC!v2qp`(`-C!ecDVAvjxtC;Hm2#LMxWGa-o@rEU~d z9_E-QW5^u>+!Q0a`}BsCrLcpBfH!S?f5m{c2>4o=&xpRYSsRP>kvTbS{`YEe#IwH- zL4GFO{sfNi2?uS3R|)Nhv&+d-p(j_4HVr-vKQZyIRPb-R;YbV;vL)|91~n~U!lF5? zvl4_sT?~8rxg6(G)Uu$7rVo z<#P5=c*3zO9bFo3tha6{$BIxZ1=J}lyq79AHS~vI?FPSb-JF)fyCPkJdIk=+ykSYg zSX^~9am*Ykddo4LsP45VXc7MJ?pRO;#~fT)c6V$SllXFqbiIfL_sKiuZi>fZQ?yjE zgq(?T8cs8OHTv^EvSM6{**ICqnaO=Rfgj%sTc40geGvdyQjE-~`Xv;9-Y!RbIVL7X z9KSSiyFs!+&9Tw_4sSqPhF8m|#h~)Hu!y_-P&ZzD;WQCs zfp;})6mh6gXp`j4*Oq|KP$)Q;piPc5O_WJ@%IZ7~UOqBupUD$c(sz!uOJjF7l0t9X z@6W_;a&y6G?1*G?Rj?;Tb#@6h!wXs1EoKvx4w48b{lJ=lZE!z^pbJWjdsS&-O@kNX zOATHXxeC%o3fmk8?-cm-7%5lq;_oTI5H~_3V8C1qVZqY_^(y!n8J}n7*t@dI-51Am z(|~3%jS%8&hqF-m%*;FY_*rXWJn2k=wsWG9jk{URf~Psow|FUqB2<8$IBu5A#I6rx zHF_<SFR3sRWz*CALx9Qncg>Pu7{e zLzL`>+X>WWsEr{&%YlZ3mg9h$Ox_YVoEeZqkSf)?#Qi!XB_x+f4OH}!Fsfo-hTIG` zI12X2FxV$|=F=Iy>P0cDMpU&&zNw(4#fxbPq!FN%S6mhWy-G{alHr+w`<9TmcT_AV zDA|cnwW>I)<5i8JN1@OcL?JRAXcV?>FT?hnNP`04lm$6+b@Vz6vyh^fPO72(m-5rp?nvEm)r@t^KuI{m4*U-Y?hE)? z6i*G8z^)kOZ-L!%!^d~Ry>tJ+>4H{8lY}0GL_VXD?~wIZl(t(4ncewa7}MRx!Do3N z0=Wc#!O@CSO}^Mk=XYMne}jp^iR7Nb|6a4xVp<%}u0c8lE)(?b_^fpIlupZ;C!uZf z5R77k;)tV6MO_I?4ZZ`pCu9iDHt|1O!o3N~;i#3^=_>VXR0Xwyrv`=1lPeEL7#aoWO{k-x7Sb1Ihd3{5 z?~D`zmnJMARRg`=z+^oEPEF{O!qB3QlngrzLj^T){o4*Yz>0HJUk;!w#lM^++7y@C z5atrJL#Tr?ZZM}g^4mi{=}?f)$a~LpV(42;m*^h56~#S5sHQXCxSFF>#Tk*quK_%J zf=3|TxAOkDNdSxH47Uga7?KEcM-pj;m_U9x%4x7^cqN`A_GH6%y%Mg6;(7>LX4cEz z6(ck&8C}R_hhy{T&T~|;|LtK=VK;egf=vzbtw>Mi!OyA~yF?cn2}o~$u?j@Uww|#^ z(>H-G4t)z$u0xUB!z#r~$`?>y{K9)DFh@JR)h>=!1^Qw^u#{ql9k%!|_+B&67sl z^5l3gW#tPKz?%;86gD^H-I#nQT;U|4@_!Y_KQhPVZ34ptKiqNK7xdp1{dY3xae^s} z!j;MZke+lXd@ENog)n#ILI}x!UWWhptKj3faH!$ur(!7KD)gYE=ZXI8g~-* zx&x^>j_&Yo(7uTE{C1R$g;Of*_EZ7|t6Xo5L>2Ba3~3scCd|swem1I=Wm8z8W5g>N z>oW*%Z^sNKAWs&msc|K=GV?fwQth3R;If6Y0c$2~BD5n#$jUbO&zwI5w#W1VEEL6s zlcIk)>YtACb@9SV9u{7~SrTMZtXl+gU5@k}`2*dNjMTJMGkPHu>NbJH2u>QyF_Pf5 z39bkIq$+~!iCEI53{U7=(ozest)i4egw_*S; zK)$0p-37Wi@Bq57TiKf+cPBD=a+)_b~zQ}w*<|Jf+&%1}y zqDB+6I`9nNdjC`5Ibkb~v@w4mWsc{Q81`?4Uz|?tm3b{+DZxkF6AoOgesQgUp>13N zzuciu$1xcIbS&Ok6kAhtS&Vk?emU&ohn$VhxG{svU+C_EQwujI@VporCY4h-&A<;h zKDUMX0W@UV?(^RiT5W(dM;f%PLI67yeK)k?sK(E0ybNEL;2eV1iA$$@&0sr_ONb?= zIvz+IZ&y|oUWq15iExrj1nqr5sv%d0|M#$pc(77L>dwq=Q$l!ffk1*(*r_Ktv(NXQ3Zg7ND0yUXE8c{0j-6KOEQ{J=5Kg z5c|AEjm9Xn+iCduy8)#svYO0G z*$ig1zi8p-=b)nhH5K+!_`zI*RUEel+=@agrygR2qo~v8Et4Sx+~G63Jexz7VO)k4 z%F|sLl_)vCd`%1KBUVKJ} z8h4z|OGi^&CPRQHKU2$O`IkY`*;3%zfgbj=WQ_S;<|tFJpNiYJqx{iOzB}e}^qMel zitASJObStj39z}~ehFTuL#E*{fe$8Kz91AV_E5+tx)1!W_)?*0YIFX+h3mVmMc9K1 z_HYCAtXN0Fc}pzp6IV#*D^Rd-NKs~q#7OosTDwx=1ZH23dM-3jt(td(9S&8)?c{{e zCy*PpMe~rigmGw8r5J5e9T1AWu$KA!A62u zU!D)W2y|!X?12?F=f&oYh(p~M)E5uhAk=l z5kYDfV`OEJA+Lp<^|(3a;iwDPzY_jBX{&=Uv2}@{m4s3a4=j9HLcbid31p{bPz0De zMQ2Eky(d_s?RJ+0FTg9_^$#IxiEUZHu^g?34ZCyN zG>m)!{SfG#_CMc9Aiast%!08~pOly4I0@4`jd!RhS3OC9y&I0SFdm8)gx%eY z>6n36&v8bTOw4pE{O*?fFOlwq<7cp1#y8<*R(LOa&}w0K&R5j+7w9J&V^|v$IWH)< ztCTD3GR{omeuNz+krI|JLT|3-7@Y}iRvCA=5-)X+okntwf!?{<0GNaA6crr}csw1) zwHWSlIkwJ_gONBks0fHb6)M0xtHw2{_=NGHnNP=pinc4x2k`o_ zP#+VAIQ+~wn_zn-e4P9<$Kr*~!ayO_F%FLBejiF9;{^V-XzNTH^$7x2$9c#v@!rZ2 zk88Y)E+m|mFgiP(Lx9_3VI=}m-h2mb{LcxFnixp*Mp)O(7ke3}GJVn0oS^7ig5M(a zXF9fvp;sw8F3sr5P9rHGH%`M=ifLCAD=Yi2e--e;D~%)Rl?_cf?J;G*j0Hy70AvX2 z61q{0QPmoh$Fn+m;l5J`P?ZxIbD$2zZ$t5$8?#`mG* ztXIcbfir>Q=)!o=6qkaU;64&^%6L|4p|Ak!4CE@tpq3V^Qbf$LN0^gM;JgHS19X3T z%>g+Dax$Im8X0zX%$s9Ye&@1)n<%y{c)kqd+aR553MteyrRDJM@GryoFRDJ13az1& z@wL9fa#s}NuJB!OpTPb-@jV?^1=b_Oce+N#RMckJec^Hj=k#!;q!8Hk%0xji0?lUe zUk)mG1o$letTnLaSj2eON>rUMMR!iEG)F_%0sE$4+Hz#PC?n5v;b%3sxL`8{fcpgY zX{fr;9|aSNo*a3jhkqZj3RcH46K-FE@pAM?*_0g3(F8{0s&b2G>;y1hj!zH?{xpYV z;*L4xVnJ_3&=X;JCXT9tds67;n4&m0o(1Iv5F!;*Sp zGU@vq&LXG}7Jxknu;PSEU7;QD2ITYURCR~v_?>H;-j|~*EH2l*F8WnpL8D5M!MmJ6 zKSry{t@5F^?wB1ocfpc`bubJ;9H>TQYVtsz!m(U%nqZyWxBqx8)N_S*mY7HZ_e$L1 zP!WA*rjKPOkG&cB{!1Jg3Rw(DAB`2+jmru6;M)Rh!&CBTXYz7wsC%`-h7A!Hg(o98auZC8QLdM4NBstfD+e)aNhuByY0hWyl z(%g+5z&@q1Qt0 z%q;gnNqU52^hAqSo`f}NiS)WeduF;6+jP99;gki3;!yzq0i2(UlY*~|nz^e36dwb4 zbgps?OwzKG-{*h?k6myVPBDH)G$llE7^gScsjbi$&3UB9jpW!=AqrfDtnzfRD<6e` zm+iKYO|TEHWE6CuKr+%|jzV%C3?dY&i2=>H*Y`tURWUb3sniARaA;DL!m-V8VJGYq zsQM;BLgbV~DlMlTcGe|^-4sWU6-fc(Ih|;1caGCB9aD{kt1wY*DCv$zVgW#?Qbe*w zu*udxrP)MaoP;0_%I64&PnrLglPe4_1 zWyNU;^~lr>Rce5A{VhmLA#KRPu+JgJ`>QIhU5Mq~9R=jummge-=@ApV>QtHA-{B>? zC_rAyi*+dQOp0@eNYmb^x9&6z+yJDeA4e zz*fRqf^sQjgryE)m7YwWlT6Z|h3IY&u7{-lYeKraV8t^$MDWw79Gp4C*)e%QX9t*E zakhzmEwp4vOR$N;8rP}!=Y*bYPAu`c%~gpz=1_tt=@v?G71WKk#Y?EexC%qp_Gu)K zfgQQ;@w+&uyki5#&a){melP2n@1KId6k@*R0t=8-5l2bv++89^&i>Y)tSPunI%7@; zo{7ZIV%)ipH}y}bv0Bc_u?9tP6~THCq%@L~j+wYVcnz))?<%<6NF=lu(1)ReAyySa zCzm*CCJppjMD7a6jqxlnW$050@$TvJ%YD`dpM%|F9i)Fd#!_=Uo@_E(0)V9^h&5f4l7bxf-VWK zR4_JRD+{Xh=uczSmL_-bI*Ft#F^;b{jxywQoLz98{NW`5xfm~vCt&D9sc314meab5QvuLX27YguFM5dB<{bIBev@n;UA`xP= zSX~w+Z7UlUoi+hE4VxtR2V;+G0gw$TgE6^7pkIQj39SM3n((%`X4n&9O6Iu!niyX^ z^jD(qRtv#H5+N+Qq5zDS63cNFC8l;U5_0Cqzzzt?Fa}wqL17I9z;DFz&SdDx@vM$6 z!1lj09$&zCIes33{JY~;9lHamI$Zb?ZIMLyH5FGp;P0;7KY$r2g{1-cXS`eyld2&A zOTy{P6o!7ZyWq96Dmt2?zXY!i97AzW3y*Ul|1{Xi!)oP3L7W1$@SyS(Xytc$nr4Ph z#l9ZO37CXdrdC5yLpNoVwQ&F3b5J8I58+-NzB!J}k*y3sJ5khoe~*b-++2}fnViXT zYXVajuq#SYWN;LIWk=cpdjQw5nBWLr49nRy-I}6&3-+hs_@3C7F*Oi`7hQ#l_|3Ua zONIM>TP&VSXS8$83^Uj~q4x=li6x3w6jx>XT^af-`cxwQEjh)zI@p14%aAw6(-NMW z;AisiDQ0HV7#djn{98)Egd@9d1<-wKzym7yP}$-zW}=gvnX(P$b-?fs^mt@ z_hpC>>K%r@EzG>o5=D9|{jHJMjqilG;~{kM%pJ(CVWS0f19W3Nd43DSRwPC4r0qyKN znTgL{O)Pfv;<#WtBeSFkQYGd)E&B1#L-8zvTpfEdI;INmkEGPrXcQ1{%DcYrtn{{d zI=nfy(8gThHe2a^SJNeuUw;jbOAiX$?bDhs(Vs?=NL$Y)>+ z9y8`U1cT$`6c!~W>hY~QggdjD1Zg+a$q^Z;BYbG@FT>|^AoqnP>>NBgn!EvfJg{gK zV$ET23!&mJJgmf!rr;K02OHd5nH$lLp0Ft^$EL)TLW9(GIdV710=fz#SszTLt4lC0 z$JrT{a-9?iN+!`yB91b9_+~vuMwQNprEw^|!%L=Pn~GZ}yg{0Uutgz-9E7w0dl_CY zN4pZ%?*jZou?}L5w+Xl>*ap~5a16y}#B!b@jJ8DlnT3^M9u9jg^kqmzAxA=vWVsea zN-@VK^ny1w$4DMYj>9ox#AGpUPJ<7oOp2pcB4N=g{iOqnqX`~=IBtI}{Le1xW`)^7(e61p99(qZ3xCv^b{O~%!y4jit}>u zHw46Zu=_i4ad2YN%(0eZN5E1Ra2pHV2Cx82!88|n@|+GB|#p+GIzvE?~OsGaF7`uOzj&_igJC?E}XOC8pP45 zInDxnZ-k4;n%J+5qVnrN-6pVKj{00!lbRS!&Xl!-qR7=9dU8_W;9llzm5F=U?3ybs<#<>QgX|MhnD(ioK)RxfV-~|o9Q9w~kYpfi zQNjh}sA<9r*EeM-KA_keN8vWP*SAXU`*NTdzuz+fuZ`=|0w7F4%g4IML=RyKwqNk%+JoWx8Iokyk)sIExPY;=H}NpJ@7c5}sQMyvV^(yYa#d6Qp7 zLcJXJG>o~B$Is5)yk}@q)U3D##jRS5;x9hPdRNF}p=3ke;*iY@IVn%Yn_;O=(UBTO z6B>oV`imcAuLAp_*fv8g#*VowY6EVk;qf)Fy$03-MoD;0q7OeNF_RSz*=fs>$#lVG zzy(e%rf{s3B1+dI6C5+Bd}gSF(37z##w`@3PoOmF3Pc<$Ddt8O!5ci0z)O5rU^HMg z!)pQCp(u$Xt?k8X*n)507KCZ&ERI%` zNo6&5ipnX%(6GONg;j~}?OA|lCRJ}P$DEFBDIS;N{#S%$Yy$g8u(fb1eX_qT{%j_} za+r#=C~$c?FWYf*=)nSNzXbpI3EaA~uPltj>Df1B zA-lqP(g!C!3xZN?IzyD=8z8A*#LT=0>c;$q1%Pis;Iv9S;iT=gkUrJ;>2v1re=R z0}8vl-BXB`xMI3BBUU>iu6t5kJL8)-2W~$ZS&Qj`C#A5nCfW##0FSU7TV}>Ya)AxQ zQpMLa{8vi&_;Bq1(J@EDITG4Z6ps~PR%{#h5PGKzMjWuo%=I)9&b6=-@LO`!$OB(x z!fy-v9FTLv`qu&fR%~C6_N15nha1AOS)}v+o_LX3r(_Ln;F|*;>X(haEbRH1U z!~5ni!(0kqKx&3*{A?0U7HCNbJu86jY#7|hLb-vup+7Sxl0Q1#3ceXU+!|YFL81=G z;V2P!gdv!N?hJhk%m>eOhr=V*yDIZJG}aTZUioJScxu-S?NspF$EvX z@B;7+AWK#ZpBTL%w~VTakDQPzFA~IfZyMuoWdi?zqq^aOVhRC0wL03Of3Hl3WC|We zrhw<@)E3wTP6wnb)=ThlOyum)WyljKuZ7!S>cy1`uB2#*50nLY3W+ImDFb_FcRMGA zD}7OG@E)Z==t(ZBn1$&?sdKE~CZ`zMLOpr$028n(yD|^HZVnj?&Cvk5)I|Z=3CA@T ztOJjvIGiwv0VaN_v%>#UgA`!jIKik*kf*|&DT5_5=Cm890jUG~>Cn?K7eh{FQiwB7 z`TAdm^i#2ZD7N1OA9sW9#KhiVj7Zju=f#76#wfh55L_mpAis+w#a1WNdDDhdp z{mB-NfSgH|`W1*{e^<*T1FI@paa_A2ZNNy5o)$h|1^@9cL;mMLON!U;86!Jt)d)YL z8zjfx3?3uS36#(kTyXTkL+R#%-(JAylYE3{Q8KzS9Udumy#s>9skGl47?xm|?(`AN-65!&J2iEjXfg8v;kDR}2kQ2Ujl zYjS0WZsxGg=xY%{R^V{@pH+xKe%skZfuaJ-?BXM7gPeiI?D;0-jO!`zmq1P7l{U?7 z3AQib{-@z~O_Z^a+>zZd9XLgCRu+pc9n(B35M%qgXk{GBaf#rp40P!* z_(^-2hcf*`6#JkaraEz?muMu9Z#zL@UCqX2|pI%W)In`$%Dd zeiYc4=xe~9jxnh6Z5=otf^>J_#!K=N9ss$MYu-#TkdU(>*9F@Rb7xgryW?j|2%Qr4 zgP}IRx}yoqKLGnvDI{5r?VL!B`_Zu}%oQ_pJSP_F@=!rajH|o?QWXkXe^#8gnrORQ z@0d;p+F%rL2@m)fj_X|5UV@tv(dxpE#UlfGMwpO>NJ3ZGBm_tyz?)7eg@Mo)e3EFE z8-37S8Sw1h;0WeUbH06qY?a>w>0A2ggSBZ@3;K zo|YqGL@cI1DXba0xrhIb?7rICcRF{lDepI_zYXfD8)MY|_%2F2O?t zTNHwCh={*+BZ*T2LAV5jdsVx*hiB58BQ+A&R_DH^XGPux4YV9pD}xMNu-jAy-;&JPTn!Wl^nvWi}rpI&yI)&-F`i?6hvRaC_Xkz`hgCwlItWxbxfa zpO&zVipS?bDnM1kOB82fegcJ}3Rwz&6{a>SSD31b*Okzk;?j(@0a6V9U65wQz9v)* zRXgmrg;_FQT`}OeGE+IXOuK6uf^`b=$-IdfYr%4(Sne#L&^Cdq{fZ+QlRA$r^Gtr* zK~;f#0NY)lM?(LpuxDteE&|+o5!`;u@Kf=9EsSo^i~IOl6T@UW19~iUNjd0xDVz`L zq^`cU1lt7bCV1{77Uo!wr9%Sr#5IPFV2A9SG}bAoCrOb-XtQnxd_Nt#C~{GFB}>2^ zNH^g46s(Jkmc9Zb{NJXhZ8`b{+)Qu>p;_j@S(sp_phX=2Qm8O4TSFh_$tlck9LCm4Gvl(<5QZh(lWt^1!<|%>}wx~R4TVf~A$`H!kA96(Xj(2y#u_x2x*cFIy zzq+4_T}e5cl}!E=5w#w3{Sj(>3j0X9OI7etj+926!ZTBmtudx*$_gJltK0h!w8p%3 zbAlIeaWwVT4;(#Ve-fl`N_(PIr6>PJNY4H;w6-uBE5%k8$|EBmgjT%ZcwUMRpSbk} zpWM?Ip=x@B3N|lR`qCY#am>9f$17|oNgR)J;phe#3#X6?jhMVNPC(JsnXq+ZAQBRb zxgJ1{A5BnN!oS+YDGO^F(gxJSG5!tE!AlB_*w~{1XLtPB7Mdp9Rd7@atF9tQ)vzy4 zNYaakr^a}F?Si>+pXxED+j@wtCii}C@~c*k>1XDeC||&SEm#9GIQeKx;giB3QN$WV zP}SJ6hXNauSqW+sNev}vXn~Uz7uv!qD3CgfqTL)`9CbN967cgc(zlkw`X_#l~I)=8EshPCRE8sZ!rNAYlgja=01t;%I%Xh3*qGIkYGIxFy^t zr*thV`kk<_vk;@YnZTp7tA)=OLsNkI1X_1|cZa#+UVydn_c>DRN}JLm+&EeJGQ(F_ zr0)WMEGC~7LsyCh+f3M3SbdkGtHac=CsOD7%IMeIey8{p#rC8qbE!g(Ne_9RkPqji zWhN{SlAbDR=ZJruEPCsp=BOzf89z9sGode7-eWhD5DN3c_i9)<1jXv`Y}g^V?F)j6 zvkA&Ki_mc>+99Y3$Y;X*DZpO~xfwhlP^&xi#b;z}%q2;Q!p`YzG?0bQN2Q8?ZhR(+ zggrRUZW$De*NYL@M4+=NIB3*i)8I~6&t&ZRpG~nfQay|FQr67(C}+V{Np6kcM5Gkr z!RBa}VdMmfV(7|(pUPRdn9$kJ#97DUFn3p_4xndH}-f%|IZAtT8#?uthQfV>6|l$8tx8cKd6cu4uOieZuMz8rXE00&SX3)TheN@&I+ zMiLdo+Xdv)kS{}iEoh&#kmL+;M|v*QYeC&GOG4We*O9Olfo+O2JI=eqcdAWaBap*~ z@ufF1^*@d7hd(#LB?}Vj>#rk0J{HOne10qDKLzXQNZrs4IMvX;9F{WVhhY2UIBlxc=QCU_-f=hP^Go0(%l$qu^-dJ1dP2m1G!*1j=({f*W3RAIsndC&M$bGpVRG38l#|_yL>a>JAl3 z$a_*O;{mCIleUx+$%GT=aC~IJ?GXGl;OT~(6i4BDZGrpe?EHJgol}?q@$~AjYDjZn z{jY+}4PFIb_l!{lW8*Fy(J$yC`BEdVLpzJu1YlIbnG9A8A|&EfkBIBTajVR*m>^bo z8Sre8C9r^`3!YivNK@D)XjRdU1>GmgQmiu}ORjWKg}0;qBhyQ8y!Zxh%6;I?3OMIHjdTc#WjD;Rm+py)0uKKlbmU%)I1+xh&d8Y7>< zVsmt^7Zgk}9Znjk53>2wU2uH@SLO7en&1GZZ=BKzlCtQs%*jBy@tG0lIKCK@HESm` zWOPn5>qUK)!C^_zlHj{CN>>&5Fa^3HH4Do ze=sK7l^U#51!+sDWuc~pO$AN3y8Bwxw8;QwqYles0HI1AN%}XXelBSB8f(K`F5v!B zbN3gXt&5qw!q9L&aIY3+^zFVEk^5~ zfE}wF_oVO6mb5eK2A&(ceGy;x7#U0$kfw?JOyFkp~M$t%G5#3l6wE!jZ+@~*!!WMTu5(1*j1Sb?9o1oSO6yTcN zq2@_t@S`!QE>A5XFT68FwHzL7h~&kHlons=%!x+BJT7fVy%<7sEyrs(5)369ZpO=}89_Aa{9N6^bi7I}$W4G6$)yPkpSpm%qg%qC zBjFP69MR*rLtuEh?WyNcR+C+x_B%Z6+2TH>db- z8Su3M*M~zs4C>Uoq`?OA?Xc(-$PZ4>EO5)d(OOq0R{##~&Wn==GX9Jh?-0}G5KzB@ zV!I`3bbbU-=LD^M*Dg43z*Iw-g5H>?lEL*k{kB|-=)x6dYKj{YYBp>z zWK|@KSnUMt_Rf`HQYsrB{&QuEu)0R(iG|iEmp>f!q)4J|1>+vM;7X-wPYZWY$TbVF z=jd#j!Uj|NG|_LF#4&^*6LlaJ!vO+>Qs&4U2avlUx3_0iAc_23k%Z->N+AbSqBE(l z-HAYTP-QPB=qtc4;Yp`cgCQfDxRch&DpeQ9>}AkvKEr!oX6&3hV_VZh0#9i;5^Xv8 z67RhHmxTrYfQ#Zs!b8HLIx>de601lSy{689`|A=;!4 zVLFr0lF%hmnqV6V(7=qwemrNvj)XJ>wHx|0m@9H-M>$Hu*h0YTTz{#A#cKGkQR#5U zDV*$-LhS{dqA>$)MSf5#QKq8y4DW$k76yLRi&m_uj(?Vn5=E9-U~M}m2x$s39L-~t z8Sod&z|#bNInoov30I)(2@*sz7ji{Vfa3-vPw=0P*&MwwsAc^H_(-3}taXn*_U$Khyd=*yA56v+~{6`1xU*kz%JL3b8?`9+ABSfpgS zaXKpeuF8kPWOyOKpAs>qQK@F2GVC&?s_@chXR{*SrV}4W2nj&`Ba>OnNn-X_rSe#0@*JIeg-ck z3*@mlj_c%oa|>Ly4BksEzt`be87*5Cmn#7>_Ai091ycuSC+-WL5Oq73ap0Ev6yQ~P z$nQ$7z2KL#5-OZ>JcnRB6>}xbO;C&BLzzDSG3J-&&}!`L8fyo}wfHmK657FT{~bFj zizEA|FRadk^4u1Z0{`%Y-^L8?eztctx0@U6)0@1J`kV+Lm;k*|p0Xu$EcV2c>( z`<=12m?UDka`#;?;MopH&AU4aGEo#FzM|u{*Gmaa^-{!1L&3{g`s15)NCnj#phg9No~f^3Qc#Yfoip3Tq} zJ;gcEdUV@~cDW6$AyJg zH5^lLtIFr1JD$mb&xFqppdLb=f-}7>WfDQh%i%AF4nyvSI_P(A!h#mdm{$RgqJ2~7 z#!1dK71I^}$K9YAxKcvj1h2{oM_z)*P~aHXg{wR)DR(!o8`S6+0 zZ-S5NcqC3RPX{hgThy~5iJ-QG-1QeH#kyi!=E{W?rh26fzAq4Ar>9=2&E*T?^lm@UVn`AHsk7ogpXM z_yNsDk<9{kubQ~SP&Mq!kza4-x+4df%atOE2rNSmAOEu0flkAvB#iYyw{Dw+)d7Kf z4g7J?+{#;YWvt#(oZ z-KB`>C1i-jMRj&KrE@&|4yUNaP^#eSf;pmtj?_ScHrPdJQT`&TxOU+CFpO%bP;91{ z!31Cj!LhgnSg@<0Wnx(Ka%{3-$uL6E{8B|vicJYJ+LmMPinfIfEV2+<1?0iQ-Q6%d z$M*}47bdo@cuc|XZgevYx+juQ1^jvhlGPGkW5O3X+GQD}k^ZHpAl*pn!%j@{V^`P# zoKM4ShWQu2lkY<#>5C*vsdPmY2Og(lHAabw(XpZ63-Q{h8sC@V(G0h>@Ml;2grKTm zs|L&R9vks;ue6u7q_{S&h*EH6}>I(!x*8II}WjBqDoL zci5unXH|i06l>+FR23&$OnV#@ULB=SUy>G|vv*W;_(PzRnH*_y^nD1aat1{k1co4< zvF%PQE?$cBT<8wG8olOTfVEL|pC_;aM;SC=Vp1~;S;Sr#m6kjN13apW56>9KJB>CjikV@vQu`OhlhuGoXLr|-QcEx~TY2B$30TcTop8rI7&I~$P# z!E+h@dQKdx;4?hj(*^x>%xb6!j6Sp7bMsg<(Swu#df2srwesAX$lSp z#<6hT82LPfU0-R6tw*p^qEHUTb8b#fHg9QR-yQ1|tQReORs{0~%$@5q1sjo+dA{l^ zVKw9E`UYGNgWVkBisN+r*eBM^Bt>>E|8rJM;rG!0WS6WWI6BFy%Yj*y&)235`%*xk zEcCXRP>sfms`|ouqu?`{9bZ$>1|Trx5MF;(4W|*$LPXT9EysEhNAA@zYKW8H;fbKq zzn+St1NUxtB^G?!5>y!Wa0*DH5J#P#?Up!^c&EL-isH(^mU(IS%aEX4T>;3MZi&}2 z3^;|+nNu`g?h#WQxNIw2YtEH~ z$}}9OVMT>1nO3?{dFj^aC{OONJucO3hASslL7^g6Q0i+bl>8O}M4&oY*c`)0_|HS^ zloxk$L&-KSQ&2m8C3sC?G0;13I?0Vlr2mKIol4dspsRdtry>!Btqvu_vX%IcS=z(Ghb`g0cbEXU3|Itq8UkuwE0( zB8kr_U~D1G%~c|Lmg|fRYK62@fYz&E+^KHPri{Ywbt-dTYDO;D-Lzk&b$sA zoNy%NN*!ErK$aV*8RGYl!pev%cx4)6>5&*Ca zLGK0&E8jLr)-<8U;@A|L99;V+$~WI&cEUse(n5o|SM`J!iTv6Ad{;@cEYsd#PN zUurVk)Ue4Szq=*kP=l|X7CSXZh-F~Ex>9~Sk($>9`&`J8f;62wpm|rEcfqwAef@!p zH?S%~FfTN6v|$vqc#y@?;0|mxw$|@>M!A669JMhV5>c%Ms7e`)9uJaD) zGhQrV+FLSSP#WETbH}3zK3@j;T3Bmgb7oVU03|V_*K_{cD6SM{jzZ9oF0R-5O)w9_ zd6qlsOvnkiZzO=t;n<&w{pIkvXyM7MGEdGuL6PgcNGus#NhT<2Z6O>NLjqTd-zAK!?O`-zP`Mc|_p^b}8mm`_C$9 z6~DaV@5{q8FpEP9J=|$=DuxL0$PqEEo#}2q^WL>WeDn31CL*S^ZS>8T(D1(CBR;2jSC`Gnb+gX}pqP!BB*MJ|d>tx}gn%k!nx} z=cF)kC4y^?!UKBG33VsuqTLh=1OjDE@JvylXC6`rf@EsI_N3~s{~%L-s-S~`O;QDQ zXVEP~uogSbZ7~38EJruTU1?j4j~e1E0xS#6V~nc)t9ZKI1NaFJ&5`|W$?#I_cnhB3HBTows zzQDdYCCH2GK_Be;{$2sn&YUeKM9!kJ&c&vxWD2Q`YbI-svB|Hgko2I)Ef!F(39O$6 z{0SJ0%>BdZ^tu82A$Zixopcx6f;iajz*U%iaRWP)E1Xw7(SEoG;@OoxdTq3^!MJ|v zDI&;-08RL`6|O=`Pz(&_Z9hP2axWsB3*6`?mrQyk{73cO;S zuupo#!z6^79sA8-74R=X`lhvTCWYJt^{&)Y^ub3Kve; z*M+g@13>v&D4~(C4A?#4d?b`DGE3ayiF&uHf;2VkycE%|n_^`}ndEeICGvh$1ksUa zMoth6BZx+;pgt1DRHX37noUd?@;Pyif&a9MTUGqFaSW2(Q9z_2R)X}%U0%ePit*N- zg@@s)R6PrV-qszp8`pzAb*RV83sLYDdUv+_Z;9CrRca$RlN<%PS5fNG(7RZDCEv zw+m8LW@l%?l^kDK=w<{LspGf4;s13d+^$ZSL4PYv9uz_iXsi3B59bupoqYMNkWAX6i8( z-ni!%=k&-YEpe_KgKJE--bH{oZ9l3+bq*E0cE#A}RW|2kKa0UOc5m-)K3$4BNRML; zOKwDp#JvPzdY&`CJuRX{XXFGVk3g+P;i5C~l;R@*w?S30&NpokxDrWfS{S{&pN29W zBmNmH0jDe4J<`E8A(lEbH8aIZK;l0a&|Wyl-xoH4TXdz&IK0 zR1fv*e;mpywMSvW10=HLUg51S-%VMfiW4h>zK4=eD z5Eh_rz+yD)xGC11D>N}OPJFTGxEfcYqc~7wqV_L=TEg)|AxDtj6r|nl5g-IA(Dh85 zQq_emENN*eQd5*uaJ&+7WS~f*n;|tSy368aaVm8KHFO~`2EQ0+(<@>q(~!DhIL}{k zM|O}r2f@pz7r70AjNnEni_hH!^8)&{U`oXL6N*DJM&VfdvB!khIiV5f88ws>rWKB< zvPc<}4%o$lFuk)-0Mzilk2v$K2y%Aj*c;H5e1TPgegoRgfZM{}9PKH{KMD1k;4=Ww z+BrL5MqbK=n5ru2e$>Px6Cyx`!@;#6SF`(b48I=P?Y!M zDGDtD-y*cG2u|=kb8+gTfVFY_G7!8;gkY9%F35?$Gub&IAYeV{v zgx?-USioRB?|xFGVF1R?@oWYl%^8F;mE)DA$Ths-i61QD@J;S`A)#M_6C4S)#L;Bl zjNKBzl&FiS)wjS;6V;MX?&XkTTpzocIytK88&k><_9!2rgpFXLl^nJ+l6MIA^{dhW z(33$R$dhBn-kAEg6?&cb5;VfbhpivLxh+^R=*k2NX$YAD1-TZ{pI#U8=49FrA^j`? zZO{iV@w4VBkShcw8xP;#+;R5_9F9!{J`$%MDmXpEjY@8-g0U;+2I%3i!UJ|Lj&BJk)i@9fG0ab`etVQ(Ze!U^^95H}{L8d!p&R@&bz1Fh_KE}>wmxZp5e zjN{4Q>a;w8^=j^hhJjLA)%xpf&=S%W&{{K*Ta-W>KL65AG|RV7m?oknPm+|hTz zb_+FhbyLXH|Q|M@0TX4;Ic@b?{WyV`QRJfT$q9*(||Fu6BERpbCl%2K5dg9tl5~-MVGoTX6WRt%G07w-Ig%_C+0#3l_=!l>$B6+(tb}DxB6%5$@2tqg;k{p0%X=8N`@$ ztJ9!D?O5A0zW0Pu8D+oCx5Y6&?+$F8peRW~3ae05pCgU-Iw){wvrT*Db|}}r@=0r7 zFOHH8Nfe%_*xmO8E36ccNtpW+3$LFSB<=V=RPlKSv>J8;9$3JlH?bQ#fFl{o#_*Ii zIC|+xuvS7&RC}X3k~?JSuRxrxz@@aoHE{2nryz@BSErvkF8t-doEqMFj@QpBu)-=Z zO?GfKqCls_A)6!Hd~t1{ga3@7gprg=0p0))slKs7**me7{qEt!&giS zo|!Mr-j#}G{d-6H>+)A#zGJ?b`(#&ZHIyPqkw=hCAyBSVg#3Ip;D4M6zx~mHMn(Tt z3{wR|71Ic*5m)p{G1)YB_W8xJd)tBbh(kEyVYAZGq#H4$`eICNjnIs=2`)|eE2?qN z##rFOar89DdO1G73jWVOCh9qHB??w7zB`!+bS4wWP!tY>1jNIp;RJWB4;7S+qpma+ zzKrVo#BTNZ?a-z8_+%t*1pA;yU>t(ioy_(GK{m$`d;PTa00i z-5v{31|gLZ;HW{}PEHXQETJU{JGs48^kI!X3lH|+CNm9C9Oq81gM^*7M=m--kh;Y` zYsM-mg`auGD&lSwscK|BcnC}_2O%>>0a5rKw-b2vh2e~_Mra$fkf~mpV|2rFrGnG(cBgAOuI0GEcxdBmU1PKCr$J|}`X z`B~NAC14k79ulc_a~gVKPW+YLuOB2>TWBq3E z+rkDWE7r_bwM|+t^XAYv&s=z3ujJUL;sHWl{A7NLbVSrNr%~@IYkJ$*oWV>8q_nOO zS(kS#Y)m~fc`KVM($iqW@ib0QxBho7yeL>Cg5Bmm{{~uJoz_=LT%VOpZH{D4oQ~Im zUkl$^fh{4K;p%Ks$`05j=$qpEA$T5+>!8D+LXe*c=}U1u1=nE4iyJMZ7{I*?(w~lU z&N#!PfXr;}BnmzZ@*l3orm6;s7E_Rd66yfjrr7SlmKQ#%;<^YXa^tR^s|rp(Y5=FI z@m-ynocp{KMS!&NuCZZ|wvcU+0~7G7{;0Ta3GLuSb&JV>4JYPV9oqtOb9ARrx9rrn zNh4e5QgZ!GIjt_y2&V~@EXeZ_z5CDq<$rBHVY#jejGq`XM*{h*f`}M!e zF1S$lcGi`$JLve17S$Scrurop&3b`fx!xi_Bh4sjoCcmsd7Nn_E zSdg5RGwk95-Q)S034R*ve>uD59C2O>{=3Cqd;slo{51`h6uXgBzWbbF zA{AmDae`W8Td->6d@sS#61FCIW_sL@aY8PpE1D; zzjQ9NW@!l0U`*;Fa|GTOTbTe z+_R$YbizR2?@~hkr$V1BdP^|pum8maQQQ;c4m>h&pDbv;K;hn29kv_YOtE@+r{mo~ z-Hn3z!fhNzw?gnJZx~t{Kz?x5=E34stH=5a6NE3`{djaom@p-dWf*W2zrw zZE7j>Si*}mg9T!FHw8vujRr5m1gOSK>(LiB7=CUEXHk?wvFcn7bcS-BDrmdlG{HQ9 zb|r`=+zNRy`%6#9VjZNO*eGq^&HpDI}- zWiA$tg*L!71P`6~NE7~%aUCgEz)XuRo$iAI3KMGz0iiMLQw*WVzLX^v4Hc3)V@nOe0NQLKpwVL3UC(5S9WBELIAcjRQnXUfCz z3PBeHk#gwp#!IcZK&SH35blq3ajdKMH@wL122sQB|19|b{x8Gt|9rulQ3>t_!@^QR zk!Fk)r}FT7XLBzBWLKPf!e3*-uILma(8Y~d%-f>=U@0eD?!nYA5%8l_==yLpb*!By zKrt>Px5%P~0(�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

dH)Peqx| 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%0A01=eI7MLTi3rN_zx-VP%_gZFFjTkrg(J8SOp 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 Xt=Ou(9jybZ00000NkvXXu0mjfa0O|J literal 0 HcmV?d00001 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..4b308e9677 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +# The zlib/libpng License + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. +Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..0620f5a490 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Armory - 3D Game Engine +## [Download](https://armory3d.org/download) • [Release Notes](https://armory3d.org/notes) • [Manual](https://github.com/armory3d/armory/wiki) • [API](https://api.armory3d.org) • [Community](https://armory3d.org/community) + +Armory is an open-source 3D game engine focused on portability, minimal footprint and performance. The renderer is fully scriptable with deferred and forward paths supported out of the box. + +Armory as a Blender add-on provides a full Blender integration, turning it into a complete game development tool and a unified workflow from start to finish. + +![](http://armory3d.org/git/img1.jpg) diff --git a/Shaders/blend_pass/blend_pass.json b/Shaders/blend_pass/blend_pass.json new file mode 100644 index 0000000000..163a86ec68 --- /dev/null +++ b/Shaders/blend_pass/blend_pass.json @@ -0,0 +1,17 @@ +{ + "contexts": [ + { + "name": "blend_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "../include/pass_copy.frag.glsl" + } + ] +} diff --git a/Shaders/bloom_pass/bloom_pass.json b/Shaders/bloom_pass/bloom_pass.json new file mode 100644 index 0000000000..6ec3f1e0da --- /dev/null +++ b/Shaders/bloom_pass/bloom_pass.json @@ -0,0 +1,62 @@ +{ + "contexts": [ + { + "name": "downsample_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + }, + { + "name": "currentMipLevel", + "link": "_downsampleCurrentMip" + }, + { + "name": "BloomThresholdData", + "link": "_BloomThresholdData", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "downsample_pass.frag.glsl" + }, + { + "name": "upsample_pass", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_zero", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + }, + { + "name": "currentMipLevel", + "link": "_upsampleCurrentMip" + }, + { + "name": "sampleScale", + "link": "_bloomSampleScale" + }, + { + "name": "PPComp11", + "link": "_PPComp11", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "upsample_pass.frag.glsl" + } + ] +} diff --git a/Shaders/bloom_pass/downsample_pass.frag.glsl b/Shaders/bloom_pass/downsample_pass.frag.glsl new file mode 100644 index 0000000000..925af66e03 --- /dev/null +++ b/Shaders/bloom_pass/downsample_pass.frag.glsl @@ -0,0 +1,79 @@ +#version 450 + +#include "compiled.inc" // bloomKnee, bloomThreshold +#include "std/resample.glsl" + +uniform sampler2D tex; +uniform vec2 screenSizeInv; +uniform int currentMipLevel; + +#ifdef _CPostprocess + uniform vec4 BloomThresholdData; // Only filled with data if currentMipLevel == 0 +#endif + +in vec2 texCoord; + +out vec4 fragColor; + +const float epsilon = 6.2e-5; // see https://github.com/keijiro/KinoBloom/issues/15 + +#ifdef _BloomAntiFlicker + const bool antiFlicker = true; +#else + const bool antiFlicker = false; +#endif + +void main() { + if (antiFlicker && currentMipLevel == 0) { + #ifdef _BloomQualityHigh + fragColor.rgb = downsample_13_tap_anti_flicker(tex, texCoord, screenSizeInv); + #else + #ifdef _BloomQualityMedium + fragColor.rgb = downsample_dual_filter_anti_flicker(tex, texCoord, screenSizeInv); + #else // _BloomQualityLow + fragColor.rgb = downsample_box_filter_anti_flicker(tex, texCoord, screenSizeInv); + #endif + #endif + } + else { + #ifdef _BloomQualityHigh + fragColor.rgb = downsample_13_tap(tex, texCoord, screenSizeInv); + #else + #ifdef _BloomQualityMedium + fragColor.rgb = downsample_dual_filter(tex, texCoord, screenSizeInv); + #else // _BloomQualityLow + fragColor.rgb = downsample_box_filter(tex, texCoord, screenSizeInv); + #endif + #endif + } + + if (currentMipLevel == 0) { + // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.2 + // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 + + float brightness = max(fragColor.r, max(fragColor.g, fragColor.b)); + + #ifdef _CPostprocess + // Only apply precalculation optimization if _CPostprocess, otherwise + // the compiler is able to do the same optimization for the constant + // values from compiled.inc without the need to pass a uniform + float softeningCurve = brightness - BloomThresholdData.y; + softeningCurve = clamp(softeningCurve, 0.0, BloomThresholdData.z); // "connect" to hard knee curve + softeningCurve = softeningCurve * softeningCurve * BloomThresholdData.w; + + float contributionFactor = max(softeningCurve, brightness - BloomThresholdData.x); + #else + float softeningCurve = brightness - bloomThreshold + bloomKnee; + softeningCurve = clamp(softeningCurve, 0.0, 2.0 * bloomKnee); + softeningCurve = softeningCurve * softeningCurve / (4 * bloomKnee + epsilon); + + float contributionFactor = max(softeningCurve, brightness - bloomThreshold); + #endif + + contributionFactor /= max(epsilon, brightness); + + fragColor.rgb *= contributionFactor; + } + + fragColor.a = 1.0; +} diff --git a/Shaders/bloom_pass/upsample_pass.frag.glsl b/Shaders/bloom_pass/upsample_pass.frag.glsl new file mode 100644 index 0000000000..ea98a2c89f --- /dev/null +++ b/Shaders/bloom_pass/upsample_pass.frag.glsl @@ -0,0 +1,39 @@ +#version 450 + +#include "compiled.inc" // bloomStrength +#include "std/resample.glsl" + +uniform sampler2D tex; +uniform vec2 screenSizeInv; +uniform int currentMipLevel; +uniform float sampleScale; + +#ifdef _CPostprocess + uniform vec3 PPComp11; +#endif + +in vec2 texCoord; + +out vec4 fragColor; + +void main() { + #ifdef _BloomQualityHigh + fragColor.rgb = upsample_tent_filter_3x3(tex, texCoord, screenSizeInv, sampleScale); + #else + #ifdef _BloomQualityMedium + fragColor.rgb = upsample_dual_filter(tex, texCoord, screenSizeInv, sampleScale); + #else // _BloomQualityLow + fragColor.rgb = upsample_4tap_bilinear(tex, texCoord, screenSizeInv, sampleScale); + #endif + #endif + + if (currentMipLevel == 0) { + #ifdef _CPostprocess + fragColor.rgb *= PPComp11.x; + #else + fragColor.rgb *= bloomStrength; + #endif + } + + fragColor.a = 1.0; +} diff --git a/Shaders/blur_adaptive_pass/blur_adaptive_pass.frag.glsl b/Shaders/blur_adaptive_pass/blur_adaptive_pass.frag.glsl new file mode 100644 index 0000000000..2afa8b2e22 --- /dev/null +++ b/Shaders/blur_adaptive_pass/blur_adaptive_pass.frag.glsl @@ -0,0 +1,32 @@ +// Exclusive to SSR for now +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D tex; +uniform sampler2D gbuffer0; // Roughness + +uniform vec2 dirInv; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + float roughness = textureLod(gbuffer0, texCoord, 0.0).b; + // if (roughness == 0.0) { // Always blur for now, non blured output can produce noise + // fragColor.rgb = textureLod(tex, texCoord).rgb; + // return; + // } + if (roughness >= 0.8) { // No reflections + fragColor.rgb = textureLod(tex, texCoord, 0.0).rgb; + return; + } + + fragColor.rgb = textureLod(tex, texCoord + dirInv * 2.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord + dirInv * 1.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 1.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 2.5, 0.0).rgb; + fragColor.rgb /= vec3(5.0); +} diff --git a/Shaders/blur_adaptive_pass/blur_adaptive_pass.json b/Shaders/blur_adaptive_pass/blur_adaptive_pass.json new file mode 100644 index 0000000000..9b06466a51 --- /dev/null +++ b/Shaders/blur_adaptive_pass/blur_adaptive_pass.json @@ -0,0 +1,84 @@ +{ + "contexts": [ + { + "name": "blur_adaptive_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2xInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_adaptive_pass.frag.glsl" + }, + { + "name": "blur_adaptive_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_adaptive_pass.frag.glsl" + }, + + + { + "name": "blur_adaptive_pass_x2", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2x2Inv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_adaptive_pass.frag.glsl" + }, + { + "name": "blur_adaptive_pass_y3", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2y3Inv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_adaptive_pass.frag.glsl" + }, + { + "name": "blur_adaptive_pass_y3_blend", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "links": [ + { + "name": "dirInv", + "link": "_vec2y3Inv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_adaptive_pass.frag.glsl" + } + ] +} diff --git a/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.frag.glsl b/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.frag.glsl new file mode 100644 index 0000000000..210921445e --- /dev/null +++ b/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.frag.glsl @@ -0,0 +1,50 @@ +// Exclusive to volumetric light for now +#version 450 + +#include "compiled.inc" + +uniform sampler2D tex; +uniform vec2 dir; +uniform vec2 screenSize; + +in vec2 texCoord; +out vec4 fragColor; + +const float weight[10] = float[] (0.132572, 0.125472, 0.106373, 0.08078, 0.05495, 0.033482, 0.018275, 0.008934, 0.003912, 0.001535); + +float normpdf(float x, float sigma) { + return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma; +} + +float normpdf3(vec3 v, float sigma) { + return 0.39894 * exp(-0.5 * dot(v, v) / (sigma * sigma)) / sigma; +} + +void main() { + vec2 step = (dir / screenSize.xy); + vec3 colf = textureLod(tex, texCoord, 0.0).rgb * weight[0]; + + float col; + float res = 0.0; + float sumfactor = 0.0; + float factor; + float f = 1.0 / normpdf(0.0, 1.0); + + for (int i = 1; i < 10; i++) { + float fw = f * weight[i]; + vec2 s = step * (float(i) + 0.5); + + col = textureLod(tex, texCoord + s, 0.0).r; + factor = normpdf3(col - colf, 1.0) * fw; + sumfactor += factor; + res += factor * col; + + col = textureLod(tex, texCoord - s, 0.0).r; + factor = normpdf3(col - colf, 1.0) * fw; + sumfactor += factor; + res += factor * col; + } + + res /= sumfactor; + fragColor = vec4(volumAirColor * res, 1.0); +} diff --git a/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.json b/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.json new file mode 100644 index 0000000000..2b127184d2 --- /dev/null +++ b/Shaders/blur_bilat_blend_pass/blur_bilat_blend_pass.json @@ -0,0 +1,29 @@ +{ + "contexts": [ + { + "name": "blur_bilat_blend_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "dir", + "link": "_vec2y" + }, + { + "name": "screenSize", + "link": "_screenSize" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_bilat_blend_pass.frag.glsl" + } + ] +} diff --git a/Shaders/blur_bilat_pass/blur_bilat_pass.frag.glsl b/Shaders/blur_bilat_pass/blur_bilat_pass.frag.glsl new file mode 100644 index 0000000000..4203559be3 --- /dev/null +++ b/Shaders/blur_bilat_pass/blur_bilat_pass.frag.glsl @@ -0,0 +1,49 @@ +// Exclusive to volumetric light for now +#version 450 + +#include "compiled.inc" + +uniform sampler2D tex; +uniform vec2 dir; +uniform vec2 screenSize; + +in vec2 texCoord; +out float fragColor; + +const float weight[10] = float[] (0.132572, 0.125472, 0.106373, 0.08078, 0.05495, 0.033482, 0.018275, 0.008934, 0.003912, 0.001535); + +float normpdf(float x, float sigma) { + return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma; +} + +float normpdf3(vec3 v, float sigma) { + return 0.39894 * exp(-0.5 * dot(v, v) / (sigma * sigma)) / sigma; +} + +void main() { + vec2 step = (dir / screenSize.xy); + vec3 colf = textureLod(tex, texCoord, 0.0).rgb * weight[0]; + + float col; + float sumfactor = 0.0; + float factor; + float f = 1.0 / normpdf(0.0, 1.0); + fragColor = 0.0; + + for (int i = 1; i < 10; i++) { + float fw = f * weight[i]; + vec2 s = step * (float(i) + 0.5); + + col = textureLod(tex, texCoord + s, 0.0).r; + factor = normpdf3(col - colf, 1.0) * fw; + sumfactor += factor; + fragColor += factor * col; + + col = textureLod(tex, texCoord - s, 0.0).r; + factor = normpdf3(col - colf, 1.0) * fw; + sumfactor += factor; + fragColor += factor * col; + } + + fragColor /= sumfactor; +} diff --git a/Shaders/blur_bilat_pass/blur_bilat_pass.json b/Shaders/blur_bilat_pass/blur_bilat_pass.json new file mode 100644 index 0000000000..7a15c2c3a6 --- /dev/null +++ b/Shaders/blur_bilat_pass/blur_bilat_pass.json @@ -0,0 +1,67 @@ +{ + "contexts": [ + { + "name": "blur_bilat_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dir", + "link": "_vec2x" + }, + { + "name": "screenSize", + "link": "_screenSize" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_bilat_pass.frag.glsl" + }, + { + "name": "blur_bilat_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dir", + "link": "_vec2y" + }, + { + "name": "screenSize", + "link": "_screenSize" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_bilat_pass.frag.glsl" + }, + { + "name": "blur_bilat_pass_y_blend", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "dir", + "link": "_vec2y" + }, + { + "name": "screenSize", + "link": "_screenSize" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_bilat_pass.frag.glsl" + } + ] +} diff --git a/Shaders/blur_edge_pass/blur_edge_pass.frag.glsl b/Shaders/blur_edge_pass/blur_edge_pass.frag.glsl new file mode 100644 index 0000000000..20bea60580 --- /dev/null +++ b/Shaders/blur_edge_pass/blur_edge_pass.frag.glsl @@ -0,0 +1,44 @@ +// Exclusive to SSAO for now +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D tex; +uniform sampler2D gbuffer0; + +uniform vec2 dirInv; // texStep + +in vec2 texCoord; +out float fragColor; + +// const float blurWeights[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); +const float blurWeights[10] = float[] (0.132572, 0.125472, 0.106373, 0.08078, 0.05495, 0.033482, 0.018275, 0.008934, 0.003912, 0.001535); +const float discardThreshold = 0.95; + +void main() { + vec3 nor = getNor(textureLod(gbuffer0, texCoord, 0.0).rg); + + fragColor = textureLod(tex, texCoord, 0.0).r * blurWeights[0]; + float weight = blurWeights[0]; + + for (int i = 1; i < 8; ++i) { + float posadd = i;// + 0.5; + + vec3 nor2 = getNor(textureLod(gbuffer0, texCoord + i * dirInv, 0.0).rg); + float influenceFactor = step(discardThreshold, dot(nor2, nor)); + float col = textureLod(tex, texCoord + posadd * dirInv, 0.0).r; + float w = blurWeights[i] * influenceFactor; + fragColor += col * w; + weight += w; + + nor2 = getNor(textureLod(gbuffer0, texCoord - i * dirInv, 0.0).rg); + influenceFactor = step(discardThreshold, dot(nor2, nor)); + col = textureLod(tex, texCoord - posadd * dirInv, 0.0).r; + w = blurWeights[i] * influenceFactor; + fragColor += col * w; + weight += w; + } + + fragColor = fragColor / weight; +} \ No newline at end of file diff --git a/Shaders/blur_edge_pass/blur_edge_pass.json b/Shaders/blur_edge_pass/blur_edge_pass.json new file mode 100644 index 0000000000..0d9ecf85bf --- /dev/null +++ b/Shaders/blur_edge_pass/blur_edge_pass.json @@ -0,0 +1,74 @@ +{ + "contexts": [ + { + "name": "blur_edge_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2xInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_edge_pass.frag.glsl" + }, + { + "name": "blur_edge_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_edge_pass.frag.glsl" + }, + { + "name": "blur_edge_pass_y_blend", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "destination_color", + "blend_destination": "blend_zero", + "blend_operation": "add", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_edge_pass.frag.glsl" + }, + + { + "name": "blur_edge_pass_y_blend_add", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_edge_pass.frag.glsl" + } + ] +} diff --git a/Shaders/blur_pass/blur_pass.frag.glsl b/Shaders/blur_pass/blur_pass.frag.glsl new file mode 100644 index 0000000000..48f0852912 --- /dev/null +++ b/Shaders/blur_pass/blur_pass.frag.glsl @@ -0,0 +1,23 @@ +#version 450 + +uniform sampler2D tex; + +uniform vec2 dirInv; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor.rgb = textureLod(tex, texCoord + dirInv * 5.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord + dirInv * 4.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord + dirInv * 3.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord + dirInv * 2.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord + dirInv * 1.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 1.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 2.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 3.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 4.5, 0.0).rgb; + fragColor.rgb += textureLod(tex, texCoord - dirInv * 5.5, 0.0).rgb; + fragColor.rgb /= vec3(11.0); +} diff --git a/Shaders/blur_pass/blur_pass.json b/Shaders/blur_pass/blur_pass.json new file mode 100644 index 0000000000..71182fddc0 --- /dev/null +++ b/Shaders/blur_pass/blur_pass.json @@ -0,0 +1,66 @@ +{ + "contexts": [ + { + "name": "blur_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2xInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_pass.frag.glsl" + }, + { + "name": "blur_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_pass.frag.glsl" + }, + + + { + "name": "blur_pass_x2", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2x2Inv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_pass.frag.glsl" + }, + { + "name": "blur_pass_y2", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2y2Inv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "blur_pass.frag.glsl" + } + ] +} diff --git a/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.frag.glsl b/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.frag.glsl new file mode 100644 index 0000000000..176b886877 --- /dev/null +++ b/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.frag.glsl @@ -0,0 +1,78 @@ +#version 450 + +#include "compiled.inc" + +uniform sampler2D tex; + +#ifdef _CPostprocess +uniform vec3 PPComp13; +#endif + +in vec2 texCoord; +out vec4 fragColor; + +vec2 barrelDistortion(vec2 coord, float amt) { + vec2 cc = coord - 0.5; + float dist = dot(cc, cc); + return coord + cc * dist * amt; +} +float sat(float value) +{ + return clamp(value, 0.0, 1.0); +} +float linterp(float t) { + return sat(1.0 - abs(2.0 * t - 1.0) ); +} +float remap(float t, float a, float b ) { + return sat((t - a) / (b - a)); +} +vec4 spectrum_offset(float t) { + vec4 ret; + float low = step(t,0.5); + float high = 1.0 - low; + float minMap = 1.0; + float maxMap = 6.0; + float w = linterp( remap(t, minMap/maxMap, 5.0/maxMap ) ); + ret = vec4(low, 1.0, high, 1.) * vec4(1.0-w, w, 1.0-w, 1.0); + + return pow(ret, vec4(1.0/2.2) ); +} + +void main() { + + #ifdef _CPostprocess + float max_distort = PPComp13.x; + int num_iter = int(PPComp13.y); + #else + float max_distort = compoChromaticStrength; + int num_iter = compoChromaticSamples; + #endif + + // Spectral + if (compoChromaticType == 1) { + float reci_num_iter_f = 1.0 / float(num_iter); + + vec2 resolution = vec2(1,1); + vec2 uv = (texCoord.xy/resolution.xy); + vec4 sumcol = vec4(0.0); + vec4 sumw = vec4(0.0); + for (int i=0; i < num_iter; ++i) + { + float t = float(i) * reci_num_iter_f; + vec4 w = spectrum_offset(t); + sumw += w; + sumcol += w * texture(tex, barrelDistortion(uv, 0.6 * max_distort * t)); + } + + fragColor = sumcol / sumw; + } + + // Simple + else { + vec3 col = vec3(0.0); + col.x = texture(tex, texCoord + ((vec2(0.0, 1.0) * max_distort) / vec2(1000.0))).x; + col.y = texture(tex, texCoord + ((vec2(-0.85, -0.5) * max_distort) / vec2(1000.0))).y; + col.z = texture(tex, texCoord + ((vec2(0.85, -0.5) * max_distort) / vec2(1000.0))).z; + fragColor = vec4(col.x, col.y, col.z, fragColor.w); + } +} diff --git a/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.json b/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.json new file mode 100644 index 0000000000..4e4ad76a9d --- /dev/null +++ b/Shaders/chromatic_aberration_pass/chromatic_aberration_pass.json @@ -0,0 +1,21 @@ +{ + "contexts": [ + { + "name": "chromatic_aberration_pass", + "depth_write": false, + "color_write_alpha": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "PPComp13", + "link": "_PPComp13", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "chromatic_aberration_pass.frag.glsl" + } + ] +} diff --git a/Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl b/Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl new file mode 100644 index 0000000000..e32026b839 --- /dev/null +++ b/Shaders/clear_color_depth_pass/clear_color_depth_pass.frag.glsl @@ -0,0 +1,9 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + gl_FragDepth = 1.0; +} diff --git a/Shaders/clear_color_depth_pass/clear_color_depth_pass.json b/Shaders/clear_color_depth_pass/clear_color_depth_pass.json new file mode 100644 index 0000000000..5a7b5b9322 --- /dev/null +++ b/Shaders/clear_color_depth_pass/clear_color_depth_pass.json @@ -0,0 +1,15 @@ +{ + "contexts": [ + { + "name": "clear_color_depth_pass", + "depth_write": true, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_color_depth_pass.frag.glsl", + "color_attachments": ["_HDR"] + } + ] +} diff --git a/Shaders/clear_color_pass/clear_color_pass.frag.glsl b/Shaders/clear_color_pass/clear_color_pass.frag.glsl new file mode 100644 index 0000000000..c8cf9ad804 --- /dev/null +++ b/Shaders/clear_color_pass/clear_color_pass.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); +} diff --git a/Shaders/clear_color_pass/clear_color_pass.json b/Shaders/clear_color_pass/clear_color_pass.json new file mode 100644 index 0000000000..3c2c5582df --- /dev/null +++ b/Shaders/clear_color_pass/clear_color_pass.json @@ -0,0 +1,15 @@ +{ + "contexts": [ + { + "name": "clear_color_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_color_pass.frag.glsl", + "color_attachments": ["_HDR"] + } + ] +} diff --git a/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl b/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl new file mode 100644 index 0000000000..78cb125e66 --- /dev/null +++ b/Shaders/clear_depth_pass/clear_depth_pass.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + gl_FragDepth = 1.0; +} diff --git a/Shaders/clear_depth_pass/clear_depth_pass.json b/Shaders/clear_depth_pass/clear_depth_pass.json new file mode 100644 index 0000000000..ca318696fb --- /dev/null +++ b/Shaders/clear_depth_pass/clear_depth_pass.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "clear_depth_pass", + "depth_write": true, + "color_write_red": false, + "color_write_green": false, + "color_write_blue": false, + "color_write_alpha": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "clear_depth_pass.frag.glsl", + "color_attachments": ["_HDR"] + } + ] +} diff --git a/Shaders/compositor_pass/compositor_pass.frag.glsl b/Shaders/compositor_pass/compositor_pass.frag.glsl new file mode 100644 index 0000000000..f86716c3e6 --- /dev/null +++ b/Shaders/compositor_pass/compositor_pass.frag.glsl @@ -0,0 +1,617 @@ +#version 450 + +#include "compiled.inc" +#include "std/tonemap.glsl" +#include "std/math.glsl" +#ifdef _CDOF +#include "std/dof.glsl" +#endif +#ifdef _CPostprocess +#include "std/colorgrading.glsl" +#endif + +uniform sampler2D tex; +#ifdef _CDepth +uniform sampler2D gbufferD; +#endif + +#ifdef _CLensTex +uniform sampler2D lensTexture; +#endif + +#ifdef _CLUT +uniform sampler2D lutTexture; +#endif + +#ifdef _AutoExposure +uniform sampler2D histogram; +#endif + +#ifdef _CPostprocess +uniform vec3 globalWeight; +uniform vec3 globalTint; +uniform vec3 globalSaturation; +uniform vec3 globalContrast; +uniform vec3 globalGamma; +uniform vec3 globalGain; +uniform vec3 globalOffset; + +uniform vec3 shadowSaturation; +uniform vec3 shadowContrast; +uniform vec3 shadowGamma; +uniform vec3 shadowGain; +uniform vec3 shadowOffset; + +uniform vec3 midtoneSaturation; +uniform vec3 midtoneContrast; +uniform vec3 midtoneGamma; +uniform vec3 midtoneGain; +uniform vec3 midtoneOffset; + +uniform vec3 highlightSaturation; +uniform vec3 highlightContrast; +uniform vec3 highlightGamma; +uniform vec3 highlightGain; +uniform vec3 highlightOffset; + +uniform vec3 PPComp1; +uniform vec3 PPComp2; +uniform vec3 PPComp3; +uniform vec3 PPComp4; +uniform vec3 PPComp5; +uniform vec3 PPComp6; +uniform vec3 PPComp7; +uniform vec3 PPComp8; +uniform vec3 PPComp14; +uniform vec4 PPComp15; +#endif + +// #ifdef _CPos +// uniform vec3 eye; +// uniform vec3 eyeLook; +// #endif + +#ifdef _CGlare +uniform vec3 light; +uniform mat4 VP; +uniform vec3 eye; +uniform vec3 eyeLook; +uniform float aspectRatio; +#endif + +#ifdef _CTexStep +uniform vec2 texStep; +#endif + +#ifdef _CDistort +uniform float time; +#else +#ifdef _CGrain +uniform float time; +#endif +#endif + +#ifdef _DynRes +uniform float dynamicScale; +#endif + +#ifdef _CCameraProj +uniform vec2 cameraProj; +#endif + +in vec2 texCoord; +// #ifdef _CPos + // in vec3 viewRay; +// #endif +out vec4 fragColor; + +#ifdef _CFog +// const vec3 compoFogColor = vec3(0.5, 0.6, 0.7); +// const float compoFogAmountA = 1.0; // b = 0.01 +// const float compoFogAmountB = 1.0; // c = 0.1 +// vec3 applyFog(vec3 rgb, // original color of the pixel + // float distance, // camera to point distance + // vec3 rayOri, // camera position + // vec3 rayDir) { // camera to point vector + // float fogAmount = compoFogAmountB * exp(-rayOri.y * compoFogAmountA) * (1.0 - exp(-distance * rayDir.y * compoFogAmountA)) / rayDir.y; + // return mix(rgb, compoFogColor, fogAmount); +// } +vec3 applyFog(vec3 rgb, float distance) { + // float fogAmount = 1.0 - exp(-distance * compoFogAmountA); + float fogAmount = 1.0 - exp(-distance * (compoFogAmountA / 100)); + return mix(rgb, compoFogColor, fogAmount); +} +#endif + +#ifdef _CPostprocess +float ComputeEV100(const float aperture2, const float shutterTime, const float ISO) { + return log2(aperture2 / shutterTime * 100.0 / ISO); +} +float ConvertEV100ToExposure(float EV100) { + return 1/0.8 * exp2(-EV100); +} +float ComputeEV(float avgLuminance) { + const float sqAperture = PPComp1[0].x * PPComp1.x; + const float shutterTime = 1.0 / PPComp1.y; + const float ISO = PPComp1.z; + const float EC = PPComp2.x; + + float EV100 = ComputeEV100(sqAperture, shutterTime, ISO); + + return ConvertEV100ToExposure(EV100 - EC) * PI; +} +#endif + +vec4 LUTlookup(in vec4 textureColor, in sampler2D lookupTable) { + + //Clamp to prevent weird results + textureColor = clamp(textureColor, 0.0, 1.0); + + mediump float blueColor = textureColor.b * 63.0; + mediump vec2 quad1; + + quad1.y = floor(floor(blueColor) / 8.0); + quad1.x = floor(blueColor) - (quad1.y * 8.0); + + mediump vec2 quad2; + quad2.y = floor(ceil(blueColor) / 8.0); + quad2.x = ceil(blueColor) - (quad2.y * 8.0); + + highp vec2 texelPosition1; + texelPosition1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); + texelPosition1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); + + highp vec2 texelPosition2; + texelPosition2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); + texelPosition2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); + + lowp vec4 newColor1 = textureLod(lookupTable, texelPosition1, 0.0); + lowp vec4 newColor2 = textureLod(lookupTable, texelPosition2, 0.0); + + lowp vec4 colorGradedResult = mix(newColor1, newColor2, fract(blueColor)); + + return colorGradedResult; + +} + +#ifdef _CVignette +float vignette() { + #ifdef _CPostprocess + float strengthVignette = PPComp14.z; + #else + float strengthVignette = compoVignetteStrength; + #endif + return (1.0 - strengthVignette) + strengthVignette * pow(16.0 * texCoord.x * texCoord.y * (1.0 - texCoord.x) * (1.0 - texCoord.y), 0.2); +} +#endif + +#ifdef _CGlare +// Based on lense flare implementation by musk +// https://www.shadertoy.com/view/4sX3Rs +vec3 lensflare(vec2 uv, vec2 pos) { + vec2 uvd = uv * (length(uv)); + float f2 = max(1.0/(1.0+32.0*pow(length(uvd+0.8*pos),2.0)),0.0)*0.25; + float f22 = max(1.0/(1.0+32.0*pow(length(uvd+0.85*pos),2.0)),0.0)*0.23; + float f23 = max(1.0/(1.0+32.0*pow(length(uvd+0.9*pos),2.0)),0.0)*0.21; + + vec2 uvx = mix(uv, uvd, -0.5); + float f4 = max(0.01-pow(length(uvx+0.4*pos),2.4),0.0)*6.0; + float f42 = max(0.01-pow(length(uvx+0.45*pos),2.4),0.0)*5.0; + float f43 = max(0.01-pow(length(uvx+0.5*pos),2.4),0.0)*3.0; + + uvx = mix(uv, uvd, -0.4); + float f5 = max(0.01-pow(length(uvx+0.2*pos),5.5),0.0)*2.0; + float f52 = max(0.01-pow(length(uvx+0.4*pos),5.5),0.0)*2.0; + float f53 = max(0.01-pow(length(uvx+0.6*pos),5.5),0.0)*2.0; + + uvx = mix(uv, uvd, -0.5); + float f6 = max(0.01-pow(length(uvx-0.3*pos),1.6),0.0)*6.0; + float f62 = max(0.01-pow(length(uvx-0.325*pos),1.6),0.0)*3.0; + float f63 = max(0.01-pow(length(uvx-0.35*pos),1.6),0.0)*5.0; + + vec3 c = vec3(0.0); + c.r += f2 + f4 + f5 + f6; + c.g += f22 + f42 + f52 + f62; + c.b += f23 + f43 + f53 + f63; + return c; +} +#endif + +void main() { + vec2 texCo = texCoord; +#ifdef _DynRes + texCo *= dynamicScale; +#endif + +#ifdef _CLetterbox + #ifdef _CPostprocess + vec3 uColor = vec3(PPComp15.x, PPComp15.y, PPComp15.z); + float uSize = PPComp15.w; + #else + vec3 uColor = compoLetterboxColor; + float uSize = compoLetterboxSize; + #endif + if (texCo.y < uSize || texCo.y > (1.0 - uSize)) { + fragColor.rgb = uColor; + return; + } +#endif + +#ifdef _CFishEye + #ifdef _CPostprocess + const float fishEyeStrength = -(PPComp2.y); + #else + const float fishEyeStrength = -0.01; + #endif + const vec2 m = vec2(0.5, 0.5); + vec2 d = texCo - m; + float r = sqrt(dot(d, d)); + float power = (2.0 * PI / (2.0 * sqrt(dot(m, m)))) * fishEyeStrength; + float bind; + if (power > 0.0) { bind = sqrt(dot(m, m)); } + else { bind = m.x; } + if (power > 0.0) { + texCo = m + normalize(d) * tan(r * power) * bind / tan(bind * power); + } + else { + texCo = m + normalize(d) * atan(r * -power * 10.0) * bind / atan(-power * bind * 10.0); + } +#endif + +#ifdef _CDistort + #ifdef _CPostprocess + float strengthDistort = PPComp14.x; + #else + float strengthDistort = compoDistortStrength; + #endif + float uX = time * strengthDistort; + texCo.y = texCo.y + (sin(texCo.x*4.0+uX*2.0)*0.01); + texCo.x = texCo.x + (cos(texCo.y*4.0+uX*2.0)*0.01); +#endif + +#ifdef _CDepth + float depth = textureLod(gbufferD, texCo, 0.0).r * 2.0 - 1.0; +#endif + +#ifdef _CFXAA + const float FXAA_REDUCE_MIN = 1.0 / 128.0; + const float FXAA_REDUCE_MUL = 1.0 / 8.0; + const float FXAA_SPAN_MAX = 8.0; + + vec2 tcrgbNW = (texCo + vec2(-1.0, -1.0) * texStep); + vec2 tcrgbNE = (texCo + vec2(1.0, -1.0) * texStep); + vec2 tcrgbSW = (texCo + vec2(-1.0, 1.0) * texStep); + vec2 tcrgbSE = (texCo + vec2(1.0, 1.0) * texStep); + vec2 tcrgbM = vec2(texCo); + + vec3 rgbNW = textureLod(tex, tcrgbNW, 0.0).rgb; + vec3 rgbNE = textureLod(tex, tcrgbNE, 0.0).rgb; + vec3 rgbSW = textureLod(tex, tcrgbSW, 0.0).rgb; + vec3 rgbSE = textureLod(tex, tcrgbSE, 0.0).rgb; + vec3 rgbM = textureLod(tex, tcrgbM, 0.0).rgb; + vec3 luma = vec3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + vec2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * + (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) * texStep; + + vec3 rgbA = 0.5 * ( + textureLod(tex, texCo + dir * (1.0 / 3.0 - 0.5), 0.0).rgb + + textureLod(tex, texCo + dir * (2.0 / 3.0 - 0.5), 0.0).rgb); + vec3 rgbB = rgbA * 0.5 + 0.25 * ( + textureLod(tex, texCo + dir * -0.5, 0.0).rgb + + textureLod(tex, texCo + dir * 0.5, 0.0).rgb); + + float lumaB = dot(rgbB, luma); + if ((lumaB < lumaMin) || (lumaB > lumaMax)) fragColor = vec4(rgbA, 1.0); + else fragColor = vec4(rgbB, 1.0); + +#else + + #ifdef _CDOF + #ifdef _CPostprocess + + bool compoAutoFocus = false; + float compoDistance = PPComp3.x; + float compoLength = PPComp3.y; + float compoStop = PPComp3.z; + + if (PPComp2.z == 1){ + compoAutoFocus = true; + } else { + compoAutoFocus = false; + } + + fragColor.rgb = dof(texCo, depth, tex, gbufferD, texStep, cameraProj, compoAutoFocus, compoDistance, compoLength, compoStop); + #else + fragColor.rgb = dof(texCo, depth, tex, gbufferD, texStep, cameraProj, true, compoDOFDistance, compoDOFLength, compoDOFFstop); + #endif + #else + fragColor = textureLod(tex, texCo, 0.0); + #endif + +#endif + +#ifdef _CSharpen + #ifdef _CPostprocess + float strengthSharpen = PPComp14.y; + #else + float strengthSharpen = compoSharpenStrength; + #endif + vec3 col1 = textureLod(tex, texCo + vec2(-texStep.x, -texStep.y) * 1.5, 0.0).rgb; + vec3 col2 = textureLod(tex, texCo + vec2(texStep.x, -texStep.y) * 1.5, 0.0).rgb; + vec3 col3 = textureLod(tex, texCo + vec2(-texStep.x, texStep.y) * 1.5, 0.0).rgb; + vec3 col4 = textureLod(tex, texCo + vec2(texStep.x, texStep.y) * 1.5, 0.0).rgb; + vec3 colavg = (col1 + col2 + col3 + col4) * 0.25; + fragColor.rgb += (fragColor.rgb - colavg) * strengthSharpen; +#endif + +#ifdef _CFog + // if (depth < 1.0) { + // vec3 pos = getPos(depth, cameraProj); + // float dist = distance(pos, eye); + float dist = linearize(depth, cameraProj); + // vec3 eyedir = eyeLook;// normalize(eye + pos); + // fragColor.rgb = applyFog(fragColor.rgb, dist, eye, eyedir); + fragColor.rgb = applyFog(fragColor.rgb, dist); + // } +#endif + +#ifdef _CGlare + if (dot(light, eyeLook) > 0.0) { // Facing light + vec4 lndc = VP * vec4(light, 1.0); + lndc.xy /= lndc.w; + vec2 lss = lndc.xy * 0.5 + 0.5; + float lssdepth = linearize(textureLod(gbufferD, lss, 0.0).r * 2.0 - 1.0, cameraProj); + float lightDistance = distance(eye, light); + if (lightDistance <= lssdepth) { + vec2 lensuv = texCo * 2.0 - 1.0; + lensuv.x *= aspectRatio; + vec3 lensflarecol = vec3(1.4, 1.2, 1.0) * lensflare(lensuv, lndc.xy); + fragColor.rgb += lensflarecol; + } + } +#endif + +#ifdef _CGrain + float x = (texCo.x + 4.0) * (texCo.y + 4.0) * (time * 10.0); + #ifdef _CPostprocess + fragColor.rgb += vec3(mod((mod(x, 13.0) + 1.0) * (mod(x, 123.0) + 1.0), 0.01) - 0.005) * PPComp4.y; + #else + fragColor.rgb += vec3(mod((mod(x, 13.0) + 1.0) * (mod(x, 123.0) + 1.0), 0.01) - 0.005) * compoGrainStrength; + #endif +#endif + +#ifdef _CGrainStatic + float x = (texCo.x + 4.0) * (texCo.y + 4.0) * 10.0; + fragColor.rgb += vec3(mod((mod(x, 13.0) + 1.0) * (mod(x, 123.0) + 1.0), 0.01) - 0.005) * 0.09; +#endif + +#ifdef _CVignette + fragColor.rgb *= vignette(); +#endif + +#ifdef _CExposure + fragColor.rgb += fragColor.rgb * compoExposureStrength; +#endif + +#ifdef _CPostprocess + fragColor.rgb *= ComputeEV(0.0); +#endif + +#ifdef _AutoExposure + float expo = 2.0 - clamp(length(textureLod(histogram, vec2(0.5, 0.5), 0).rgb), 0.0, 1.0); + fragColor.rgb *= pow(expo, autoExposureStrength * 2.0); +#endif + +// Clamp color to get rid of INF values that don't work for the tone mapping below +// The max value is kind of arbitrary (16 bit float max * 0.5), but it should be large enough +fragColor.rgb = min(fragColor.rgb, 65504 * 0.5); + +#ifdef _CPostprocess + + #ifdef _CToneCustom + fragColor.rgb = clamp((fragColor.rgb * (PPComp4.z * fragColor.rgb + PPComp5.x)) / (fragColor.rgb * (PPComp5.y * fragColor.rgb + PPComp5.z) + PPComp6.x), 0.0, 1.0); + #else + if(PPComp4.x == 0){ //Filmic 1 + fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma + } else if (PPComp4.x == 1){ //Filmic 2 + fragColor.rgb = acesFilm(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); + } else if (PPComp4.x == 2){ //Reinhard + fragColor.rgb = tonemapReinhard(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); + } else if (PPComp4.x == 3){ //Uncharted2 + fragColor.rgb = tonemapUncharted2(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); // To gamma + fragColor.rgb = clamp(fragColor.rgb, 0.0, 1.0); + } else if (PPComp4.x == 4){ //None + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); // To gamma + } else if (PPComp4.x == 5){ //Non-Gamma / Linear + fragColor.rgb = fragColor.rgb; + } else if (PPComp4.x == 6){ //HDP + vec3 x = fragColor.rgb - 0.004; + //vec3 x = max(0, fragColor.rgb - 0.004); + fragColor.rgb = (x*(6.2*x+.5))/(x*(6.2*x+1.7)+0.06); + } else if (PPComp4.x == 7){ //Raw + vec4 vh = vec4(fragColor.rgb, 1); + vec4 va = (1.425 * vh) + 0.05; + vec4 vf = ((vh * va + 0.004) / ((vh * (va + 0.55) + 0.0491))) - 0.0821; + fragColor.rgb = vf.rgb / vf.www; + } else if (PPComp4.x == 8){ //False Colors for luminance control + + vec4 c = vec4(fragColor.r,fragColor.g,fragColor.b,0); //Linear without gamma + + vec3 luminanceVector = vec3(0.2125, 0.7154, 0.0721); //Relative Luminance Vector + float luminance = dot(luminanceVector, c.xyz); + + vec3 maxLumColor = vec3(1,0,0); //High values (> 1.0) + //float maxLum = 2.0; Needs to read the highest pixel, but I don't know how to yet + //Probably easier with a histogram too, once it's it in place? + + vec3 midLumColor = vec3(0,1,0); //Mid values (< 1.0) + float midLum = 1.0; + + vec3 minLumColor = vec3(0,0,1); //Low values (< 1.0) + float minLum = 0.0; + + if(luminance < midLum){ + fragColor.rgb = mix(minLumColor, midLumColor, luminance); + } else { + fragColor.rgb = mix(midLumColor, maxLumColor, luminance); + } + + } else { + fragColor.rgb = vec3(0,1,0); //ERROR + } + #endif + +#else + #ifdef _CToneFilmic + fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma + #endif + #ifdef _CToneFilmic2 + fragColor.rgb = acesFilm(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); + #endif + #ifdef _CToneReinhard + fragColor.rgb = tonemapReinhard(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); + #endif + #ifdef _CToneUncharted + fragColor.rgb = tonemapUncharted2(fragColor.rgb); + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); // To gamma + fragColor.rgb = clamp(fragColor.rgb, 0.0, 2.2); + #endif + #ifdef _CToneNone + fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2)); // To gamma + #endif + #ifdef _CToneCustom + fragColor.rgb = clamp((fragColor.rgb * (1 * fragColor.rgb + 1)) / (fragColor.rgb * (1 * fragColor.rgb + 1 ) + 1), 0.0, 1.0); + #endif +#endif + +#ifdef _CBW + // fragColor.rgb = vec3(clamp(dot(fragColor.rgb, fragColor.rgb), 0.0, 1.0)); + fragColor.rgb = vec3((fragColor.r * 0.3 + fragColor.g * 0.59 + fragColor.b * 0.11) / 3.0) * 2.5; +#endif + +// #ifdef _CContrast + // -0.5 - 0.5 + // const float compoContrast = 0.2; + // fragColor.rgb = ((fragColor.rgb - 0.5) * max(compoContrast + 1.0, 0.0)) + 0.5; +// #endif + +// #ifdef _CBrighness + // fragColor.rgb += compoBrightness; +// #endif + +#ifdef _CPostprocess + //Global Values + + float factor = 1; + float colorTempK = globalWeight.x; + vec3 ColorTempRGB = ColorTemperatureToRGB(colorTempK); + + float originalLuminance = Luminance(fragColor.rgb); + vec3 blended = mix(fragColor.rgb, fragColor.rgb * ColorTempRGB, factor); + vec3 resultHSL = RGBtoHSL(blended); + vec3 luminancePreservedRGB = HSLtoRGB(vec3(resultHSL.x, resultHSL.y, originalLuminance)); + fragColor = vec4(mix(blended, luminancePreservedRGB, LUMINANCE_PRESERVATION), 1.0); + + mat3 CCSaturation = mat3 ( //Saturation + globalSaturation.r * shadowSaturation.r, globalSaturation.g * shadowSaturation.g, globalSaturation.b * shadowSaturation.b, //Shadows + Global + globalSaturation.r * midtoneSaturation.r, globalSaturation.g * midtoneSaturation.g, globalSaturation.b * midtoneSaturation.b, //Midtones + Global + globalSaturation.r * highlightSaturation.r, globalSaturation.g * highlightSaturation.g, globalSaturation.b * highlightSaturation.b //Highlights + Global + ); + + mat3 CCContrast = mat3 ( + globalContrast.r * shadowContrast.r, globalContrast.g * shadowContrast.g, globalContrast.b * shadowContrast.b, //Shadows + Global + globalContrast.r * midtoneContrast.r, globalContrast.g * midtoneContrast.g, globalContrast.b * midtoneContrast.b, //Midtones + Global + globalContrast.r * highlightContrast.r, globalContrast.g * highlightContrast.g, globalContrast.b * highlightContrast.b //Highlights + Global + ); + + mat3 CCGamma = mat3 ( + globalGamma.r * shadowGamma.r, globalGamma.g * shadowGamma.g, globalGamma.b * shadowGamma.b, //Shadows + Global + globalGamma.r * midtoneGamma.r, globalGamma.g * midtoneGamma.g, globalGamma.b * midtoneGamma.b, //Midtones + Global + globalGamma.r * highlightGamma.r, globalGamma.g * highlightGamma.g, globalGamma.b * highlightGamma.b //Highlights + Global + ); + + mat3 CCGain = mat3 ( + globalGain.r * shadowGain.r, globalGain.g * shadowGain.g, globalGain.b * shadowGain.b, //Shadows + Global + globalGain.r * midtoneGain.r, globalGain.g * midtoneGain.g, globalGain.b * midtoneGain.b, //Midtones + Global + globalGain.r * highlightGain.r, globalGain.g * highlightGain.g, globalGain.b * highlightGain.b //Highlights + Global + ); + + mat3 CCOffset = mat3 ( + globalOffset.r * shadowOffset.r, globalOffset.g * shadowOffset.g, globalOffset.b * shadowOffset.b, //Shadows + Global + globalOffset.r * midtoneOffset.r, globalOffset.g * midtoneOffset.g, globalOffset.b * midtoneOffset.b, //Midtones + Global + globalOffset.r * highlightOffset.r, globalOffset.g * highlightOffset.g, globalOffset.b * highlightOffset.b //Highlights + Global + ); + + vec2 ToneWeights = vec2(globalWeight.y, globalWeight.z); + + fragColor.rgb = FinalizeColorCorrection( + fragColor.rgb, + CCSaturation, + CCContrast, + CCGamma, + CCGain, + CCOffset, + ToneWeights + ); + + //Tint + fragColor.rgb *= vec3(globalTint.r,globalTint.g,globalTint.b); +#endif + +#ifdef _CLensTex + #ifdef _CLensTexMasking + vec4 scratches = texture(lensTexture, texCo); + vec3 scratchBlend = fragColor.rgb + scratches.rgb; + + #ifdef _CPostprocess + float centerMaxClip = PPComp6.y; + float centerMinClip = PPComp6.z; + float luminanceMax = PPComp7.x; + float luminanceMin = PPComp7.y; + float brightnessExp = PPComp7.z; + #else + float centerMaxClip = compoCenterMaxClip; + float centerMinClip = compoCenterMinClip; + float luminanceMax = compoLuminanceMax; + float luminanceMin = compoLuminanceMin; + float brightnessExp = compoBrightnessExponent; + #endif + + float center = smoothstep(centerMaxClip, centerMinClip, length(texCo - 0.5)); + float luminance = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114)); + float brightnessMap = smoothstep(luminanceMax, luminanceMin, luminance * center); + fragColor.rgb = clamp(mix(fragColor.rgb, scratchBlend, brightnessMap * brightnessExp), 0, 1); + #else + fragColor.rgb += textureLod(lensTexture, texCo, 0.0).rgb; + #endif +#endif + +//3D LUT Implementation from GPUGems 2 by Nvidia +//https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter24.html + +#ifdef _CLUT + fragColor = LUTlookup(fragColor, lutTexture); +#endif +} diff --git a/Shaders/compositor_pass/compositor_pass.json b/Shaders/compositor_pass/compositor_pass.json new file mode 100644 index 0000000000..e743fbb94d --- /dev/null +++ b/Shaders/compositor_pass/compositor_pass.json @@ -0,0 +1,245 @@ +{ + "contexts": [ + { + "name": "compositor_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "eye", + "link": "_cameraPosition", + "ifdef": ["_CGlare"] + }, + { + "name": "eye", + "link": "_cameraPosition", + "ifdef": ["_Disabled_CPos"] + }, + { + "name": "eyeLook", + "link": "_cameraLook", + "ifdef": ["_CGlare"] + }, + { + "name": "eyeLook", + "link": "_cameraLook", + "ifdef": ["_Disabled_CPos"] + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix", + "ifdef": ["_Disabled_CPos"] + }, + { + "name": "light", + "link": "_lightPosition", + "ifdef": ["_CGlare"] + }, + { + "name": "VP", + "link": "_viewProjectionMatrix", + "ifdef": ["_CGlare"] + }, + { + "name": "time", + "link": "_time", + "ifdef": ["_CDistort", "_CGrain"] + }, + { + "name": "texStep", + "link": "_screenSizeInv", + "ifdef": ["_CTexStep"] + }, + { + "name": "dynamicScale", + "link": "_dynamicScale", + "ifdef": ["_DynRes"] + }, + { + "name": "aspectRatio", + "link": "_aspectRatioF", + "ifdef": ["_CGlare"] + }, + { + "name": "lensTexture", + "link": "$lenstexture.jpg", + "ifdef": ["_CLensTex"] + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj", + "ifdef": ["_CCameraProj"] + }, + { + "name": "lutTexture", + "link": "$luttexture.jpg", + "ifdef": ["_CLUT"] + }, + { + "name": "globalWeight", + "link": "_globalWeight", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalTint", + "link": "_globalTint", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalSaturation", + "link": "_globalSaturation", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalContrast", + "link": "_globalContrast", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalGamma", + "link": "_globalGamma", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalGain", + "link": "_globalGain", + "ifdef": ["_CPostprocess"] + }, + { + "name": "globalOffset", + "link": "_globalOffset", + "ifdef": ["_CPostprocess"] + }, + { + "name": "shadowSaturation", + "link": "_shadowSaturation", + "ifdef": ["_CPostprocess"] + }, + { + "name": "shadowContrast", + "link": "_shadowContrast", + "ifdef": ["_CPostprocess"] + }, + { + "name": "shadowGamma", + "link": "_shadowGamma", + "ifdef": ["_CPostprocess"] + }, + { + "name": "shadowGain", + "link": "_shadowGain", + "ifdef": ["_CPostprocess"] + }, + { + "name": "shadowOffset", + "link": "_shadowOffset", + "ifdef": ["_CPostprocess"] + }, + { + "name": "midtoneSaturation", + "link": "_midtoneSaturation", + "ifdef": ["_CPostprocess"] + }, + { + "name": "midtoneContrast", + "link": "_midtoneContrast", + "ifdef": ["_CPostprocess"] + }, + { + "name": "midtoneGamma", + "link": "_midtoneGamma", + "ifdef": ["_CPostprocess"] + }, + { + "name": "midtoneGain", + "link": "_midtoneGain", + "ifdef": ["_CPostprocess"] + }, + { + "name": "midtoneOffset", + "link": "_midtoneOffset", + "ifdef": ["_CPostprocess"] + }, + { + "name": "highlightSaturation", + "link": "_highlightSaturation", + "ifdef": ["_CPostprocess"] + }, + { + "name": "highlightContrast", + "link": "_highlightContrast", + "ifdef": ["_CPostprocess"] + }, + { + "name": "highlightGamma", + "link": "_highlightGamma", + "ifdef": ["_CPostprocess"] + }, + { + "name": "highlightGain", + "link": "_highlightGain", + "ifdef": ["_CPostprocess"] + }, + { + "name": "highlightOffset", + "link": "_highlightOffset", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp1", + "link": "_PPComp1", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp2", + "link": "_PPComp2", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp3", + "link": "_PPComp3", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp4", + "link": "_PPComp4", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp5", + "link": "_PPComp5", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp6", + "link": "_PPComp6", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp7", + "link": "_PPComp7", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp8", + "link": "_PPComp8", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp14", + "link": "_PPComp14", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp15", + "link": "_PPComp15", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "compositor_pass.vert.glsl", + "fragment_shader": "compositor_pass.frag.glsl" + } + ] +} diff --git a/Shaders/compositor_pass/compositor_pass.vert.glsl b/Shaders/compositor_pass/compositor_pass.vert.glsl new file mode 100644 index 0000000000..4e0a095d30 --- /dev/null +++ b/Shaders/compositor_pass/compositor_pass.vert.glsl @@ -0,0 +1,34 @@ +#version 450 + +#include "compiled.inc" + +// #ifdef _CPos + // uniform mat4 invVP; + // uniform vec3 eye; +// #endif + +in vec2 pos; + +out vec2 texCoord; +// #ifdef _CPos + // out vec3 viewRay; +// #endif + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + gl_Position = vec4(pos.xy, 0.0, 1.0); + +// #ifdef _CPos + // NDC (at the back of cube) + // vec4 v = vec4(pos.xy, 1.0, 1.0); + // v = vec4(invVP * v); + // v.xyz /= v.w; + // viewRay = v.xyz - eye; +// #endif +} diff --git a/Shaders/copy_mrt2_pass/copy_mrt2_pass.frag.glsl b/Shaders/copy_mrt2_pass/copy_mrt2_pass.frag.glsl new file mode 100644 index 0000000000..ca310fa9e9 --- /dev/null +++ b/Shaders/copy_mrt2_pass/copy_mrt2_pass.frag.glsl @@ -0,0 +1,12 @@ +#version 450 + +uniform sampler2D tex0; +uniform sampler2D tex1; + +in vec2 texCoord; +out vec4 fragColor[2]; + +void main() { + fragColor[0] = textureLod(tex0, texCoord, 0.0); + fragColor[1] = textureLod(tex1, texCoord, 0.0); +} diff --git a/Shaders/copy_mrt2_pass/copy_mrt2_pass.json b/Shaders/copy_mrt2_pass/copy_mrt2_pass.json new file mode 100644 index 0000000000..ec47e8bd42 --- /dev/null +++ b/Shaders/copy_mrt2_pass/copy_mrt2_pass.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "copy_mrt2_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "copy_mrt2_pass.frag.glsl" + } + ] +} diff --git a/Shaders/copy_mrt3_pass/copy_mrt3_pass.frag.glsl b/Shaders/copy_mrt3_pass/copy_mrt3_pass.frag.glsl new file mode 100644 index 0000000000..ce24084261 --- /dev/null +++ b/Shaders/copy_mrt3_pass/copy_mrt3_pass.frag.glsl @@ -0,0 +1,14 @@ +#version 450 + +uniform sampler2D tex0; +uniform sampler2D tex1; +uniform sampler2D tex2; + +in vec2 texCoord; +out vec4 fragColor[3]; + +void main() { + fragColor[0] = textureLod(tex0, texCoord, 0.0); + fragColor[1] = textureLod(tex1, texCoord, 0.0); + fragColor[2] = textureLod(tex2, texCoord, 0.0); +} diff --git a/Shaders/copy_mrt3_pass/copy_mrt3_pass.json b/Shaders/copy_mrt3_pass/copy_mrt3_pass.json new file mode 100644 index 0000000000..8c90724323 --- /dev/null +++ b/Shaders/copy_mrt3_pass/copy_mrt3_pass.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "copy_mrt3_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "copy_mrt3_pass.frag.glsl" + } + ] +} diff --git a/Shaders/copy_pass/copy_pass.json b/Shaders/copy_pass/copy_pass.json new file mode 100644 index 0000000000..a92997e6d6 --- /dev/null +++ b/Shaders/copy_pass/copy_pass.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "copy_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "../include/pass_copy.frag.glsl" + } + ] +} diff --git a/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl new file mode 100644 index 0000000000..d93a2903a0 --- /dev/null +++ b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl @@ -0,0 +1,55 @@ +#version 450 + +#include "../compiled.inc" + +// Include functions for gbuffer operations (packFloat2() etc.) +#include "../std/gbuffer.glsl" + +// World-space normal from the vertex shader stage +in vec3 wnormal; + +/* + The G-Buffer output. Deferred rendering uses the following render target layout: + + | Index | Needs #define || R | G | B | A | + +===================+=================++==============+==============+=================+====================+ + | GBUF_IDX_0 | || normal (XY) | roughness | metallic/matID | + +-------------------+-----------------++--------------+--------------+-----------------+--------------------+ + | GBUF_IDX_1 | || base color (RGB) | occlusion/specular | + +-------------------+-----------------++--------------+--------------+-----------------+--------------------+ + | GBUF_IDX_2 | _gbuffer2 || velocity (XY) | ignore radiance | unused | + +-------------------+-----------------++--------------+--------------+-----------------+--------------------+ + | GBUF_IDX_EMISSION | _EmissionShaded || emission color (RGB) | unused | + +-------------------+-----------------++--------------+--------------+-----------------+--------------------+ + + The indices as well as the GBUF_SIZE define are defined in "compiled.inc". +*/ +out vec4 fragColor[GBUF_SIZE]; + +void main() { + // Pack normals into 2 components to fit into the gbuffer + vec3 n = normalize(wnormal); + n /= (abs(n.x) + abs(n.y) + abs(n.z)); + n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy); + + // Define PBR material values + vec3 basecol = vec3(1.0); + float roughness = 0.0; + float metallic = 0.0; + float occlusion = 1.0; + float specular = 1.0; + uint materialId = 0; + vec3 emissionCol = vec3(0.0); + + // Store in gbuffer (see layout table above) + fragColor[GBUF_IDX_0] = vec4(n.xy, roughness, packFloatInt16(metallic, materialId)); + fragColor[GBUF_IDX_1] = vec4(basecol.rgb, packFloat2(occlusion, specular)); + + #ifdef _EmissionShaded + fragColor[GBUF_IDX_EMISSION] = vec4(emissionCol, 0.0); + #endif + + #ifdef _SSRefraction + fragColor[GBUF_IDX_REFRACTION] = vec4(1.0); + #endif +} diff --git a/Shaders/custom_mat_presets/custom_mat_deferred.vert.glsl b/Shaders/custom_mat_presets/custom_mat_deferred.vert.glsl new file mode 100644 index 0000000000..986d60ddee --- /dev/null +++ b/Shaders/custom_mat_presets/custom_mat_deferred.vert.glsl @@ -0,0 +1,20 @@ +#version 450 + +// World to view projection matrix to correctly position the vertex on screen +uniform mat4 WVP; +// Matrix to transform normals from local into world space +uniform mat3 N; + +// Position and normal vectors of the current vertex in local space +// Armory packs the vertex data to preserve memory, so nor.z values are +// saved in pos.w +in vec4 pos; // pos.xyz, nor.w +in vec2 nor; // nor.xy + +// Normal vector in world space +out vec3 wnormal; + +void main() { + wnormal = normalize(N * vec3(nor.xy, pos.w)); + gl_Position = WVP * vec4(pos.xyz, 1.0); +} diff --git a/Shaders/custom_mat_presets/custom_mat_forward.frag.glsl b/Shaders/custom_mat_presets/custom_mat_forward.frag.glsl new file mode 100644 index 0000000000..73dcb75189 --- /dev/null +++ b/Shaders/custom_mat_presets/custom_mat_forward.frag.glsl @@ -0,0 +1,9 @@ +#version 450 + +// Color of each fragment on the screen +out vec4 fragColor; + +void main() { + // Shadeless white color + fragColor = vec4(1.0); +} diff --git a/Shaders/custom_mat_presets/custom_mat_forward.vert.glsl b/Shaders/custom_mat_presets/custom_mat_forward.vert.glsl new file mode 100644 index 0000000000..a1d12fd73a --- /dev/null +++ b/Shaders/custom_mat_presets/custom_mat_forward.vert.glsl @@ -0,0 +1,11 @@ +#version 450 + +// World to view projection matrix to correctly position the vertex on screen +uniform mat4 WVP; + +// Position vector of the current vertex in local space +in vec3 pos; + +void main() { + gl_Position = WVP * vec4(pos, 1.0); +} diff --git a/Shaders/debug_draw/line.frag.glsl b/Shaders/debug_draw/line.frag.glsl new file mode 100644 index 0000000000..2c8b9f9873 --- /dev/null +++ b/Shaders/debug_draw/line.frag.glsl @@ -0,0 +1,8 @@ +#version 450 + +in vec3 color; +out vec4 fragColor; + +void main() { + fragColor = vec4(color, 1.0); +} diff --git a/Shaders/debug_draw/line.vert.glsl b/Shaders/debug_draw/line.vert.glsl new file mode 100644 index 0000000000..7a43a5e330 --- /dev/null +++ b/Shaders/debug_draw/line.vert.glsl @@ -0,0 +1,12 @@ +#version 450 + +in vec3 pos; +in vec3 col; + +uniform mat4 ViewProjection; +out vec3 color; + +void main() { + color = col; + gl_Position = ViewProjection * vec4(pos, 1.0); +} diff --git a/Shaders/debug_draw/line_deferred.frag.glsl b/Shaders/debug_draw/line_deferred.frag.glsl new file mode 100644 index 0000000000..a96a603fa1 --- /dev/null +++ b/Shaders/debug_draw/line_deferred.frag.glsl @@ -0,0 +1,15 @@ +#version 450 + +#include "compiled.inc" + +in vec3 color; +out vec4 fragColor[GBUF_SIZE]; + +void main() { + fragColor[GBUF_IDX_0] = vec4(1.0, 1.0, 0.0, 1.0); + fragColor[GBUF_IDX_1] = vec4(color, 1.0); + + #ifdef _EmissionShaded + fragColor[GBUF_IDX_EMISSION] = vec4(0.0); + #endif +} diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl new file mode 100644 index 0000000000..ea27617774 --- /dev/null +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -0,0 +1,519 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" +#ifdef _Clusters +#include "std/clusters.glsl" +#endif +#ifdef _Irr +#include "std/shirr.glsl" +#endif +#ifdef _VoxelGI +#include "std/conetrace.glsl" +#endif +#ifdef _VoxelAOvar +#include "std/conetrace.glsl" +#endif +#ifdef _SSS +#include "std/sss.glsl" +#endif +#ifdef _SSRS +#include "std/ssrs.glsl" +#endif + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D gbuffer1; +#ifdef _gbuffer2 + uniform sampler2D gbuffer2; +#endif +#ifdef _EmissionShaded + uniform sampler2D gbufferEmission; +#endif + +#ifdef _VoxelAOvar +uniform sampler3D voxels; +#endif +#ifdef _VoxelGITemporal +uniform sampler3D voxelsLast; +uniform float voxelBlend; +#endif +#ifdef _VoxelGICam +uniform vec3 eyeSnap; +#endif + +uniform float envmapStrength; +#ifdef _Irr +uniform vec4 shirr[7]; +#endif +#ifdef _Brdf +uniform sampler2D senvmapBrdf; +#endif +#ifdef _Rad +uniform sampler2D senvmapRadiance; +uniform int envmapNumMipmaps; +#endif +#ifdef _EnvCol +uniform vec3 backgroundCol; +#endif + +#ifdef _SSAO +uniform sampler2D ssaotex; +#endif + +#ifdef _SSS +uniform vec2 lightPlane; +#endif + +#ifdef _SSRS +//!uniform mat4 VP; +uniform mat4 invVP; +#endif + +#ifdef _LightIES +//!uniform sampler2D texIES; +#endif + +#ifdef _SMSizeUniform +//!uniform vec2 smSizeUniform; +#endif + +#ifdef _LTC +//!uniform vec3 lightArea0; +//!uniform vec3 lightArea1; +//!uniform vec3 lightArea2; +//!uniform vec3 lightArea3; +//!uniform sampler2D sltcMat; +//!uniform sampler2D sltcMag; +#ifdef _ShadowMap + #ifdef _SinglePoint + //!uniform sampler2DShadow shadowMapSpot[1]; + //!uniform mat4 LWVPSpot[1]; + #endif + #ifdef _Clusters + //!uniform sampler2DShadow shadowMapSpot[4]; + //!uniform mat4 LWVPSpotArray[4]; + #endif +#endif +#endif + +uniform vec2 cameraProj; +uniform vec3 eye; +uniform vec3 eyeLook; + +#ifdef _Clusters +uniform vec4 lightsArray[maxLights * 3]; + #ifdef _Spot + uniform vec4 lightsArraySpot[maxLights * 2]; + #endif +uniform sampler2D clustersData; +uniform vec2 cameraPlane; +#endif + +#ifdef _ShadowMap +#ifdef _SinglePoint + #ifdef _Spot + //!uniform sampler2DShadow shadowMapSpot[1]; + //!uniform mat4 LWVPSpot[1]; + #else + //!uniform samplerCubeShadow shadowMapPoint[1]; + //!uniform vec2 lightProj; + #endif +#endif +#ifdef _Clusters + #ifdef _ShadowMapAtlas + #ifdef _SingleAtlas + uniform sampler2DShadow shadowMapAtlas; + #endif + #endif + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlasPoint; + #endif + //!uniform vec4 pointLightDataArray[4]; + #else + //!uniform samplerCubeShadow shadowMapPoint[4]; + #endif + //!uniform vec2 lightProj; + #ifdef _Spot + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlasSpot; + #endif + #else + //!uniform sampler2DShadow shadowMapSpot[4]; + #endif + //!uniform mat4 LWVPSpotArray[4]; + #endif +#endif +#endif + +#ifdef _Sun +uniform vec3 sunDir; +uniform vec3 sunCol; + #ifdef _ShadowMap + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasSun; + #endif + #else + uniform sampler2DShadow shadowMap; + #endif + uniform float shadowsBias; + #ifdef _CSM + //!uniform vec4 casData[shadowmapCascades * 4 + 4]; + #else + uniform mat4 LWVP; + #endif + #endif // _ShadowMap +#endif + +#ifdef _SinglePoint // Fast path for single light +uniform vec3 pointPos; +uniform vec3 pointCol; + #ifdef _ShadowMap + uniform float pointBias; + #endif + #ifdef _Spot + uniform vec3 spotDir; + uniform vec3 spotRight; + uniform vec4 spotData; + #endif +#endif + +#ifdef _LightClouds +uniform sampler2D texClouds; +uniform float time; +#endif + +#include "std/light.glsl" + +in vec2 texCoord; +in vec3 viewRay; +out vec4 fragColor; + +void main() { + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, roughness, metallic/matid + + vec3 n; + n.z = 1.0 - abs(g0.x) - abs(g0.y); + n.xy = n.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy); + n = normalize(n); + + float roughness = g0.b; + float metallic; + uint matid; + unpackFloatInt16(g0.a, metallic, matid); + + vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ + vec2 occspec = unpackFloat2(g1.a); + vec3 albedo = surfaceAlbedo(g1.rgb, metallic); // g1.rgb - basecolor + vec3 f0 = surfaceF0(g1.rgb, metallic); + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 p = getPos(eye, eyeLook, normalize(viewRay), depth, cameraProj); + 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 +#endif + +#ifdef _Brdf + vec2 envBRDF = texelFetch(senvmapBrdf, ivec2(vec2(dotNV, 1.0 - roughness) * 256.0), 0).xy; +#endif + +// Envmap +#ifdef _Irr + vec3 envl = shIrradiance(n, shirr); + #ifdef _gbuffer2 + if (g2.b < 0.5) { + envl = envl; + } else { + envl = vec3(1.0); + } + #endif + #ifdef _EnvTex + envl /= PI; + #endif +#else + vec3 envl = vec3(1.0); +#endif + +#ifdef _Rad + vec3 reflectionWorld = reflect(-v, n); + float lod = getMipFromRoughness(roughness, envmapNumMipmaps); + vec3 prefilteredColor = textureLod(senvmapRadiance, envMapEquirect(reflectionWorld), lod).rgb; +#endif + +#ifdef _EnvLDR + envl.rgb = pow(envl.rgb, vec3(2.2)); + #ifdef _Rad + prefilteredColor = pow(prefilteredColor, vec3(2.2)); + #endif +#endif + + envl.rgb *= albedo; + +#ifdef _Brdf + envl.rgb *= 1.0 - (f0 * envBRDF.x + envBRDF.y); //LV: We should take refracted light into account +#endif + +#ifdef _Rad // Indirect specular + envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y); //LV: Removed "1.5 * occspec.y". Specular should be weighted only by FV LUT +#else + #ifdef _EnvCol + envl.rgb += backgroundCol * (f0 * envBRDF.x + envBRDF.y); //LV: Eh, what's the point of weighting it only by F0? + #endif +#endif + + envl.rgb *= envmapStrength * occspec.x; + +#ifdef _VoxelAOvar + #ifdef _VoxelGICam + vec3 voxpos = (p - eyeSnap) / voxelgiHalfExtents; + #else + vec3 voxpos = p / voxelgiHalfExtents; + #endif + + #ifndef _VoxelAONoTrace + #ifdef _VoxelGITemporal + envl.rgb *= 1.0 - (traceAO(voxpos, n, voxels) * voxelBlend + + traceAO(voxpos, n, voxelsLast) * (1.0 - voxelBlend)); + #else + envl.rgb *= 1.0 - traceAO(voxpos, n, voxels); + #endif + #endif + +#endif + +#ifdef _VoxelAOvar + fragColor.rgb += envl; +#else + fragColor.rgb = envl; +#endif + + +#ifdef _SSAO + fragColor.rgb *= textureLod(ssaotex, texCoord, 0.0).r; +#endif + +#ifdef _EmissionShadeless + if (matid == 1) { // pure emissive material, color stored in basecol + fragColor.rgb += g1.rgb; + fragColor.a = 1.0; // Mark as opaque + return; + } +#endif +#ifdef _EmissionShaded + #ifdef _EmissionShadeless + else { + #endif + vec3 emission = textureLod(gbufferEmission, texCoord, 0.0).rgb; + fragColor.rgb += emission; + #ifdef _EmissionShadeless + } + #endif +#endif + + // Show voxels + + //Show SSAO + //fragColor.rgb = texture(ssaotex, texCoord).rrr; + +#ifdef _Sun + vec3 sh = normalize(v + sunDir); + float sdotNH = max(0.0, dot(n, sh)); + float sdotVH = max(0.0, dot(v, sh)); + float sdotNL = max(0.0, dot(n, sunDir)); + float svisibility = 1.0; + vec3 sdirect = lambertDiffuseBRDF(albedo, sdotNL) + + specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y; + + #ifdef _ShadowMap + #ifdef _CSM + svisibility = shadowTestCascade( + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + , eye, p + n * shadowsBias * 10, shadowsBias + ); + #else + vec4 lPos = LWVP * vec4(p + n * shadowsBias * 100, 1.0); + if (lPos.w > 0.0) + svisibility = shadowTest( + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + , lPos.xyz / lPos.w, shadowsBias + ); + #endif + #endif + + #ifdef _VoxelShadow + svisibility *= 1.0 - traceShadow(voxels, voxpos, sunDir); + #endif + + #ifdef _SSRS + // vec2 coords = getProjectedCoord(hitCoord); + // vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); + // float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); + svisibility *= traceShadowSS(sunDir, p, gbufferD, invVP, eye); + #endif + + #ifdef _LightClouds + svisibility *= textureLod(texClouds, vec2(p.xy / 100.0 + time / 80.0), 0.0).r * dot(n, vec3(0,0,1)); + #endif + + #ifdef _MicroShadowing + // See https://advances.realtimerendering.com/other/2016/naughty_dog/NaughtyDog_TechArt_Final.pdf + svisibility *= clamp(sdotNL + 2.0 * occspec.x * occspec.x - 1.0, 0.0, 1.0); + #endif + + fragColor.rgb += sdirect * svisibility * sunCol; + +// #ifdef _Hair // Aniso +// if (matid == 2) { +// const float shinyParallel = roughness; +// const float shinyPerpendicular = 0.1; +// const vec3 v = vec3(0.99146, 0.11664, 0.05832); +// vec3 T = abs(dot(n, v)) > 0.99999 ? cross(n, vec3(0.0, 1.0, 0.0)) : cross(n, v); +// fragColor.rgb = orenNayarDiffuseBRDF(albedo, roughness, dotNV, dotNL, dotVH) + wardSpecular(n, h, dotNL, dotNV, dotNH, T, shinyParallel, shinyPerpendicular) * spec; +// } +// #endif + + #ifdef _SSS + if (matid == 2) { + #ifdef _CSM + int casi, casindex; + mat4 LWVP = getCascadeMat(distance(eye, p), casi, casindex); + #endif + fragColor.rgb += fragColor.rgb * SSSSTransmittance( + LWVP, p, n, sunDir, lightPlane.y, + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + ); + } + #endif + +#endif // _Sun + +#ifdef _SinglePoint + fragColor.rgb += sampleLight( + p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0 + #ifdef _ShadowMap + , 0, pointBias, true + #endif + #ifdef _Spot + , true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight + #endif + #ifdef _VoxelAOvar + #ifdef _VoxelShadow + , voxels, voxpos + #endif + #endif + #ifdef _MicroShadowing + , occspec.x + #endif + #ifdef _SSRS + , gbufferD, invVP, eye + #endif + ); + + #ifdef _Spot + #ifdef _SSS + if (matid == 2) fragColor.rgb += fragColor.rgb * SSSSTransmittance(LWVPSpot0, p, n, normalize(pointPos - p), lightPlane.y, shadowMapSpot[0]); + #endif + #endif +#endif + +#ifdef _Clusters + float viewz = linearize(depth * 0.5 + 0.5, cameraProj); + int clusterI = getClusterI(texCoord, viewz, cameraPlane); + int numLights = int(texelFetch(clustersData, ivec2(clusterI, 0), 0).r * 255); + + #ifdef HLSL + viewz += textureLod(clustersData, vec2(0.0), 0.0).r * 1e-9; // TODO: krafix bug, needs to generate sampler + #endif + + #ifdef _Spot + int numSpots = int(texelFetch(clustersData, ivec2(clusterI, 1 + maxLightsCluster), 0).r * 255); + int numPoints = numLights - numSpots; + #endif + + for (int i = 0; i < min(numLights, maxLightsCluster); i++) { + int li = int(texelFetch(clustersData, ivec2(clusterI, i + 1), 0).r * 255); + fragColor.rgb += sampleLight( + p, + n, + v, + dotNV, + lightsArray[li * 3].xyz, // lp + lightsArray[li * 3 + 1].xyz, // lightCol + albedo, + roughness, + occspec.y, + f0 + #ifdef _ShadowMap + // light index, shadow bias, cast_shadows + , li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0 + #endif + #ifdef _Spot + , lightsArray[li * 3 + 2].y != 0.0 + , lightsArray[li * 3 + 2].y // spot size (cutoff) + , lightsArraySpot[li * 2].w // spot blend (exponent) + , lightsArraySpot[li * 2].xyz // spotDir + , vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale + , lightsArraySpot[li * 2 + 1].xyz // right + #endif + #ifdef _VoxelAOvar + #ifdef _VoxelShadow + , voxels, voxpos + #endif + #endif + #ifdef _MicroShadowing + , occspec.x + #endif + #ifdef _SSRS + , gbufferD, invVP, eye + #endif + ); + } +#endif // _Clusters +// fragColor.a = 1.0; // Mark as opaque + + // Show voxels + /* + vec3 origin = vec3(texCoord * 2.0 - 1.0, 0.99); + vec3 direction = vec3(0.0, 0.0, -1.0); + vec4 color = vec4(0.0f); + for(uint step = 0; step < 400 && color.a < 0.99f; ++step) { + vec3 point = origin + 0.005 * step * direction; + color += (1.0f - color.a) * textureLod(voxels, point * 0.5 + 0.5, 0); + } + fragColor.rgb += color.rgb; + */ +} diff --git a/Shaders/deferred_light/deferred_light.json b/Shaders/deferred_light/deferred_light.json new file mode 100644 index 0000000000..6a5f13da44 --- /dev/null +++ b/Shaders/deferred_light/deferred_light.json @@ -0,0 +1,246 @@ +{ + "variants": ["_VoxelAOvar"], + "contexts": [ + { + "name": "deferred_light", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeSnap", + "link": "_cameraPositionSnap", + "ifdef": ["_VoxelGICam"] + }, + { + "name": "voxelBlend", + "link": "_voxelBlend", + "ifdef": ["_VoxelGITemporal"] + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "shirr", + "link": "_envmapIrradiance", + "ifdef": ["_Irr"] + }, + { + "name": "senvmapRadiance", + "link": "_envmapRadiance", + "ifdef": ["_Rad"] + }, + { + "name": "envmapNumMipmaps", + "link": "_envmapNumMipmaps", + "ifdef": ["_Rad"] + }, + { + "name": "senvmapBrdf", + "link": "$brdf.png", + "ifdef": ["_Brdf"] + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "envmapStrength", + "link": "_envmapStrength" + }, + { + "name": "backgroundCol", + "link": "_backgroundCol", + "ifdef": ["_EnvCol"] + }, + { + "name": "lightsArray", + "link": "_lightsArray", + "ifdef": ["_Clusters"] + }, + { + "name": "lightsArraySpot", + "link": "_lightsArraySpot", + "ifdef": ["_Clusters", "_Spot"] + }, + { + "name": "clustersData", + "link": "_clustersData", + "ifdef": ["_Clusters"] + }, + { + "name": "cameraPlane", + "link": "_cameraPlane", + "ifdef": ["_Clusters"] + }, + { + "name": "sunDir", + "link": "_sunDirection", + "ifdef": ["_Sun"] + }, + { + "name": "sunCol", + "link": "_sunColor", + "ifdef": ["_Sun"] + }, + { + "name": "shadowsBias", + "link": "_sunShadowsBias", + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "LWVP", + "link": "_biasLightWorldViewProjectionMatrixSun", + "ifndef": ["_CSM"], + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "casData", + "link": "_cascadeData", + "ifdef": ["_Sun", "_ShadowMap", "_CSM"] + }, + { + "name": "lightPlane", + "link": "_lightPlane", + "ifdef": ["_SSS"] + }, + { + "name": "VP", + "link": "_viewProjectionMatrix", + "ifdef": ["_SSRS"] + }, + { + "name": "texClouds", + "link": "$cloudstexture.png", + "ifdef": ["_LightClouds"] + }, + { + "name": "time", + "link": "_time", + "ifdef": ["_LightClouds"] + }, + { + "name": "texIES", + "link": "$iestexture.png", + "ifdef": ["_LightIES"] + }, + { + "name": "lightArea0", + "link": "_lightArea0", + "ifdef": ["_LTC"] + }, + { + "name": "lightArea1", + "link": "_lightArea1", + "ifdef": ["_LTC"] + }, + { + "name": "lightArea2", + "link": "_lightArea2", + "ifdef": ["_LTC"] + }, + { + "name": "lightArea3", + "link": "_lightArea3", + "ifdef": ["_LTC"] + }, + { + "name": "sltcMat", + "link": "_ltcMat", + "ifdef": ["_LTC"] + }, + { + "name": "sltcMag", + "link": "_ltcMag", + "ifdef": ["_LTC"] + }, + { + "name": "smSizeUniform", + "link": "_shadowMapSize", + "ifdef": ["_SMSizeUniform"] + }, + { + "name": "lightProj", + "link": "_lightPlaneProj", + "ifdef": ["_ShadowMap"] + }, + { + "name": "pointPos", + "link": "_pointPosition", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointCol", + "link": "_pointColor", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointBias", + "link": "_pointShadowsBias", + "ifdef": ["_SinglePoint", "_ShadowMap"] + }, + { + "name": "spotDir", + "link": "_spotDirection", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotData", + "link": "_spotData", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotRight", + "link": "_spotRight", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "LWVPSpotArray", + "link": "_biasLightWorldViewProjectionMatrixSpotArray", + "ifdef": ["_Clusters", "_ShadowMap", "_Spot"] + }, + { + "name": "pointLightDataArray", + "link": "_pointLightsAtlasArray", + "ifdef": ["_Clusters", "_ShadowMap", "_ShadowMapAtlas"] + }, + { + "name": "LWVPSpot[0]", + "link": "_biasLightWorldViewProjectionMatrixSpot0", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[1]", + "link": "_biasLightWorldViewProjectionMatrixSpot1", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[2]", + "link": "_biasLightWorldViewProjectionMatrixSpot2", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[3]", + "link": "_biasLightWorldViewProjectionMatrixSpot3", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + } + ], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "deferred_light.frag.glsl", + "color_attachments": ["RGBA64"] + } + ] +} diff --git a/Shaders/deferred_light_mobile/deferred_light.frag.glsl b/Shaders/deferred_light_mobile/deferred_light.frag.glsl new file mode 100644 index 0000000000..bb2292de0e --- /dev/null +++ b/Shaders/deferred_light_mobile/deferred_light.frag.glsl @@ -0,0 +1,283 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" +#include "std/math.glsl" +#ifdef _Clusters +#include "std/clusters.glsl" +#endif +#ifdef _Irr +#include "std/shirr.glsl" +#endif + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D gbuffer1; + +uniform float envmapStrength; +#ifdef _Irr +uniform vec4 shirr[7]; +#endif +#ifdef _Brdf +uniform sampler2D senvmapBrdf; +#endif +#ifdef _Rad +uniform sampler2D senvmapRadiance; +uniform int envmapNumMipmaps; +#endif +#ifdef _EnvCol +uniform vec3 backgroundCol; +#endif + +#ifdef _SMSizeUniform +//!uniform vec2 smSizeUniform; +#endif +uniform vec2 cameraProj; +uniform vec3 eye; +uniform vec3 eyeLook; + +#ifdef _Clusters +uniform vec4 lightsArray[maxLights * 3]; + #ifdef _Spot + uniform vec4 lightsArraySpot[maxLights * 2]; + #endif +uniform sampler2D clustersData; +uniform vec2 cameraPlane; +#endif + +#ifdef _ShadowMap +#ifdef _SinglePoint + #ifdef _Spot + //!uniform sampler2DShadow shadowMapSpot[1]; + //!uniform mat4 LWVPSpot[1]; + #else + //!uniform samplerCubeShadow shadowMapPoint[1]; + //!uniform vec2 lightProj; + #endif +#endif +#ifdef _Clusters + #ifdef _ShadowMapAtlas + #ifdef _SingleAtlas + uniform sampler2DShadow shadowMapAtlas; + #endif + #endif + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlasPoint; + #endif + //!uniform vec4 pointLightDataArray[4]; + #else + //!uniform samplerCubeShadow shadowMapPoint[4]; + #endif + //!uniform vec2 lightProj; + #ifdef _Spot + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlasSpot; + #endif + #else + //!uniform sampler2DShadow shadowMapSpot[4]; + #endif + //!uniform mat4 LWVPSpotArray[4]; + #endif +#endif +#endif + +#ifdef _Sun +uniform vec3 sunDir; +uniform vec3 sunCol; + #ifdef _ShadowMap + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasSun; + #endif + #else + uniform sampler2DShadow shadowMap; + #endif + uniform float shadowsBias; + #ifdef _CSM + //!uniform vec4 casData[shadowmapCascades * 4 + 4]; + #else + uniform mat4 LWVP; + #endif + #endif // _ShadowMap +#endif + +#ifdef _SinglePoint // Fast path for single light +uniform vec3 pointPos; +uniform vec3 pointCol; +uniform float pointBias; + #ifdef _Spot + uniform vec3 spotDir; + uniform vec3 spotRight; + uniform vec4 spotData; + #endif +#endif + +#include "std/light_mobile.glsl" + +in vec2 texCoord; +in vec3 viewRay; +out vec4 fragColor; + +void main() { + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, roughness, metallic/matid + + vec3 n; + n.z = 1.0 - abs(g0.x) - abs(g0.y); + n.xy = n.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy); + n = normalize(n); + + float roughness = g0.b; + float metallic; + uint matid; + unpackFloatInt16(g0.a, metallic, matid); + + vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ + vec2 occspec = unpackFloat2(g1.a); + vec3 albedo = surfaceAlbedo(g1.rgb, metallic); // g1.rgb - basecolor + vec3 f0 = surfaceF0(g1.rgb, metallic); + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 p = getPos(eye, eyeLook, normalize(viewRay), depth, cameraProj); + vec3 v = normalize(eye - p); + float dotNV = max(dot(n, v), 0.0); + +#ifdef _Brdf + vec2 envBRDF = texelFetch(senvmapBrdf, ivec2(vec2(dotNV, 1.0 - roughness) * 256.0), 0).xy; +#endif + + // Envmap +#ifdef _Irr + vec3 envl = shIrradiance(n, shirr); + #ifdef _EnvTex + envl /= PI; + #endif +#else + vec3 envl = vec3(1.0); +#endif + +#ifdef _Rad + vec3 reflectionWorld = reflect(-v, n); + float lod = getMipFromRoughness(roughness, envmapNumMipmaps); + vec3 prefilteredColor = textureLod(senvmapRadiance, envMapEquirect(reflectionWorld), lod).rgb; +#endif + +#ifdef _EnvLDR + envl.rgb = pow(envl.rgb, vec3(2.2)); + #ifdef _Rad + prefilteredColor = pow(prefilteredColor, vec3(2.2)); + #endif +#endif + + envl.rgb *= albedo; + +#ifdef _Rad // Indirect specular + envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y; +#else + #ifdef _EnvCol + envl.rgb += backgroundCol * surfaceF0(g1.rgb, metallic); // f0 + #endif +#endif + + envl.rgb *= envmapStrength * occspec.x; + fragColor.rgb = envl; + +#ifdef _Sun + vec3 sh = normalize(v + sunDir); + float sdotNH = max(0.0, dot(n, sh)); + float sdotVH = max(0.0, dot(v, sh)); + float sdotNL = max(0.0, dot(n, sunDir)); + float svisibility = 1.0; + vec3 sdirect = lambertDiffuseBRDF(albedo, sdotNL) + + specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y; + + #ifdef _ShadowMap + #ifdef _CSM + svisibility = shadowTestCascade( + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + , eye, p + n * shadowsBias * 10, shadowsBias + ); + #else + vec4 lPos = LWVP * vec4(p + n * shadowsBias * 100, 1.0); + if (lPos.w > 0.0) svisibility = shadowTest( + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + , lPos.xyz / lPos.w, shadowsBias + ); + #endif + #endif + + fragColor.rgb += sdirect * svisibility * sunCol; +#endif + +#ifdef _SinglePoint + fragColor.rgb += sampleLight( + p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0 + #ifdef _ShadowMap + , 0, pointBias, true + #endif + #ifdef _Spot + , true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight // TODO: Test! + #endif + ); +#endif + +#ifdef _Clusters + float viewz = linearize(depth * 0.5 + 0.5, cameraProj); + int clusterI = getClusterI(texCoord, viewz, cameraPlane); + int numLights = int(texelFetch(clustersData, ivec2(clusterI, 0), 0).r * 255); + + #ifdef HLSL + viewz += textureLod(clustersData, vec2(0.0), 0.0).r * 1e-9; // TODO: krafix bug, needs to generate sampler + #endif + + #ifdef _Spot + int numSpots = int(texelFetch(clustersData, ivec2(clusterI, 1 + maxLightsCluster), 0).r * 255); + int numPoints = numLights - numSpots; + #endif + + for (int i = 0; i < min(numLights, maxLightsCluster); i++) { + int li = int(texelFetch(clustersData, ivec2(clusterI, i + 1), 0).r * 255); + fragColor.rgb += sampleLight( + p, + n, + v, + dotNV, + lightsArray[li * 3].xyz, // lp + lightsArray[li * 3 + 1].xyz, // lightCol + albedo, + roughness, + occspec.y, + f0 + #ifdef _ShadowMap + // light index, shadow bias, cast_shadows + , li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0 + #endif + #ifdef _Spot + , lightsArray[li * 3 + 2].y != 0.0 + , lightsArray[li * 3 + 2].y // spot size (cutoff) + , lightsArraySpot[li].w // spot blend (exponent) + , lightsArraySpot[li].xyz // spotDir + , vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale + , lightsArraySpot[li * 2 + 1].xyz // right + #endif + ); + } +#endif // _Clusters +} diff --git a/Shaders/deferred_light_mobile/deferred_light_mobile.json b/Shaders/deferred_light_mobile/deferred_light_mobile.json new file mode 100644 index 0000000000..c1c9e519de --- /dev/null +++ b/Shaders/deferred_light_mobile/deferred_light_mobile.json @@ -0,0 +1,190 @@ +{ + "contexts": [ + { + "name": "deferred_light", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "shirr", + "link": "_envmapIrradiance", + "ifdef": ["_Irr"] + }, + { + "name": "senvmapRadiance", + "link": "_envmapRadiance", + "ifdef": ["_Rad"] + }, + { + "name": "envmapNumMipmaps", + "link": "_envmapNumMipmaps", + "ifdef": ["_Rad"] + }, + { + "name": "senvmapBrdf", + "link": "$brdf.png", + "ifdef": ["_Brdf"] + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "envmapStrength", + "link": "_envmapStrength" + }, + { + "name": "backgroundCol", + "link": "_backgroundCol", + "ifdef": ["_EnvCol"] + }, + { + "name": "lightsArray", + "link": "_lightsArray", + "ifdef": ["_Clusters"] + }, + { + "name": "lightsArraySpot", + "link": "_lightsArraySpot", + "ifdef": ["_Clusters", "_Spot"] + }, + { + "name": "clustersData", + "link": "_clustersData", + "ifdef": ["_Clusters"] + }, + { + "name": "cameraPlane", + "link": "_cameraPlane", + "ifdef": ["_Clusters"] + }, + { + "name": "sunDir", + "link": "_sunDirection", + "ifdef": ["_Sun"] + }, + { + "name": "sunCol", + "link": "_sunColor", + "ifdef": ["_Sun"] + }, + { + "name": "shadowsBias", + "link": "_sunShadowsBias", + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "LWVP", + "link": "_biasLightWorldViewProjectionMatrixSun", + "ifndef": ["_CSM"], + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "casData", + "link": "_cascadeData", + "ifdef": ["_Sun", "_ShadowMap", "_CSM"] + }, + { + "name": "lightPlane", + "link": "_lightPlane", + "ifdef": ["_SSS"] + }, + { + "name": "VP", + "link": "_viewProjectionMatrix", + "ifdef": ["_SSRS"] + }, + { + "name": "smSizeUniform", + "link": "_shadowMapSize", + "ifdef": ["_SMSizeUniform"] + }, + { + "name": "lightProj", + "link": "_lightPlaneProj", + "ifdef": ["_ShadowMap"] + }, + { + "name": "pointPos", + "link": "_pointPosition", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointCol", + "link": "_pointColor", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointBias", + "link": "_pointShadowsBias", + "ifdef": ["_SinglePoint"] + }, + { + "name": "spotDir", + "link": "_spotDirection", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotData", + "link": "_spotData", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotRight", + "link": "_spotRight", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "LWVPSpotArray", + "link": "_biasLightWorldViewProjectionMatrixSpotArray", + "ifdef": ["_Clusters", "_ShadowMap", "_Spot"] + }, + { + "name": "pointLightDataArray", + "link": "_pointLightsAtlasArray", + "ifdef": ["_Clusters", "_ShadowMap", "_ShadowMapAtlas"] + }, + { + "name": "LWVPSpot[0]", + "link": "_biasLightWorldViewProjectionMatrixSpot0", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[1]", + "link": "_biasLightWorldViewProjectionMatrixSpot1", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[2]", + "link": "_biasLightWorldViewProjectionMatrixSpot2", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + }, + { + "name": "LWVPSpot[3]", + "link": "_biasLightWorldViewProjectionMatrixSpot3", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_LTC", "_ShadowMap"] + } + ], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "deferred_light.frag.glsl", + "color_attachments": ["RGBA64"] + } + ] +} diff --git a/Shaders/deferred_light_solid/deferred_light.frag.glsl b/Shaders/deferred_light_solid/deferred_light.frag.glsl new file mode 100644 index 0000000000..197daa88cc --- /dev/null +++ b/Shaders/deferred_light_solid/deferred_light.frag.glsl @@ -0,0 +1,13 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D gbuffer1; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor.rgb = textureLod(gbuffer1, texCoord, 0.0).rgb; // Basecolor.rgb +} diff --git a/Shaders/deferred_light_solid/deferred_light_solid.json b/Shaders/deferred_light_solid/deferred_light_solid.json new file mode 100644 index 0000000000..04997ba005 --- /dev/null +++ b/Shaders/deferred_light_solid/deferred_light_solid.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "deferred_light", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "deferred_light.frag.glsl", + "color_attachments": ["RGBA64"] + } + ] +} diff --git a/Shaders/downsample_depth/downsample_depth.frag.glsl b/Shaders/downsample_depth/downsample_depth.frag.glsl new file mode 100644 index 0000000000..4dbd4e2f1e --- /dev/null +++ b/Shaders/downsample_depth/downsample_depth.frag.glsl @@ -0,0 +1,18 @@ +#version 450 + +#include "compiled.inc" + +uniform sampler2D texdepth; +uniform vec2 screenSizeInv; + +in vec2 texCoord; + +out float fragColor; + +void main() { + float d0 = textureLod(texdepth, texCoord, 0.0).r; + float d1 = textureLod(texdepth, texCoord + vec2(screenSizeInv.x, 0.0), 0.0).r; + float d2 = textureLod(texdepth, texCoord + vec2(0.0, screenSizeInv.y), 0.0).r; + float d3 = textureLod(texdepth, texCoord + vec2(screenSizeInv.x, screenSizeInv.y), 0.0).r; + fragColor = max(max(d0, d1), max(d2, d3)); +} diff --git a/Shaders/downsample_depth/downsample_depth.json b/Shaders/downsample_depth/downsample_depth.json new file mode 100644 index 0000000000..5301fe98ff --- /dev/null +++ b/Shaders/downsample_depth/downsample_depth.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "downsample_depth", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "downsample_depth.frag.glsl" + } + ] +} diff --git a/Shaders/fxaa_pass/fxaa_pass.frag.glsl b/Shaders/fxaa_pass/fxaa_pass.frag.glsl new file mode 100644 index 0000000000..cabafc460d --- /dev/null +++ b/Shaders/fxaa_pass/fxaa_pass.frag.glsl @@ -0,0 +1,58 @@ +#version 450 + +uniform sampler2D tex; +uniform vec2 screenSizeInv; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + const float FXAA_REDUCE_MIN = 1.0 / 128.0; + const float FXAA_REDUCE_MUL = 1.0 / 8.0; + const float FXAA_SPAN_MAX = 8.0; + + vec2 tcrgbNW = (texCoord + vec2(-1.0, -1.0) * screenSizeInv); + vec2 tcrgbNE = (texCoord + vec2(1.0, -1.0) * screenSizeInv); + vec2 tcrgbSW = (texCoord + vec2(-1.0, 1.0) * screenSizeInv); + vec2 tcrgbSE = (texCoord + vec2(1.0, 1.0) * screenSizeInv); + vec2 tcrgbM = vec2(texCoord); + + vec3 rgbNW = textureLod(tex, tcrgbNW, 0.0).rgb; + vec3 rgbNE = textureLod(tex, tcrgbNE, 0.0).rgb; + vec3 rgbSW = textureLod(tex, tcrgbSW, 0.0).rgb; + vec3 rgbSE = textureLod(tex, tcrgbSE, 0.0).rgb; + vec4 texColor = textureLod(tex, tcrgbM, 0.0); + vec3 rgbM = texColor.rgb; + vec3 luma = vec3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + vec2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * + (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) * screenSizeInv; + + vec3 rgbA = 0.5 * ( + textureLod(tex, texCoord + dir * (1.0 / 3.0 - 0.5), 0.0).rgb + + textureLod(tex, texCoord + dir * (2.0 / 3.0 - 0.5), 0.0).rgb); + fragColor.rgb = rgbA * 0.5 + 0.25 * ( // vec3 rgbB + textureLod(tex, texCoord + dir * -0.5, 0.0).rgb + + textureLod(tex, texCoord + dir * 0.5, 0.0).rgb); + + // float lumaB = dot(rgbB, luma); + float lumaB = dot(fragColor.rgb, luma); + if ((lumaB < lumaMin) || (lumaB > lumaMax)) fragColor.rgb = rgbA; + // else fragColor.rgb = rgbB; +} diff --git a/Shaders/fxaa_pass/fxaa_pass.json b/Shaders/fxaa_pass/fxaa_pass.json new file mode 100644 index 0000000000..b1014ab391 --- /dev/null +++ b/Shaders/fxaa_pass/fxaa_pass.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "fxaa_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "fxaa_pass.frag.glsl" + } + ] +} diff --git a/Shaders/histogram_pass/histogram_pass.frag.glsl b/Shaders/histogram_pass/histogram_pass.frag.glsl new file mode 100644 index 0000000000..f36a929434 --- /dev/null +++ b/Shaders/histogram_pass/histogram_pass.frag.glsl @@ -0,0 +1,18 @@ +#version 450 + +#include "compiled.inc" + +uniform sampler2D tex; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor.a = 0.01 * autoExposureSpeed; + fragColor.rgb = textureLod(tex, vec2(0.5, 0.5), 0.0).rgb + + textureLod(tex, vec2(0.2, 0.2), 0.0).rgb + + textureLod(tex, vec2(0.8, 0.2), 0.0).rgb + + textureLod(tex, vec2(0.2, 0.8), 0.0).rgb + + textureLod(tex, vec2(0.8, 0.8), 0.0).rgb; + fragColor.rgb /= 5.0; +} diff --git a/Shaders/histogram_pass/histogram_pass.json b/Shaders/histogram_pass/histogram_pass.json new file mode 100644 index 0000000000..0c17e50a17 --- /dev/null +++ b/Shaders/histogram_pass/histogram_pass.json @@ -0,0 +1,17 @@ +{ + "contexts": [ + { + "name": "histogram_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "source_alpha", + "blend_destination": "inverse_source_alpha", + "blend_operation": "add", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "histogram_pass.frag.glsl" + } + ] +} diff --git a/Shaders/include/pass.vert.glsl b/Shaders/include/pass.vert.glsl new file mode 100644 index 0000000000..1d924d7295 --- /dev/null +++ b/Shaders/include/pass.vert.glsl @@ -0,0 +1,18 @@ +#version 450 + +#include "compiled.inc" + +in vec2 pos; + +out vec2 texCoord; + +void main() { + // Scale vertex attribute to 0-1 range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + gl_Position = vec4(pos.xy, 0.0, 1.0); +} diff --git a/Shaders/include/pass_copy.frag.glsl b/Shaders/include/pass_copy.frag.glsl new file mode 100644 index 0000000000..51302dac84 --- /dev/null +++ b/Shaders/include/pass_copy.frag.glsl @@ -0,0 +1,10 @@ +#version 450 + +uniform sampler2D tex; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + fragColor = textureLod(tex, texCoord, 0.0); +} diff --git a/Shaders/include/pass_viewray.vert.glsl b/Shaders/include/pass_viewray.vert.glsl new file mode 100644 index 0000000000..0b77b14db4 --- /dev/null +++ b/Shaders/include/pass_viewray.vert.glsl @@ -0,0 +1,31 @@ +#version 450 + +#include "compiled.inc" + +uniform mat4 invVP; +uniform vec3 eye; + +in vec2 pos; + +out vec2 texCoord; +out vec3 viewRay; + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + gl_Position = vec4(pos.xy, 0.0, 1.0); + + // fullscreen triangle: http://de.slideshare.net/DevCentralAMD/vertex-shader-tricks-bill-bilodeau + // gl_Position = vec4((gl_VertexID % 2) * 4.0 - 1.0, (gl_VertexID / 2) * 4.0 - 1.0, 0.0, 1.0); + + // NDC (at the back of cube) + vec4 v = vec4(pos.x, pos.y, 1.0, 1.0); + v = vec4(invVP * v); + v.xyz /= v.w; + viewRay = v.xyz - eye; +} diff --git a/Shaders/include/pass_viewray2.vert.glsl b/Shaders/include/pass_viewray2.vert.glsl new file mode 100644 index 0000000000..8370ab389a --- /dev/null +++ b/Shaders/include/pass_viewray2.vert.glsl @@ -0,0 +1,26 @@ +#version 450 + +#include "compiled.inc" + +uniform mat4 invP; + +in vec2 pos; + +out vec2 texCoord; +out vec3 viewRay; + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + gl_Position = vec4(pos.xy, 0.0, 1.0); + + // NDC (at the back of cube) + vec4 v = vec4(pos.x, pos.y, 1.0, 1.0); + v = vec4(invP * v); + viewRay = vec3(v.xy / v.z, 1.0); +} diff --git a/Shaders/include/pass_volume.vert.glsl b/Shaders/include/pass_volume.vert.glsl new file mode 100644 index 0000000000..583ec628a0 --- /dev/null +++ b/Shaders/include/pass_volume.vert.glsl @@ -0,0 +1,12 @@ +#version 450 + +uniform mat4 VWVP; + +in vec3 pos; + +out vec4 wvpposition; + +void main() { + wvpposition = VWVP * vec4(pos, 1.0); + gl_Position = wvpposition; +} diff --git a/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl b/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl new file mode 100644 index 0000000000..f58a2e80fc --- /dev/null +++ b/Shaders/motion_blur_pass/motion_blur_pass.frag.glsl @@ -0,0 +1,54 @@ +// Based on GPU Gems 3 +// http://http.developer.nvidia.com/GPUGems3/gpugems3_ch27.html +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D gbufferD; +uniform sampler2D tex; +uniform mat4 prevVP; +uniform vec3 eye; +uniform vec3 eyeLook; +uniform vec2 cameraProj; +uniform float frameScale; + +in vec2 texCoord; +in vec3 viewRay; +out vec4 fragColor; + +vec2 getVelocity(vec2 coord, float depth) { + #ifdef _InvY + coord.y = 1.0 - coord.y; + #endif + vec4 currentPos = vec4(coord.xy * 2.0 - 1.0, depth, 1.0); + vec4 worldPos = vec4(getPos(eye, eyeLook, normalize(viewRay), depth, cameraProj), 1.0); + vec4 previousPos = prevVP * worldPos; + previousPos /= previousPos.w; + vec2 velocity = (currentPos - previousPos).xy / 40.0; + #ifdef _InvY + velocity.y = -velocity.y; + #endif + return velocity; +} + +void main() { + fragColor.rgb = textureLod(tex, texCoord, 0.0).rgb; + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (depth == 1.0) { + return; + } + + float blurScale = motionBlurIntensity * frameScale; + vec2 velocity = getVelocity(texCoord, depth) * blurScale; + + vec2 offset = texCoord; + int processed = 1; + for(int i = 0; i < 8; ++i) { + offset += velocity; + fragColor.rgb += textureLod(tex, offset, 0.0).rgb; + processed++; + } + fragColor.rgb /= processed; +} diff --git a/Shaders/motion_blur_pass/motion_blur_pass.json b/Shaders/motion_blur_pass/motion_blur_pass.json new file mode 100644 index 0000000000..dc5a34156e --- /dev/null +++ b/Shaders/motion_blur_pass/motion_blur_pass.json @@ -0,0 +1,39 @@ +{ + "contexts": [ + { + "name": "motion_blur_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "prevVP", + "link": "_prevViewProjectionMatrix" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "frameScale", + "link": "_frameScale" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "motion_blur_pass.frag.glsl" + } + ] +} diff --git a/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl new file mode 100644 index 0000000000..e521f7b1da --- /dev/null +++ b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.frag.glsl @@ -0,0 +1,33 @@ +// Per-Object Motion Blur +// http://john-chapman-graphics.blogspot.com/2013/01/per-object-motion-blur.html +#version 450 + +#include "compiled.inc" + +uniform sampler2D sveloc; +uniform sampler2D tex; +// uniform vec2 texStep; +uniform float frameScale; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + vec2 velocity = textureLod(sveloc, texCoord, 0.0).rg * motionBlurIntensity * frameScale; + + #ifdef _InvY + velocity.y = -velocity.y; + #endif + + fragColor.rgb = textureLod(tex, texCoord, 0.0).rgb; + + // float speed = length(velocity / texStep); + // const int MAX_SAMPLES = 8; + // int samples = clamp(int(speed), 1, MAX_SAMPLES); + const int samples = 8; + for (int i = 0; i < samples; ++i) { + vec2 offset = velocity * (float(i) / float(samples - 1) - 0.5); + fragColor.rgb += textureLod(tex, texCoord + offset, 0.0).rgb; + } + fragColor.rgb /= float(samples + 1); +} diff --git a/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.json b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.json new file mode 100644 index 0000000000..8e79d63b1c --- /dev/null +++ b/Shaders/motion_blur_veloc_pass/motion_blur_veloc_pass.json @@ -0,0 +1,24 @@ +{ + "contexts": [ + { + "name": "motion_blur_veloc_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "frameScale", + "link": "_frameScale" + }, + { + "name": "texStep", + "link": "_screenSizeInv", + "ifdef": ["_Disabled"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "motion_blur_veloc_pass.frag.glsl" + } + ] +} diff --git a/Shaders/probe_cubemap/probe_cubemap.frag.glsl b/Shaders/probe_cubemap/probe_cubemap.frag.glsl new file mode 100644 index 0000000000..4e1e7e1293 --- /dev/null +++ b/Shaders/probe_cubemap/probe_cubemap.frag.glsl @@ -0,0 +1,54 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform samplerCube probeTex; +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D gbuffer1; +uniform mat4 invVP; +uniform vec3 probep; +uniform vec3 eye; + +in vec4 wvpposition; +out vec4 fragColor; + +void main() { + vec2 texCoord = wvpposition.xy / wvpposition.w; + texCoord = texCoord * 0.5 + 0.5; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, roughness, metallic/matid + + float roughness = g0.b; + if (roughness > 0.95) { + fragColor.rgb = vec3(0.0); + return; + } + + float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); + if (spec == 0.0) { + fragColor.rgb = vec3(0.0); + return; + } + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 wp = getPos2(invVP, depth, texCoord); + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + vec3 v = wp - eye; + vec3 r = reflect(v, n); + #ifdef _InvY + r.y = -r.y; + #endif + float intensity = clamp((1.0 - roughness) * dot(wp - probep, n), 0.0, 1.0); + fragColor.rgb = texture(probeTex, r).rgb * intensity; +} diff --git a/Shaders/probe_cubemap/probe_cubemap.json b/Shaders/probe_cubemap/probe_cubemap.json new file mode 100644 index 0000000000..f206483c11 --- /dev/null +++ b/Shaders/probe_cubemap/probe_cubemap.json @@ -0,0 +1,36 @@ +{ + "contexts": [ + { + "name": "probe_cubemap", + "depth_write": false, + "compare_mode": "less", + "cull_mode": "clockwise", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "VWVP", + "link": "_worldViewProjectionMatrix" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "probep", + "link": "_probePosition" + }, + { + "name": "eye", + "link": "_cameraPosition" + } + ], + "vertex_shader": "../include/pass_volume.vert.glsl", + "fragment_shader": "probe_cubemap.frag.glsl" + } + ] +} diff --git a/Shaders/probe_planar/probe_planar.frag.glsl b/Shaders/probe_planar/probe_planar.frag.glsl new file mode 100644 index 0000000000..c55ea95a60 --- /dev/null +++ b/Shaders/probe_planar/probe_planar.frag.glsl @@ -0,0 +1,54 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D probeTex; +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D gbuffer1; +uniform mat4 probeVP; +uniform mat4 invVP; +uniform vec3 proben; + +in vec4 wvpposition; +out vec4 fragColor; + +void main() { + vec2 texCoord = wvpposition.xy / wvpposition.w; + texCoord = texCoord * 0.5 + 0.5; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); // Normal.xy, roughness, metallic/matid + + float roughness = g0.b; + if (roughness > 0.95) { + fragColor.rgb = vec3(0.0); + return; + } + + float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); + if (spec == 0.0) { + fragColor.rgb = vec3(0.0); + return; + } + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 wp = getPos2(invVP, depth, texCoord); + vec4 pp = probeVP * vec4(wp.xyz, 1.0); + vec2 tc = (pp.xy / pp.w) * 0.5 + 0.5; + #ifdef _InvY + tc.y = 1.0 - tc.y; + #endif + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + float intensity = clamp((1.0 - roughness) * dot(n, proben), 0.0, 1.0); + fragColor.rgb = texture(probeTex, tc).rgb * intensity; +} diff --git a/Shaders/probe_planar/probe_planar.json b/Shaders/probe_planar/probe_planar.json new file mode 100644 index 0000000000..ba39b1b4be --- /dev/null +++ b/Shaders/probe_planar/probe_planar.json @@ -0,0 +1,36 @@ +{ + "contexts": [ + { + "name": "probe_planar", + "depth_write": false, + "compare_mode": "less", + "cull_mode": "clockwise", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "VWVP", + "link": "_worldViewProjectionMatrix" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "probeVP", + "link": "_probeViewProjectionMatrix" + }, + { + "name": "proben", + "link": "_probeNormal" + } + ], + "vertex_shader": "../include/pass_volume.vert.glsl", + "fragment_shader": "probe_planar.frag.glsl" + } + ] +} diff --git a/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl b/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl new file mode 100644 index 0000000000..b1239d53f8 --- /dev/null +++ b/Shaders/smaa_blend_weight/smaa_blend_weight.frag.glsl @@ -0,0 +1,457 @@ +#version 450 + +#include "compiled.inc" +#define SMAA_MAX_SEARCH_STEPS_DIAG 8 +#define SMAA_AREATEX_MAX_DISTANCE 16 +#define SMAA_AREATEX_MAX_DISTANCE_DIAG 20 +#define SMAA_AREATEX_PIXEL_SIZE (1.0 / vec2(160.0, 560.0)) +#define SMAA_AREATEX_SUBTEX_SIZE (1.0 / 7.0) +#define SMAA_SEARCHTEX_SIZE vec2(66.0, 33.0) +#define SMAA_SEARCHTEX_PACKED_SIZE vec2(64.0, 16.0) +#define SMAA_CORNER_ROUNDING 25 +#define SMAA_CORNER_ROUNDING_NORM (float(SMAA_CORNER_ROUNDING) / 100.0) +#define SMAA_AREATEX_SELECT(sample) sample.rg +#define SMAA_SEARCHTEX_SELECT(sample) sample.r +#define mad(a, b, c) (a * b + c) +#define saturate(a) clamp(a, 0.0, 1.0) +#define round(a) floor(a + 0.5) + +uniform sampler2D edgesTex; +uniform sampler2D areaTex; +uniform sampler2D searchTex; + +uniform vec2 screenSize; +uniform vec2 screenSizeInv; + +in vec2 texCoord; +in vec2 pixcoord; +in vec4 offset0; +in vec4 offset1; +in vec4 offset2; +out vec4 fragColor; + +// Blending Weight Calculation Pixel Shader (Second Pass) +vec2 cdw_end; + +vec4 textureLodA(sampler2D tex, vec2 coord, float lod) { + #ifdef _InvY + coord.y = 1.0 - coord.y; + #endif + return textureLod(tex, coord, lod); +} + +#define SMAASampleLevelZeroOffset(tex, coord, offset) textureLodA(tex, coord + offset * screenSizeInv.xy, 0.0) + +//----------------------------------------------------------------------------- +// Diagonal Search Functions + +// #if !defined(SMAA_DISABLE_DIAG_DETECTION) +/** + * Allows to decode two binary values from a bilinear-filtered access. + */ +vec2 SMAADecodeDiagBilinearAccess(vec2 e) { + // Bilinear access for fetching 'e' have a 0.25 offset, and we are + // interested in the R and G edges: + // + // +---G---+-------+ + // | x o R x | + // +-------+-------+ + // + // Then, if one of these edge is enabled: + // Red: (0.75 * X + 0.25 * 1) => 0.25 or 1.0 + // Green: (0.75 * 1 + 0.25 * X) => 0.75 or 1.0 + // + // This function will unpack the values (mad + mul + round): + // wolframalpha.com: round(x * abs(5 * x - 5 * 0.75)) plot 0 to 1 + e.r = e.r * abs(5.0 * e.r - 5.0 * 0.75); + return round(e); +} + +vec4 SMAADecodeDiagBilinearAccess(vec4 e) { + e.rb = e.rb * abs(5.0 * e.rb - 5.0 * 0.75); + return round(e); +} + +/** + * These functions allows to perform diagonal pattern searches. + */ +vec2 SMAASearchDiag1(vec2 texcoord, vec2 dir/*, out vec2 e*/) { + vec4 coord = vec4(texcoord, -1.0, 1.0); + vec3 t = vec3(screenSizeInv.xy, 1.0); + float cw = coord.w; // TODO: krafix hlsl bug + while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && cw > 0.9) { + coord.xyz = mad(t, vec3(dir, 1.0), coord.xyz); + cdw_end /*e*/ = textureLodA(edgesTex, coord.xy, 0.0).rg; + cw = dot(cdw_end /*e*/, vec2(0.5, 0.5)); + } + coord.w = cw; + return coord.zw; +} + +vec2 SMAASearchDiag2(vec2 texcoord, vec2 dir) { + vec4 coord = vec4(texcoord, -1.0, 1.0); + coord.x += 0.25 * screenSizeInv.x; // See @SearchDiag2Optimization + vec3 t = vec3(screenSizeInv.xy, 1.0); + float cw = coord.w; // TODO: krafix hlsl bug + while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && cw > 0.9) { + coord.xyz = mad(t, vec3(dir, 1.0), coord.xyz); + // @SearchDiag2Optimization + // Fetch both edges at once using bilinear filtering: + cdw_end /*e*/ = textureLodA(edgesTex, coord.xy, 0.0).rg; + cdw_end /*e*/ = SMAADecodeDiagBilinearAccess(cdw_end /*e*/); + cw = dot(cdw_end /*e*/, vec2(0.5, 0.5)); + } + coord.w = cw; + return coord.zw; +} + +/** + * Similar to SMAAArea, this calculates the area corresponding to a certain + * diagonal distance and crossing edges 'e'. + */ +vec2 SMAAAreaDiag(vec2 dist, vec2 e, float offset) { + vec2 texcoord = mad(vec2(SMAA_AREATEX_MAX_DISTANCE_DIAG, SMAA_AREATEX_MAX_DISTANCE_DIAG), e, dist); + + // We do a scale and bias for mapping to texel space: + texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); + + // Diagonal areas are on the second half of the texture: + texcoord.x += 0.5; + + // Move to proper place, according to the subpixel offset: + texcoord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; + + // Do it! + return SMAA_AREATEX_SELECT(textureLod(areaTex, texcoord, 0.0)); +} + +/** + * This searches for diagonal patterns and returns the corresponding weights. + */ +vec2 SMAACalculateDiagWeights(vec2 texcoord, vec2 e, vec4 subsampleIndices) { + vec2 weights = vec2(0.0, 0.0); + + // Search for the line ends: + vec4 d; + if (e.r > 0.0) { + d.xz = SMAASearchDiag1(texcoord, vec2(-1.0, 1.0)/*, cdw_end*/); + float dadd = cdw_end.y > 0.9 ? 1.0 : 0.0; + d.x += dadd; + } + else { + d.xz = vec2(0.0, 0.0); + } + d.yw = SMAASearchDiag1(texcoord, vec2(1.0, -1.0)/*, cdw_end*/); + + //SMAA_BRANCH + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + vec4 coords = mad(vec4(-d.x + 0.25, d.x, d.y, -d.y - 0.25), screenSizeInv.xyxy, texcoord.xyxy); + vec4 c; + + c.xy = SMAASampleLevelZeroOffset(edgesTex, coords.xy, ivec2(-1, 0)).rg; + c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, ivec2( 1, 0)).rg; + c.yxwz = SMAADecodeDiagBilinearAccess(c.xyzw); + + // Merge crossing edges at each side into a single value: + vec2 cc = mad(vec2(2.0, 2.0), c.xz, c.yw); + + // Remove the crossing edge if we didn't found the end of the line: + // SMAAMovc(bvec2(step(0.9, d.zw)), cc, vec2(0.0, 0.0)); + float a1condx = step(0.9, d.z); + float a1condy = step(0.9, d.w); + if (a1condx == 1.0) cc.x = 0.0; + if (a1condy == 1.0) cc.y = 0.0; + + // Fetch the areas for this line: + weights += SMAAAreaDiag(d.xy, cc, subsampleIndices.z); + } + + // Search for the line ends: + d.xz = SMAASearchDiag2(texcoord, vec2(-1.0, -1.0)/*, cdw_end*/); + if (SMAASampleLevelZeroOffset(edgesTex, texcoord, ivec2(1, 0)).r > 0.0) { + d.yw = SMAASearchDiag2(texcoord, vec2(1.0, 1.0)/*, cdw_end*/); + float dadd = cdw_end.y > 0.9 ? 1.0 : 0.0; + d.y += dadd; + } + else { + d.yw = vec2(0.0, 0.0); + } + + // SMAA_BRANCH + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + vec4 coords = mad(vec4(-d.x, -d.x, d.y, d.y), screenSizeInv.xyxy, texcoord.xyxy); + vec4 c; + c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, ivec2(-1, 0)).g; + c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, ivec2( 0, -1)).r; + c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, ivec2( 1, 0)).gr; + vec2 cc = mad(vec2(2.0, 2.0), c.xz, c.yw); + + // Remove the crossing edge if we didn't found the end of the line: + // SMAAMovc(bvec2(step(0.9, d.zw)), cc, vec2(0.0, 0.0)); + float a1condx = step(0.9, d.z); + float a1condy = step(0.9, d.w); + if (a1condx == 1.0) cc.x = 0.0; + if (a1condy == 1.0) cc.y = 0.0; + + // Fetch the areas for this line: + weights += SMAAAreaDiag(d.xy, cc, subsampleIndices.w).gr; + } + + return weights; +} +// #endif + +//----------------------------------------------------------------------------- +// Horizontal/Vertical Search Functions + +/** + * This allows to determine how much length should we add in the last step + * of the searches. It takes the bilinearly interpolated edge (see + * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and + * crossing edges are active. + */ +float SMAASearchLength(vec2 e, float offset) { + // The texture is flipped vertically, with left and right cases taking half + // of the space horizontally: + vec2 scale = SMAA_SEARCHTEX_SIZE * vec2(0.5, -1.0); + vec2 bias = SMAA_SEARCHTEX_SIZE * vec2(offset, 1.0); + + // Scale and bias to access texel centers: + scale += vec2(-1.0, 1.0); + bias += vec2( 0.5, -0.5); + + // Convert from pixel coordinates to texcoords: + // (We use SMAA_SEARCHTEX_PACKED_SIZE because the texture is cropped) + scale *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + bias *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + + vec2 coord = mad(scale, e, bias); + + // Lookup the search texture: + return SMAA_SEARCHTEX_SELECT(textureLod(searchTex, coord, 0.0)); +} + +/** + * Horizontal/vertical search functions for the 2nd pass. + */ +float SMAASearchXLeft(vec2 texcoord, float end) { + /** + * @PSEUDO_GATHER4 + * This texcoord has been offset by (-0.25, -0.125) in the vertex shader to + * sample between edge, thus fetching four edges in a row. + * Sampling with different offsets in each direction allows to disambiguate + * which edges are active from the four fetched ones. + */ + vec2 e = vec2(0.0, 1.0); + while (texcoord.x > end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureLodA(edgesTex, texcoord, 0.0).rg; + texcoord = mad(-vec2(2.0, 0.0), screenSizeInv.xy, texcoord); + } + + float offset = mad(-(255.0 / 127.0), SMAASearchLength(e, 0.0), 3.25); + return mad(screenSizeInv.x, offset, texcoord.x); +} + +float SMAASearchXRight(vec2 texcoord, float end) { + vec2 e = vec2(0.0, 1.0); + while (texcoord.x < end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureLodA(edgesTex, texcoord, 0.0).rg; + texcoord = mad(vec2(2.0, 0.0), screenSizeInv.xy, texcoord); + } + + float offset = mad(-(255.0 / 127.0), SMAASearchLength(e, 0.5), 3.25); + return mad(-screenSizeInv.x, offset, texcoord.x); +} + +float SMAASearchYUp(vec2 texcoord, float end) { + vec2 e = vec2(1.0, 0.0); + while (texcoord.y > end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureLodA(edgesTex, texcoord, 0.0).rg; + texcoord = mad(-vec2(0.0, 2.0), screenSizeInv.xy, texcoord); + } + float offset = mad(-(255.0 / 127.0), SMAASearchLength(e.gr, 0.0), 3.25); + return mad(screenSizeInv.y, offset, texcoord.y); +} + +float SMAASearchYDown(vec2 texcoord, float end) { + vec2 e = vec2(1.0, 0.0); + while (texcoord.y < end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureLodA(edgesTex, texcoord, 0.0).rg; + texcoord = mad(vec2(0.0, 2.0), screenSizeInv.xy, texcoord); + } + float offset = mad(-(255.0 / 127.0), SMAASearchLength(/*searchTex,*/ e.gr, 0.5), 3.25); + return mad(-screenSizeInv.y, offset, texcoord.y); +} + +/** + * Ok, we have the distance and both crossing edges. So, what are the areas + * at each side of current edge? + */ +vec2 SMAAArea(vec2 dist, float e1, float e2, float offset) { + // Rounding prevents precision errors of bilinear filtering: + vec2 texcoord = mad(vec2(SMAA_AREATEX_MAX_DISTANCE, SMAA_AREATEX_MAX_DISTANCE), round(4.0 * vec2(e1, e2)), dist); + + // We do a scale and bias for mapping to texel space: + texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); + + // Move to proper place, according to the subpixel offset: + texcoord.y = mad(SMAA_AREATEX_SUBTEX_SIZE, offset, texcoord.y); + + // Do it! + return SMAA_AREATEX_SELECT(textureLod(areaTex, texcoord, 0.0)); +} + +//----------------------------------------------------------------------------- +// Corner Detection Functions + +vec2 SMAADetectHorizontalCornerPattern(vec2 weights, vec4 texcoord, vec2 d) { + // #if !defined(SMAA_DISABLE_CORNER_DETECTION) + vec2 leftRight = step(d.xy, d.yx); + vec2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; + + rounding /= leftRight.x + leftRight.y; // Reduce blending for pixels in the center of a line. + + vec2 factor = vec2(1.0, 1.0); + factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, ivec2(0, 1)).r; + factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, ivec2(1, 1)).r; + factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, ivec2(0, -2)).r; + factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, ivec2(1, -2)).r; + + weights *= saturate(factor); + return weights; // + // #endif +} + +vec2 SMAADetectVerticalCornerPattern(vec2 weights, vec4 texcoord, vec2 d) { + //#if !defined(SMAA_DISABLE_CORNER_DETECTION) + vec2 leftRight = step(d.xy, d.yx); + vec2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; + + rounding /= leftRight.x + leftRight.y; + + vec2 factor = vec2(1.0, 1.0); + factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, ivec2( 1, 0)).g; + factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, ivec2( 1, 1)).g; + factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, ivec2(-2, 0)).g; + factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, ivec2(-2, 1)).g; + + weights *= saturate(factor); + return weights; // + // #endif +} + + +vec4 SMAABlendingWeightCalculationPS(vec2 texcoord, vec2 pixcoord, + vec4 subsampleIndices) { // Just pass zero for SMAA 1x, see @SUBSAMPLE_INDICES. + vec4 weights = vec4(0.0, 0.0, 0.0, 0.0); + + vec2 e = textureLodA(edgesTex, texcoord, 0.0).rg; + + //SMAA_BRANCH + if (e.g > 0.0) { // Edge at north + //#if !defined(SMAA_DISABLE_DIAG_DETECTION) + // Diagonals have both north and west edges, so searching for them in + // one of the boundaries is enough. + weights.rg = SMAACalculateDiagWeights(texcoord, e, subsampleIndices); + + // We give priority to diagonals, so if we find a diagonal we skip + // horizontal/vertical processing. + //SMAA_BRANCH + if (weights.r == -weights.g) { // weights.r + weights.g == 0.0 + //#endif + + vec2 d; + + // Find the distance to the left: + vec3 coords; + coords.x = SMAASearchXLeft(offset0.xy, offset2.x); + coords.y = offset1.y; // offset[1].y = texcoord.y - 0.25 * screenSizeInv.y (@CROSSING_OFFSET) + d.x = coords.x; + + // Now fetch the left crossing edges, two at a time using bilinear + // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to + // discern what value each edge has: + float e1 = textureLodA(edgesTex, coords.xy, 0.0).r; + + // Find the distance to the right: + coords.z = SMAASearchXRight(offset0.zw, offset2.y); + d.y = coords.z; + + // We want the distances to be in pixel units (doing this here allow to + // better interleave arithmetic and memory accesses): + d = abs(round(mad(screenSize.xx, d, -pixcoord.xx))); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + vec2 sqrt_d = sqrt(d); + + // Fetch the right crossing edges: + float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.zy, ivec2(1, 0)).r; + + // Ok, we know how this pattern looks like, now it is time for getting + // the actual area: + weights.rg = SMAAArea(sqrt_d, e1, e2, subsampleIndices.y); + + // Fix corners: + coords.y = texcoord.y; + weights.rg = SMAADetectHorizontalCornerPattern(weights.rg, coords.xyzy, d); + + //#if !defined(SMAA_DISABLE_DIAG_DETECTION) + } + else { + e.r = 0.0; // Skip vertical processing. + } + //#endif + } + + //SMAA_BRANCH + if (e.r > 0.0) { // Edge at west + vec2 d; + + // Find the distance to the top: + vec3 coords; + coords.y = SMAASearchYUp(/*edgesTex, searchTex,*/ offset1.xy, offset2.z); + coords.x = offset0.x; // offset[1].x = texcoord.x - 0.25 * screenSizeInv.x; + d.x = coords.y; + + // Fetch the top crossing edges: + float e1 = textureLodA(edgesTex, coords.xy, 0.0).g; + + // Find the distance to the bottom: + coords.z = SMAASearchYDown(offset1.zw, offset2.w); + d.y = coords.z; + + // We want the distances to be in pixel units: + d = abs(round(mad(screenSize.yy, d, -pixcoord.yy))); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + vec2 sqrt_d = sqrt(d); + + // Fetch the bottom crossing edges: + float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.xz, ivec2(0, 1)).g; + + // Get the area for this direction: + weights.ba = SMAAArea(sqrt_d, e1, e2, subsampleIndices.x); + + // Fix corners: + coords.x = texcoord.x; + weights.ba = SMAADetectVerticalCornerPattern(weights.ba, coords.xyxz, d); + } + + return weights; +} + +void main() { + fragColor = SMAABlendingWeightCalculationPS(texCoord, pixcoord, vec4(0.0)); +} diff --git a/Shaders/smaa_blend_weight/smaa_blend_weight.json b/Shaders/smaa_blend_weight/smaa_blend_weight.json new file mode 100644 index 0000000000..45f0a4ebfd --- /dev/null +++ b/Shaders/smaa_blend_weight/smaa_blend_weight.json @@ -0,0 +1,31 @@ +{ + "contexts": [ + { + "name": "smaa_blend_weight", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "areaTex", + "link": "$smaa_area.png" + }, + { + "name": "searchTex", + "link": "$smaa_search.png" + }, + { + "name": "screenSize", + "link": "_screenSize" + }, + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "smaa_blend_weight.vert.glsl", + "fragment_shader": "smaa_blend_weight.frag.glsl" + } + ] +} diff --git a/Shaders/smaa_blend_weight/smaa_blend_weight.vert.glsl b/Shaders/smaa_blend_weight/smaa_blend_weight.vert.glsl new file mode 100644 index 0000000000..f87139fb2c --- /dev/null +++ b/Shaders/smaa_blend_weight/smaa_blend_weight.vert.glsl @@ -0,0 +1,36 @@ +#version 450 + +#include "compiled.inc" + +in vec2 pos; + +uniform vec2 screenSize; +uniform vec2 screenSizeInv; + +out vec2 texCoord; +out vec2 pixcoord; +out vec4 offset0; +out vec4 offset1; +out vec4 offset2; + +const int SMAA_MAX_SEARCH_STEPS = 16; + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + + // Blend Weight Calculation Vertex Shader + pixcoord = texCoord * screenSize; + + // We will use these offsets for the searches later on (see @PSEUDO_GATHER4): + offset0 = screenSizeInv.xyxy * vec4(-0.25, -0.125, 1.25, -0.125) + texCoord.xyxy; + offset1 = screenSizeInv.xyxy * vec4(-0.125, -0.25, -0.125, 1.25) + texCoord.xyxy; + + // And these for the searches, they indicate the ends of the loops: + offset2 = screenSizeInv.xxyy * + (vec4(-2.0, 2.0, -2.0, 2.0) * float(SMAA_MAX_SEARCH_STEPS)) + + vec4(offset0.xz, offset1.yw); + + gl_Position = vec4(pos.xy, 0.0, 1.0); +} diff --git a/Shaders/smaa_edge_detect/smaa_edge_detect.frag.glsl b/Shaders/smaa_edge_detect/smaa_edge_detect.frag.glsl new file mode 100644 index 0000000000..f42f080d77 --- /dev/null +++ b/Shaders/smaa_edge_detect/smaa_edge_detect.frag.glsl @@ -0,0 +1,207 @@ +/** + * Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) + * Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) + * Copyright (C) 2013 Belen Masia (bmasia@unizar.es) + * Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) + * Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to + * do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. As clarification, there + * is no requirement that the copyright notice and permission be included in + * binary distributions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * _______ ___ ___ ___ ___ + * / || \/ | / \ / \ + * | (---- | \ / | / ^ \ / ^ \ + * \ \ | |\/| | / /_\ \ / /_\ \ + * ----) | | | | | / _____ \ / _____ \ + * |_______/ |__| |__| /__/ \__\ /__/ \__\ + * + * E N H A N C E D + * S U B P I X E L M O R P H O L O G I C A L A N T I A L I A S I N G + * + * http://www.iryoku.com/smaa/ + */ +#version 450 + +#define SMAA_THRESHOLD 0.1 +#define SMAA_DEPTH_THRESHOLD (0.1 * SMAA_THRESHOLD) // For depth edge detection, depends on the depth range of the scene +#define SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR 2.0 + +uniform sampler2D colorTex; + +in vec2 texCoord; +in vec4 offset0; +in vec4 offset1; +in vec4 offset2; +out vec4 fragColor; + +// Misc functions +// Gathers current pixel, and the top-left neighbors. +// vec3 SMAAGatherNeighbours(vec2 texcoord/*, vec4 offset[3], sampler2D tex*/) { + // float P = textureLod(tex, texcoord, 0.0).r; + // float Pleft = textureLod(tex, offset0.xy, 0.0).r; + // float Ptop = textureLod(tex, offset0.zw, 0.0).r; + // return vec3(P, Pleft, Ptop); +// } + +// Edge Detection Pixel Shaders (First Pass) +// Adjusts the threshold by means of predication. +// vec2 SMAACalculatePredicatedThreshold(vec2 texcoord, vec4 offset[3], sampler2D predicationTex) { +// vec3 neighbours = SMAAGatherNeighbours(texcoord, offset, predicationTex); +// vec2 delta = abs(neighbours.xx - neighbours.yz); +// vec2 edges = step(SMAA_PREDICATION_THRESHOLD, delta); +// return SMAA_PREDICATION_SCALE * SMAA_THRESHOLD * (1.0 - SMAA_PREDICATION_STRENGTH * edges); +// } + +// Luma Edge Detection +// IMPORTANT NOTICE: luma edge detection requires gamma-corrected colors, and +// thus 'colorTex' should be a non-sRGB texture. +vec2 SMAALumaEdgeDetectionPS(vec2 texcoord + //#if SMAA_PREDICATION + //, sampler2D predicationTex + //#endif + ) { + // Calculate the threshold: + //#if SMAA_PREDICATION + //vec2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, SMAATexturePass2D(predicationTex)); + //#else + vec2 threshold = vec2(SMAA_THRESHOLD, SMAA_THRESHOLD); + //#endif + + // Calculate lumas: + vec3 weights = vec3(0.2126, 0.7152, 0.0722); + float L = dot(textureLod(colorTex, texcoord, 0.0).rgb, weights); + + float Lleft = dot(textureLod(colorTex, offset0.xy, 0.0).rgb, weights); + float Ltop = dot(textureLod(colorTex, offset0.zw, 0.0).rgb, weights); + + // We do the usual threshold: + vec4 delta; + delta.xy = abs(L - vec2(Lleft, Ltop)); + vec2 edges = step(threshold, delta.xy); + + // Then discard if there is no edge: + if (dot(edges, vec2(1.0, 1.0)) == 0.0) + discard; + + // Calculate right and bottom deltas: + float Lright = dot(textureLod(colorTex, offset1.xy, 0.0).rgb, weights); + float Lbottom = dot(textureLod(colorTex, offset1.zw, 0.0).rgb, weights); + delta.zw = abs(L - vec2(Lright, Lbottom)); + + // Calculate the maximum delta in the direct neighborhood: + vec2 maxDelta = max(delta.xy, delta.zw); + + // Calculate left-left and top-top deltas: + float Lleftleft = dot(textureLod(colorTex, offset2.xy, 0.0).rgb, weights); + float Ltoptop = dot(textureLod(colorTex, offset2.zw, 0.0).rgb, weights); + delta.zw = abs(vec2(Lleft, Ltop) - vec2(Lleftleft, Ltoptop)); + + // Calculate the final maximum delta: + maxDelta = max(maxDelta.xy, delta.zw); + float finalDelta = max(maxDelta.x, maxDelta.y); + + // Local contrast adaptation: + edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); + + return edges; +} + +// Color Edge Detection +// IMPORTANT NOTICE: color edge detection requires gamma-corrected colors, and +// thus 'colorTex' should be a non-sRGB texture. +vec2 SMAAColorEdgeDetectionPS(vec2 texcoord + //#if SMAA_PREDICATION + //, sampler2D predicationTex + //#endif + ) { + // Calculate the threshold: + //#if SMAA_PREDICATION + //vec2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, predicationTex); + //#else + vec2 threshold = vec2(SMAA_THRESHOLD, SMAA_THRESHOLD); + //#endif + + // Calculate color deltas: + vec4 delta; + vec3 C = textureLod(colorTex, texcoord, 0.0).rgb; + + vec3 Cleft = textureLod(colorTex, offset0.xy, 0.0).rgb; + vec3 t = abs(C - Cleft); + delta.x = max(max(t.r, t.g), t.b); + + vec3 Ctop = textureLod(colorTex, offset0.zw, 0.0).rgb; + t = abs(C - Ctop); + delta.y = max(max(t.r, t.g), t.b); + + // We do the usual threshold: + vec2 edges = step(threshold, delta.xy); + + // Then discard if there is no edge: + if (dot(edges, vec2(1.0, 1.0)) == 0.0) + discard; + + // Calculate right and bottom deltas: + vec3 Cright = textureLod(colorTex, offset1.xy, 0.0).rgb; + t = abs(C - Cright); + delta.z = max(max(t.r, t.g), t.b); + + vec3 Cbottom = textureLod(colorTex, offset1.zw, 0.0).rgb; + t = abs(C - Cbottom); + delta.w = max(max(t.r, t.g), t.b); + + // Calculate the maximum delta in the direct neighborhood: + vec2 maxDelta = max(delta.xy, delta.zw); + + // Calculate left-left and top-top deltas: + vec3 Cleftleft = textureLod(colorTex, offset2.xy, 0.0).rgb; + t = abs(C - Cleftleft); + delta.z = max(max(t.r, t.g), t.b); + + vec3 Ctoptop = textureLod(colorTex, offset2.zw, 0.0).rgb; + t = abs(C - Ctoptop); + delta.w = max(max(t.r, t.g), t.b); + + // Calculate the final maximum delta: + maxDelta = max(maxDelta.xy, delta.zw); + float finalDelta = max(maxDelta.x, maxDelta.y); + + // Local contrast adaptation: + edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); + + return edges; +} + +// Depth Edge Detection +// vec2 SMAADepthEdgeDetectionPS(vec2 texcoord, /*vec4 offset[3],*/ sampler2D depthTex) { + // vec3 neighbours = SMAAGatherNeighbours(texcoord, /*offset,*/ depthTex); + // vec2 delta = abs(neighbours.xx - vec2(neighbours.y, neighbours.z)); + // vec2 edges = step(SMAA_DEPTH_THRESHOLD, delta); + + // if (dot(edges, vec2(1.0, 1.0)) == 0.0) + // discard; + + // return edges; +// } + +void main() { + fragColor.rg = SMAAColorEdgeDetectionPS(texCoord); +} diff --git a/Shaders/smaa_edge_detect/smaa_edge_detect.json b/Shaders/smaa_edge_detect/smaa_edge_detect.json new file mode 100644 index 0000000000..f06b2528cc --- /dev/null +++ b/Shaders/smaa_edge_detect/smaa_edge_detect.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "smaa_edge_detect", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "smaa_edge_detect.vert.glsl", + "fragment_shader": "smaa_edge_detect.frag.glsl" + } + ] +} diff --git a/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl b/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl new file mode 100644 index 0000000000..7ce815b3f1 --- /dev/null +++ b/Shaders/smaa_edge_detect/smaa_edge_detect.vert.glsl @@ -0,0 +1,33 @@ +#version 450 + +#include "compiled.inc" + +in vec2 pos; + +uniform vec2 screenSizeInv; + +out vec2 texCoord; +out vec4 offset0; +out vec4 offset1; +out vec4 offset2; + +#ifdef _InvY +#define V_DIR(v) -(v) +#else +#define V_DIR(v) v +#endif + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + offset0 = screenSizeInv.xyxy * vec4(-1.0, 0.0, 0.0, V_DIR(-1.0)) + texCoord.xyxy; + offset1 = screenSizeInv.xyxy * vec4( 1.0, 0.0, 0.0, V_DIR(1.0)) + texCoord.xyxy; + offset2 = screenSizeInv.xyxy * vec4(-2.0, 0.0, 0.0, V_DIR(-2.0)) + texCoord.xyxy; + + gl_Position = vec4(pos.xy, 0.0, 1.0); +} diff --git a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl new file mode 100644 index 0000000000..99dca290de --- /dev/null +++ b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.frag.glsl @@ -0,0 +1,92 @@ +#version 450 + +#include "compiled.inc" + +uniform sampler2D colorTex; +uniform sampler2D blendTex; +#ifdef _Veloc +uniform sampler2D sveloc; +#endif + +uniform vec2 screenSizeInv; + +in vec2 texCoord; +in vec4 offset; +out vec4 fragColor; + +//----------------------------------------------------------------------------- +// Neighborhood Blending Pixel Shader (Third Pass) + +vec4 textureLodA(sampler2D tex, vec2 coords, float lod) { + #ifdef _InvY + coords.y = 1.0 - coords.y; + #endif + return textureLod(tex, coords, lod); +} + +vec4 SMAANeighborhoodBlendingPS(vec2 texcoord, vec4 offset) { + // Fetch the blending weights for current pixel: + vec4 a; + a.x = textureLod(blendTex, offset.xy, 0.0).a; // Right + a.y = textureLod(blendTex, offset.zw, 0.0).g; // Top + a.wz = textureLod(blendTex, texcoord, 0.0).xz; // Bottom / Left + + // Is there any blending weight with a value greater than 0.0? + //SMAA_BRANCH + if (dot(a, vec4(1.0, 1.0, 1.0, 1.0)) < 1e-5) { + vec4 color = textureLod(colorTex, texcoord, 0.0); + +#ifdef _Veloc + vec2 velocity = textureLod(sveloc, texCoord, 0.0).rg; + // Pack velocity into the alpha channel: + color.a = sqrt(5.0 * length(velocity)); +#endif + return color; + } + else { + bool h = max(a.x, a.z) > max(a.y, a.w); // max(horizontal) > max(vertical) + + // Calculate the blending offsets: + vec4 blendingOffset = vec4(0.0, a.y, 0.0, a.w); + vec2 blendingWeight = a.yw; + + if (h) { + blendingOffset.x = a.x; + blendingOffset.y = 0.0; + blendingOffset.z = a.z; + blendingOffset.w = 0.0; + blendingWeight.x = a.x; + blendingWeight.y = a.z; + } + + blendingWeight /= dot(blendingWeight, vec2(1.0, 1.0)); + + // Calculate the texture coordinates: + #ifdef _InvY + vec2 tc = vec2(texcoord.x, 1.0 - texcoord.y); + #else + vec2 tc = texcoord; + #endif + vec4 blendingCoord = blendingOffset * vec4(screenSizeInv.xy, -screenSizeInv.xy) + tc.xyxy; + + // We exploit bilinear filtering to mix current pixel with the chosen + // neighbor: + vec4 color = blendingWeight.x * textureLodA(colorTex, blendingCoord.xy, 0.0); + color += blendingWeight.y * textureLodA(colorTex, blendingCoord.zw, 0.0); + +#ifdef _Veloc + // Antialias velocity for proper reprojection in a later stage: + vec2 velocity = blendingWeight.x * textureLodA(sveloc, blendingCoord.xy, 0.0).rg; + velocity += blendingWeight.y * textureLodA(sveloc, blendingCoord.zw, 0.0).rg; + + // Pack velocity into the alpha channel: + color.a = sqrt(5.0 * length(velocity)); +#endif + return color; + } + return vec4(0.0); +} + +void main() { + fragColor = SMAANeighborhoodBlendingPS(texCoord, offset); +} diff --git a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.json b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.json new file mode 100644 index 0000000000..6c35ac140c --- /dev/null +++ b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "smaa_neighborhood_blend", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "smaa_neighborhood_blend.vert.glsl", + "fragment_shader": "smaa_neighborhood_blend.frag.glsl" + } + ] +} diff --git a/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl new file mode 100644 index 0000000000..6568ab6498 --- /dev/null +++ b/Shaders/smaa_neighborhood_blend/smaa_neighborhood_blend.vert.glsl @@ -0,0 +1,29 @@ +#version 450 + +#include "compiled.inc" + +in vec2 pos; + +uniform vec2 screenSizeInv; + +out vec2 texCoord; +out vec4 offset; + +#ifdef _InvY +#define V_DIR(v) -(v) +#else +#define V_DIR(v) v +#endif + +void main() { + // Scale vertex attribute to [0-1] range + const vec2 madd = vec2(0.5, 0.5); + texCoord = pos.xy * madd + madd; + #ifdef _InvY + texCoord.y = 1.0 - texCoord.y; + #endif + + // Neighborhood Blending Vertex Shader + offset = screenSizeInv.xyxy * vec4(1.0, 0.0, 0.0, V_DIR(1.0)) + texCoord.xyxy; + gl_Position = vec4(pos.xy, 0.0, 1.0); +} diff --git a/Shaders/ssao_pass/ssao_pass.frag.glsl b/Shaders/ssao_pass/ssao_pass.frag.glsl new file mode 100644 index 0000000000..34a63c1da6 --- /dev/null +++ b/Shaders/ssao_pass/ssao_pass.frag.glsl @@ -0,0 +1,64 @@ +// Alchemy AO / Scalable Ambient Obscurance +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform vec2 cameraProj; +uniform vec3 eye; +uniform vec3 eyeLook; +uniform vec2 screenSize; +uniform mat4 invVP; + +#ifdef _CPostprocess + uniform vec3 PPComp12; +#endif + +in vec2 texCoord; +in vec3 viewRay; +out float fragColor; + +void main() { + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (depth == 1.0) { fragColor = 1.0; return; } + + vec2 enc = textureLod(gbuffer0, texCoord, 0.0).rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + vec3 vray = normalize(viewRay); + vec3 currentPos = getPosNoEye(eyeLook, vray, depth, cameraProj); + // vec3 currentPos = getPos2NoEye(eye, invVP, depth, texCoord); + float currentDistance = length(currentPos); + #ifdef _CPostprocess + float currentDistanceA = currentDistance * PPComp12.z * (1.0 / PPComp12.y); + #else + float currentDistanceA = currentDistance * ssaoScale * (1.0 / ssaoRadius); + #endif + float currentDistanceB = currentDistance * 0.0005; + ivec2 px = ivec2(texCoord * screenSize); + float phi = (3 * px.x ^ px.y + px.x * px.y) * 10; + + fragColor = 0; + const int samples = 8; + const float samplesInv = PI2 * (1.0 / samples); + for (int i = 0; i < samples; ++i) { + float theta = samplesInv * (i + 0.5) + phi; + vec2 k = vec2(cos(theta), sin(theta)) / currentDistanceA; + depth = textureLod(gbufferD, texCoord + k, 0.0).r * 2.0 - 1.0; + // vec3 pos = getPosNoEye(eyeLook, vray, depth, cameraProj) - currentPos; + vec3 pos = getPos2NoEye(eye, invVP, depth, texCoord + k) - currentPos; + fragColor += max(0, dot(pos, n) - currentDistanceB) / (dot(pos, pos) + 0.015); + } + + #ifdef _CPostprocess + fragColor *= (PPComp12.x * 0.3) / samples; + #else + fragColor *= (ssaoStrength * 0.3) / samples; + #endif + fragColor = 1.0 - fragColor; +} diff --git a/Shaders/ssao_pass/ssao_pass.json b/Shaders/ssao_pass/ssao_pass.json new file mode 100644 index 0000000000..3793b27e7c --- /dev/null +++ b/Shaders/ssao_pass/ssao_pass.json @@ -0,0 +1,40 @@ +{ + "contexts": [ + { + "name": "ssao_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "screenSize", + "link": "_screenSize" + }, + { + "name": "PPComp12", + "link": "_PPComp12", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "ssao_pass.frag.glsl" + } + ] +} diff --git a/Shaders/ssao_pass/ssgi_pass_.frag.glsl b/Shaders/ssao_pass/ssgi_pass_.frag.glsl new file mode 100644 index 0000000000..68b7f13ebc --- /dev/null +++ b/Shaders/ssao_pass/ssgi_pass_.frag.glsl @@ -0,0 +1,61 @@ +// Alchemy AO / Scalable Ambient Obscurance +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform vec2 cameraProj; +uniform vec3 eye; +uniform vec3 eyeLook; +uniform vec2 screenSize; +uniform mat4 invVP; + +in vec2 texCoord; +in vec3 viewRay; +out float fragColor; + +void main() { + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (depth == 1.0) { fragColor = 1.0; return; } + + vec2 enc = textureLod(gbuffer0, texCoord, 0.0).rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + vec3 vray = normalize(viewRay); + vec3 currentPos = getPosNoEye(eyeLook, vray, depth, cameraProj); + // vec3 currentPos = getPos2NoEye(eye, invVP, depth, texCoord); + float currentDistance = length(currentPos); + float currentDistanceA = currentDistance * ssaoScale * (1.0 / ssaoRadius); + float currentDistanceB = currentDistance * 0.0005; + float currentDistanceC = currentDistance * 5.0; + ivec2 px = ivec2(texCoord * screenSize); + float phi = (3 * px.x ^ px.y + px.x * px.y) * 10; + + fragColor = 0; + const int samples = 8; + const float samplesInv = PI2 * (1.0 / samples); + for (int i = 0; i < samples; ++i) { + float theta = samplesInv * (i + 0.5) + phi; + vec2 k = vec2(cos(theta), sin(theta)) / currentDistanceA; + depth = textureLod(gbufferD, texCoord + k, 0.0).r * 2.0 - 1.0; + // vec3 pos = getPosNoEye(eyeLook, vray, depth, cameraProj) - currentPos; + vec3 pos = getPos2NoEye(eye, invVP, depth, texCoord + k) - currentPos; + fragColor += (max(0, dot(pos, n) - currentDistanceB) / (dot(pos, pos) + 0.015)); + } + + for (int i = 0; i < samples; ++i) { + float theta = samplesInv * (i + 0.5) + phi; + vec2 k = vec2(cos(theta), sin(theta)) / currentDistanceC; + depth = textureLod(gbufferD, texCoord + k, 0.0).r * 2.0 - 1.0; + vec3 pos = getPos2NoEye(eye, invVP, depth, texCoord + k) - currentPos; + fragColor += (max(0, dot(pos, n) - currentDistanceB) / (dot(pos, pos) + 0.015)); + } + + fragColor *= (ssaoStrength * 0.4) / samples; + fragColor = 1.0 - fragColor; +} diff --git a/Shaders/ssao_pass/ssgi_pass_.json b/Shaders/ssao_pass/ssgi_pass_.json new file mode 100644 index 0000000000..d30589cb71 --- /dev/null +++ b/Shaders/ssao_pass/ssgi_pass_.json @@ -0,0 +1,35 @@ +{ + "contexts": [ + { + "name": "ssgi_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "screenSize", + "link": "_screenSize" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "ssgi_pass.frag.glsl" + } + ] +} diff --git a/Shaders/ssgi_blur_pass/ssgi_blur_pass.frag.glsl b/Shaders/ssgi_blur_pass/ssgi_blur_pass.frag.glsl new file mode 100644 index 0000000000..0218340e7b --- /dev/null +++ b/Shaders/ssgi_blur_pass/ssgi_blur_pass.frag.glsl @@ -0,0 +1,46 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" + +uniform sampler2D tex; +uniform sampler2D gbuffer0; + +uniform vec2 dirInv; // texStep + +in vec2 texCoord; +out float fragColor; + +const float blurWeights[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); +// const float blurWeights[10] = float[] (0.132572, 0.125472, 0.106373, 0.08078, 0.05495, 0.033482, 0.018275, 0.008934, 0.003912, 0.001535); +const float discardThreshold = 0.95; + +float doBlur(const float blurWeight, const int pos, const vec3 nor, const vec2 texCoord) { + const float posadd = pos + 0.5; + + vec3 nor2 = getNor(textureLod(gbuffer0, texCoord + pos * dirInv, 0.0).rg); + float influenceFactor = step(discardThreshold, dot(nor2, nor)); + float col = textureLod(tex, texCoord + posadd * dirInv, 0.0).r; + fragColor += col * blurWeight * influenceFactor; + float weight = blurWeight * influenceFactor; + + nor2 = getNor(textureLod(gbuffer0, texCoord - pos * dirInv, 0.0).rg); + influenceFactor = step(discardThreshold, dot(nor2, nor)); + col = textureLod(tex, texCoord - posadd * dirInv, 0.0).r; + fragColor += col * blurWeight * influenceFactor; + weight += blurWeight * influenceFactor; + + return weight; +} + +void main() { + vec3 nor = getNor(textureLod(gbuffer0, texCoord, 0.0).rg); + + fragColor = textureLod(tex, texCoord, 0.0).r * blurWeights[0]; + float weight = blurWeights[0]; + for (int i = 1; i < 5; i++) { + weight += doBlur(blurWeights[i], i, nor, texCoord); + } + + fragColor = fragColor / weight; +} \ No newline at end of file diff --git a/Shaders/ssgi_blur_pass/ssgi_blur_pass.json b/Shaders/ssgi_blur_pass/ssgi_blur_pass.json new file mode 100644 index 0000000000..90292d1026 --- /dev/null +++ b/Shaders/ssgi_blur_pass/ssgi_blur_pass.json @@ -0,0 +1,74 @@ +{ + "contexts": [ + { + "name": "ssgi_blur_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2xInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "ssgi_blur_pass.frag.glsl" + }, + { + "name": "ssgi_blur_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "ssgi_blur_pass.frag.glsl" + }, + { + "name": "ssgi_blur_pass_y_blend", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "destination_color", + "blend_destination": "blend_zero", + "blend_operation": "add", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "ssgi_blur_pass.frag.glsl" + }, + + { + "name": "ssgi_blur_pass_y_blend_add", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "blend_one", + "blend_destination": "blend_one", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "dirInv", + "link": "_vec2yInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "ssgi_blur_pass.frag.glsl" + } + ] +} diff --git a/Shaders/ssgi_pass/ssgi_pass.frag.glsl b/Shaders/ssgi_pass/ssgi_pass.frag.glsl new file mode 100644 index 0000000000..694e0f2103 --- /dev/null +++ b/Shaders/ssgi_pass/ssgi_pass.frag.glsl @@ -0,0 +1,107 @@ +#version 450 + +#include "compiled.inc" +#include "std/math.glsl" +#include "std/gbuffer.glsl" + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; // Normal +// #ifdef _RTGI +// uniform sampler2D gbuffer1; // Basecol +// #endif +uniform mat4 P; +uniform mat3 V3; + +uniform vec2 cameraProj; + +const float angleMix = 0.5f; +#ifdef _SSGICone9 +const float strength = 2.0 * (1.0 / ssgiStrength); +#else +const float strength = 2.0 * (1.0 / ssgiStrength) * 1.8; +#endif + +in vec3 viewRay; +in vec2 texCoord; +out float fragColor; + +vec3 hitCoord; +vec2 coord; +float depth; +// #ifdef _RTGI +// vec3 col = vec3(0.0); +// #endif +vec3 vpos; + +vec2 getProjectedCoord(vec3 hitCoord) { + vec4 projectedCoord = P * vec4(hitCoord, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + #ifdef _InvY + projectedCoord.y = 1.0 - projectedCoord.y; + #endif + return projectedCoord.xy; +} + +float getDeltaDepth(vec3 hitCoord) { + coord = getProjectedCoord(hitCoord); + depth = textureLod(gbufferD, coord, 0.0).r * 2.0 - 1.0; + vec3 p = getPosView(viewRay, depth, cameraProj); + return p.z - hitCoord.z; +} + +void rayCast(vec3 dir) { + hitCoord = vpos; + dir *= ssgiRayStep * 2; + float dist = 0.15; + for (int i = 0; i < ssgiMaxSteps; i++) { + hitCoord += dir; + float delta = getDeltaDepth(hitCoord); + if (delta > 0.0 && delta < 0.2) { + dist = distance(vpos, hitCoord); + break; + } + } + fragColor += dist; + // #ifdef _RTGI + // col += textureLod(gbuffer1, coord, 0.0).rgb * ((ssgiRayStep * ssgiMaxSteps) - dist); + // #endif +} + +vec3 tangent(const vec3 n) { + vec3 t1 = cross(n, vec3(0, 0, 1)); + vec3 t2 = cross(n, vec3(0, 1, 0)); + if (length(t1) > length(t2)) return normalize(t1); + else return normalize(t2); +} + +void main() { + fragColor = 0; + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); + float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(V3 * n); + + vpos = getPosView(viewRay, d, cameraProj); + + rayCast(n); + vec3 o1 = normalize(tangent(n)); + vec3 o2 = (cross(o1, n)); + vec3 c1 = 0.5f * (o1 + o2); + vec3 c2 = 0.5f * (o1 - o2); + rayCast(mix(n, o1, angleMix)); + rayCast(mix(n, o2, angleMix)); + rayCast(mix(n, -c1, angleMix)); + rayCast(mix(n, -c2, angleMix)); + + #ifdef _SSGICone9 + rayCast(mix(n, -o1, angleMix)); + rayCast(mix(n, -o2, angleMix)); + rayCast(mix(n, c1, angleMix)); + rayCast(mix(n, c2, angleMix)); + #endif +} diff --git a/Shaders/ssgi_pass/ssgi_pass.json b/Shaders/ssgi_pass/ssgi_pass.json new file mode 100644 index 0000000000..a249a03246 --- /dev/null +++ b/Shaders/ssgi_pass/ssgi_pass.json @@ -0,0 +1,31 @@ +{ + "contexts": [ + { + "name": "ssgi_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "P", + "link": "_projectionMatrix" + }, + { + "name": "V3", + "link": "_viewMatrix3" + }, + { + "name": "invP", + "link": "_inverseProjectionMatrix" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray2.vert.glsl", + "fragment_shader": "ssgi_pass.frag.glsl" + } + ] +} diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl new file mode 100644 index 0000000000..f8a75a7909 --- /dev/null +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -0,0 +1,126 @@ +#version 450 + +#include "compiled.inc" +#include "std/math.glsl" +#include "std/gbuffer.glsl" + +uniform sampler2D tex; +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; // Normal, roughness +uniform sampler2D gbuffer1; // basecol, spec +uniform mat4 P; +uniform mat3 V3; +uniform vec2 cameraProj; + +#ifdef _CPostprocess +uniform vec3 PPComp9; +uniform vec3 PPComp10; +#endif + +in vec3 viewRay; +in vec2 texCoord; +out vec4 fragColor; + +vec3 hitCoord; +float depth; + +const int numBinarySearchSteps = 7; +#define maxSteps (1.0 / ssrRayStep) + +vec2 getProjectedCoord(const vec3 hit) { + vec4 projectedCoord = P * vec4(hit, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + #ifdef _InvY + projectedCoord.y = 1.0 - projectedCoord.y; + #endif + return projectedCoord.xy; +} + +float getDeltaDepth(const vec3 hit) { + float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); + return viewPos.z - hit.z; +} + +/* +vec4 binarySearch(vec3 dir) { + float d; + for (int i = 0; i < 64; i++) { + dir *= 0.05; + hitCoord -= dir; + d = getDeltaDepth(hitCoord); + if (d < 0.0) + hitCoord += dir; + } + // Ugly discard of hits too far away + #ifdef _CPostprocess + if (abs(d) > PPComp9.z) return vec4(0.0); + #else + if (abs(d) > ssrSearchDist) return vec4(0.0); + #endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); +} +*/ + +vec4 rayCast(vec3 dir) { + float d; + #ifdef _CPostprocess + dir *= PPComp9.x; + #else + dir *= ssrRayStep; + #endif + for (int i = 0; i < maxSteps; i++) { + hitCoord += dir; + float d = getDeltaDepth(hitCoord); + if(d > 0.0 && d < depth) return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); + } + return vec4(0.0); +} + +void main() { + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); + float roughness = unpackFloat(g0.b).y; + if (roughness == 1.0) { fragColor.rgb = vec3(0.0); return; } + + float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); + if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } + + depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (depth == 1.0) { fragColor.rgb = vec3(0.0); return; } + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + vec3 viewNormal = V3 * n; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 reflected = reflect(viewPos, viewNormal); + hitCoord = viewPos; + + #ifdef _CPostprocess + vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; + #else + vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; + #endif + + // * max(ssrMinRayStep, -viewPos.z) + vec4 coords = rayCast(dir); + + vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); + float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); + + float reflectivity = 1.0 - roughness; + #ifdef _CPostprocess + float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; + #else + float intensity = pow(reflectivity, ssrFalloffExp) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((ssrSearchDist - length(viewPos - hitCoord)) * (1.0 / ssrSearchDist), 0.0, 1.0) * coords.w; + #endif + + intensity = clamp(intensity, 0.0, 1.0); + vec3 reflCol = textureLod(tex, coords.xy, 0.0).rgb; + reflCol = clamp(reflCol, 0.0, 1.0); + fragColor.rgb = reflCol * intensity; +} diff --git a/Shaders/ssr_pass/ssr_pass.json b/Shaders/ssr_pass/ssr_pass.json new file mode 100644 index 0000000000..7bdc837981 --- /dev/null +++ b/Shaders/ssr_pass/ssr_pass.json @@ -0,0 +1,41 @@ +{ + "contexts": [ + { + "name": "ssr_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "P", + "link": "_projectionMatrix" + }, + { + "name": "V3", + "link": "_viewMatrix3" + }, + { + "name": "invP", + "link": "_inverseProjectionMatrix" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "PPComp9", + "link": "_PPComp9", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp10", + "link": "_PPComp10", + "ifdef": ["_CPostprocess"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray2.vert.glsl", + "fragment_shader": "ssr_pass.frag.glsl" + } + ] +} diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl new file mode 100644 index 0000000000..7420061308 --- /dev/null +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -0,0 +1,132 @@ +//refraction from modified reflection by Yvain. +#version 450 + +#include "compiled.inc" +#include "std/math.glsl" +#include "std/gbuffer.glsl" + +in vec2 texCoord; +out vec4 fragColor; + +uniform sampler2D tex; +uniform sampler2D tex1; +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D gbufferD1; +uniform sampler2D gbuffer_refraction; //ior\opacity +uniform mat4 P; +uniform mat3 V3; +uniform vec2 cameraProj; + +#ifdef _CPostprocess +uniform vec3 PPComp9; +uniform vec3 PPComp10; +#endif + +in vec3 viewRay; +vec3 hitCoord; +float depth; +vec3 viewPos; + + +const int numBinarySearchSteps = 7; +#define maxSteps (1.0 / ss_refractionRayStep) + +vec2 getProjectedCoord(const vec3 hit) { + vec4 projectedCoord = P * vec4(hit, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + #ifdef _InvY + projectedCoord.y = 1.0 - projectedCoord.y; + #endif + return projectedCoord.xy; +} + +float getDeltaDepth(const vec3 hit) { + float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(-viewRay, depth, cameraProj); + return viewPos.z - hit.z; +} + +/* +vec4 binarySearch(vec3 dir) { + float d; + for (int i = 0; i < numBinarySearchSteps; i++) { + dir *= 0.5; + hitCoord -= dir; + d = getDeltaDepth(hitCoord); + if(d < depth) + hitCoord += dir; + } + // Ugly discard of hits too far away + #ifdef _CPostprocess + if (abs(d) > PPComp9.z) return vec4(texCoord, 0.0, 1.0); + #else + if (abs(d) > ss_refractionSearchDist) return vec4(texCoord, 0.0, 1.0); + #endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); +} +*/ + +vec4 rayCast(vec3 dir) { + float d; + #ifdef _CPostprocess + dir *= PPComp9.x; + #else + dir *= ss_refractionRayStep; + #endif + for (int i = 0; i < maxSteps; i++) { + hitCoord += dir; + d = getDeltaDepth(hitCoord); + if(d > depth) return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); + } + return vec4(texCoord, 0.0, 1.0); +} + +void main() { + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); + float roughness = g0.z; + vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0); + float ior = gr.x; + float opac = gr.y; + + depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + + if(depth == 1.0 || ior == 1.0 || opac == 1.0) { + fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; + return; + } + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + + vec3 viewNormal = V3 * n; + vec3 viewPos = getPosView(-viewRay, depth, cameraProj); + vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); + hitCoord = -viewPos; + + #ifdef _CPostprocess + vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; + #else + vec3 dir = refracted * (1.0 - rand(texCoord) * ss_refractionJitter * roughness) * 2.0; + #endif + + vec4 coords = rayCast(dir); + vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); + float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); + + float refractivity = 1.0; + #ifdef _CPostprocess + float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; + #else + float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((ss_refractionSearchDist - length(viewPos - hitCoord)) * (1.0 / ss_refractionSearchDist), 0.0, 1.0) * coords.w; + #endif + + intensity = clamp(intensity, 0.0, 1.0); + vec3 refractionCol = textureLod(tex1, coords.xy, 0.0).rgb; + refractionCol = clamp(refractionCol, 0.0, 1.0); + fragColor.rgb = mix(refractionCol * intensity, textureLod(tex, texCoord.xy, 0.0).rgb, opac); +} diff --git a/Shaders/ssrefr_pass/ssrefr_pass.json b/Shaders/ssrefr_pass/ssrefr_pass.json new file mode 100755 index 0000000000..4a86991140 --- /dev/null +++ b/Shaders/ssrefr_pass/ssrefr_pass.json @@ -0,0 +1,48 @@ +{ + "contexts": [ + { + "name": "ssrefr_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "P", + "link": "_projectionMatrix" + }, + { + "name": "V3", + "link": "_viewMatrix3" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "invP", + "link": "_inverseProjectionMatrix" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "PPComp9", + "link": "_PPComp9", + "ifdef": ["_CPostprocess"] + }, + { + "name": "PPComp10", + "link": "_PPComp10", + "ifdef": ["_CPostprocess"] + } + ], + "vertex_shader": "../include/pass_viewray2.vert.glsl", + "fragment_shader": "ssrefr_pass.frag.glsl" + } + ] +} diff --git a/Shaders/sss_pass/sss_pass.frag.glsl b/Shaders/sss_pass/sss_pass.frag.glsl new file mode 100644 index 0000000000..097a4458ba --- /dev/null +++ b/Shaders/sss_pass/sss_pass.frag.glsl @@ -0,0 +1,118 @@ +// +// Copyright (C) 2012 Jorge Jimenez (jorge@iryoku.com) +// Copyright (C) 2012 Diego Gutierrez (diegog@unizar.es) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the following disclaimer +// in the documentation and/or other materials provided with the +// distribution: +// +// "Uses Separable SSS. Copyright (C) 2012 by Jorge Jimenez and Diego +// Gutierrez." +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS +// IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and documentation are +// those of the authors and should not be interpreted as representing official +// policies, either expressed or implied, of the copyright holders. +// + +#version 450 + +#include "compiled.inc" + +uniform sampler2D gbufferD; +uniform sampler2D gbuffer0; +uniform sampler2D tex; + +uniform vec2 dir; +uniform vec2 cameraProj; + +in vec2 texCoord; +out vec4 fragColor; + +const float SSSS_FOVY = 45.0; + +// Separable SSS Reflectance +// const float sssWidth = 0.005; +vec4 SSSSBlur() { + // Quality = 0 + const int SSSS_N_SAMPLES = 11; + vec4 kernel[SSSS_N_SAMPLES]; + kernel[0] = vec4(0.560479, 0.669086, 0.784728, 0); + kernel[1] = vec4(0.00471691, 0.000184771, 5.07566e-005, -2); + kernel[2] = vec4(0.0192831, 0.00282018, 0.00084214, -1.28); + kernel[3] = vec4(0.03639, 0.0130999, 0.00643685, -0.72); + kernel[4] = vec4(0.0821904, 0.0358608, 0.0209261, -0.32); + kernel[5] = vec4(0.0771802, 0.113491, 0.0793803, -0.08); + kernel[6] = vec4(0.0771802, 0.113491, 0.0793803, 0.08); + kernel[7] = vec4(0.0821904, 0.0358608, 0.0209261, 0.32); + kernel[8] = vec4(0.03639, 0.0130999, 0.00643685, 0.72); + kernel[9] = vec4(0.0192831, 0.00282018, 0.00084214, 1.28); + kernel[10] = vec4(0.00471691, 0.000184771, 5.07565e-005, 2); + + vec4 colorM = textureLod(tex, texCoord, 0.0); + + // Fetch linear depth of current pixel + float depth = textureLod(gbufferD, texCoord, 0.0).r; + float depthM = cameraProj.y / (depth - cameraProj.x); + + // Calculate the sssWidth scale (1.0 for a unit plane sitting on the projection window) + float distanceToProjectionWindow = 1.0 / tan(0.5 * radians(SSSS_FOVY)); + float scale = distanceToProjectionWindow / depthM; + + // Calculate the final step to fetch the surrounding pixels + vec2 finalStep = sssWidth * scale * dir; + // finalStep *= 1.0;//SSSS_STREGTH_SOURCE; // Modulate it using the alpha channel. + finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. + + finalStep *= 0.05; // + + // Accumulate the center sample: + vec4 colorBlurred = colorM; + colorBlurred.rgb *= kernel[0].rgb; + + // Accumulate the other samples + for (int i = 1; i < SSSS_N_SAMPLES; i++) { + // Fetch color and depth for current sample + vec2 offset = texCoord + kernel[i].a * finalStep; + vec4 color = textureLod(tex, offset, 0.0); + // #if SSSS_FOLLOW_SURFACE == 1 + // If the difference in depth is huge, we lerp color back to "colorM": + // float depth = textureLod(depthTex, offset, 0.0).r; + // float s = SSSSSaturate(300.0f * distanceToProjectionWindow * + // sssWidth * abs(depthM - depth)); + // color.rgb = SSSSLerp(color.rgb, colorM.rgb, s); + // #endif + // Accumulate + colorBlurred.rgb += kernel[i].rgb * color.rgb; + } + + return colorBlurred; +} + +void main() { + // SSS only masked objects + if (textureLod(gbuffer0, texCoord, 0.0).a == 2.0) { + fragColor = clamp(SSSSBlur(), 0.0, 1.0); + } + else { + fragColor = textureLod(tex, texCoord, 0.0); + } +} diff --git a/Shaders/sss_pass/sss_pass.json b/Shaders/sss_pass/sss_pass.json new file mode 100644 index 0000000000..a304a91b78 --- /dev/null +++ b/Shaders/sss_pass/sss_pass.json @@ -0,0 +1,42 @@ +{ + "contexts": [ + { + "name": "sss_pass_x", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dir", + "link": "_vec2x" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "sss_pass.frag.glsl" + }, + { + "name": "sss_pass_y", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "dir", + "link": "_vec2y" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "sss_pass.frag.glsl" + } + ] +} diff --git a/Shaders/std/brdf.glsl b/Shaders/std/brdf.glsl new file mode 100644 index 0000000000..d6f2def2da --- /dev/null +++ b/Shaders/std/brdf.glsl @@ -0,0 +1,139 @@ +#ifndef _BRDF_GLSL_ +#define _BRDF_GLSL_ + +// http://xlgames-inc.github.io/posts/improvedibl/ +// http://blog.selfshadow.com/publications/s2013-shading-course/ +vec3 f_schlick(const vec3 f0, const float vh) { + return f0 + (1.0 - f0) * exp2((-5.55473 * vh - 6.98316) * vh); +} + +float v_smithschlick(const float nl, const float nv, const float a) { + return 1.0 / ((nl * (1.0 - a) + a) * (nv * (1.0 - a) + a)); +} + +//Uncorrelated masking/shadowing (info below) function +//Because it is uncorrelated, G1(NdotL, a) gives us shadowing, and G1(NdotV, a) gives us masking function. +//Approximation from: https://ubm-twvideo01.s3.amazonaws.com/o1/vault/gdc2017/Presentations/Hammon_Earl_PBR_Diffuse_Lighting.pdf +float g1_approx(const float NdotX, const float alpha) +{ + return (2.0 * NdotX) * (1.0 / (NdotX * (2.0 - alpha) + alpha)); +} + +//Uncorrelated masking-shadowing function +//Approximation from: https://ubm-twvideo01.s3.amazonaws.com/o1/vault/gdc2017/Presentations/Hammon_Earl_PBR_Diffuse_Lighting.pdf +float g2_approx(const float NdotL, const float NdotV, const float alpha) +{ + vec2 helper = (2.0 * vec2(NdotL, NdotV)) * (1.0 / (vec2(NdotL, NdotV) * (2.0 - alpha) + alpha)); + return max(helper.x * helper.y, 0.0); //This can go negative, let's fix that +} + +float d_ggx(const float nh, const float a) { + float a2 = a * a; + float denom = pow(nh * nh * (a2 - 1.0) + 1.0, 2.0); + return a2 * (1.0 / 3.1415926535) / denom; +} + +vec3 specularBRDF(const vec3 f0, const float roughness, const float nl, const float nh, const float nv, const float vh) { + float a = roughness * roughness; + return d_ggx(nh, a) * g2_approx(nl, nv, a) * f_schlick(f0, vh) / max(4.0 * nv, 1e-5); //NdotL cancels out later +} + +// John Hable - Optimizing GGX Shaders +// http://filmicworlds.com/blog/optimizing-ggx-shaders-with-dotlh/ +vec3 specularBRDFb(const vec3 f0, const float roughness, const float dotNL, const float dotNH, const float dotLH) { + // D + const float pi = 3.1415926535; + float alpha = roughness * roughness; + float alphaSqr = alpha * alpha; + float denom = dotNH * dotNH * (alphaSqr - 1.0) + 1.0; + float D = alphaSqr / (pi * denom * denom); + // F + const float F_a = 1.0; + float F_b = pow(1.0 - dotLH, 5.0); + // V + float vis; + float k = alpha / 2.0; + float k2 = k * k; + float invK2 = 1.0 - k2; + vis = 1.0 / (dotLH * dotLH * invK2 + k2); + vec2 FV_helper = vec2((F_a - F_b) * vis, F_b * vis); + + vec3 FV = f0 * FV_helper.x + FV_helper.y; + vec3 specular = clamp(dotNL, 0.0, 1.0) * D * FV; + return specular / 4.0; // TODO: get rid of / 4.0 +} + +vec3 orenNayarDiffuseBRDF(const vec3 albedo, const float roughness, const float nv, const float nl, const float vh) { + float a = roughness * roughness; + float s = a; + float s2 = s * s; + float vl = 2.0 * vh * vh - 1.0; // Double angle identity + float Cosri = vl - nv * nl; + float C1 = 1.0 - 0.5 * s2 / (s2 + 0.33); + float test = 1.0; + if (Cosri >= 0.0) test = (1.0 / (max(nl, nv))); + float C2 = 0.45 * s2 / (s2 + 0.09) * Cosri * test; + return albedo * max(0.0, nl) * (C1 + C2) * (1.0 + roughness * 0.5); +} + +vec3 lambertDiffuseBRDF(const vec3 albedo, const float nl) { + return albedo * nl; +} + +vec3 surfaceAlbedo(const vec3 baseColor, const float metalness) { + return mix(baseColor, vec3(0.0), metalness); +} + +vec3 surfaceF0(const vec3 baseColor, const float metalness) { + return mix(vec3(0.04), baseColor, metalness); +} + +float getMipFromRoughness(const float roughness, const float numMipmaps) { + // First mipmap level = roughness 0, last = roughness = 1 + return roughness * numMipmaps; +} + +float wardSpecular(vec3 N, vec3 H, float dotNL, float dotNV, float dotNH, vec3 fiberDirection, float shinyParallel, float shinyPerpendicular) { + if(dotNL < 0.0 || dotNV < 0.0) { + return 0.0; + } + // fiberDirection - parse from rotation + // shinyParallel - roughness + // shinyPerpendicular - anisotropy + + vec3 fiberParallel = normalize(fiberDirection); + vec3 fiberPerpendicular = normalize(cross(N, fiberDirection)); + float dotXH = dot(fiberParallel, H); + float dotYH = dot(fiberPerpendicular, H); + const float PI = 3.1415926535; + float coeff = sqrt(dotNL/dotNV) / (4.0 * PI * shinyParallel * shinyPerpendicular); + float theta = (pow(dotXH/shinyParallel, 2.0) + pow(dotYH/shinyPerpendicular, 2.0)) / (1.0 + dotNH); + return clamp(coeff * exp(-2.0 * theta), 0.0, 1.0); +} + +// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile +// vec3 EnvBRDFApprox(vec3 SpecularColor, float Roughness, float NoV) { +// const vec4 c0 = { -1, -0.0275, -0.572, 0.022 }; +// const vec4 c1 = { 1, 0.0425, 1.04, -0.04 }; +// vec4 r = Roughness * c0 + c1; +// float a004 = min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y; +// vec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw; +// return SpecularColor * AB.x + AB.y; +// } +// float EnvBRDFApproxNonmetal(float Roughness, float NoV) { +// // Same as EnvBRDFApprox( 0.04, Roughness, NoV ) +// const vec2 c0 = { -1, -0.0275 }; +// const vec2 c1 = { 1, 0.0425 }; +// vec2 r = Roughness * c0 + c1; +// return min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y; +// } +float D_Approx(const float Roughness, const float RoL) { + float a = Roughness * Roughness; + float a2 = a * a; + float rcp_a2 = 1.0 / a2;//rcp(a2); + // 0.5 / ln(2), 0.275 / ln(2) + float c = 0.72134752 * rcp_a2 + 0.39674113; + return rcp_a2 * exp2( c * RoL - c ); +} + +#endif diff --git a/Shaders/std/clusters.glsl b/Shaders/std/clusters.glsl new file mode 100644 index 0000000000..9a8b66e671 --- /dev/null +++ b/Shaders/std/clusters.glsl @@ -0,0 +1,18 @@ + +const vec3 clusterSlices = vec3(16, 16, 16); + +int getClusterI(vec2 tc, float viewz, vec2 cameraPlane) { + int sliceZ = 0; + float cnear = clusterNear + cameraPlane.x; + if (viewz >= cnear) { + float z = log(viewz - cnear + 1.0) / log(cameraPlane.y - cnear + 1.0); + sliceZ = int(z * (clusterSlices.z - 1)) + 1; + } + // address gap between near plane and cluster near offset + else if (viewz >= cameraPlane.x) { + sliceZ = 1; + } + return int(tc.x * clusterSlices.x) + + int(int(tc.y * clusterSlices.y) * clusterSlices.x) + + int(sliceZ * clusterSlices.x * clusterSlices.y); +} diff --git a/Shaders/std/colorgrading.glsl b/Shaders/std/colorgrading.glsl new file mode 100644 index 0000000000..3fcc8720a3 --- /dev/null +++ b/Shaders/std/colorgrading.glsl @@ -0,0 +1,144 @@ +// Colorgrading library functions - Inspired by UE4 +//No specific license (maybe zlib), but just do whatever + +#define LUMINANCE_PRESERVATION 0.75 +#define EPSILON 1e-10 +#define LUMA1 0.2722287168 +#define LUMA2 0.6740817658 +#define LUMA3 0.0536895174 + +float saturate(float v) { return clamp(v, 0.0, 1.0); } +vec2 saturate(vec2 v) { return clamp(v, vec2(0.0), vec2(1.0)); } +vec3 saturate(vec3 v) { return clamp(v, vec3(0.0), vec3(1.0)); } +vec4 saturate(vec4 v) { return clamp(v, vec4(0.0), vec4(1.0)); } + +float LumaKey (vec3 color) { + return dot(color, vec3(LUMA1, LUMA2, LUMA3)); +} + +vec3 ColorTemperatureToRGB(float temperatureInKelvins) +{ + vec3 retColor; + + temperatureInKelvins = clamp(temperatureInKelvins, 1000.0, 40000.0) / 100.0; + + if (temperatureInKelvins <= 66.0) + { + retColor.r = 1.0; + retColor.g = saturate(0.39008157876901960784 * log(temperatureInKelvins) - 0.63184144378862745098); + } + else + { + float t = temperatureInKelvins - 60.0; + retColor.r = saturate(1.29293618606274509804 * pow(t, -0.1332047592)); + retColor.g = saturate(1.12989086089529411765 * pow(t, -0.0755148492)); + } + + if (temperatureInKelvins >= 66.0) + retColor.b = 1.0; + else if(temperatureInKelvins <= 19.0) + retColor.b = 0.0; + else + retColor.b = saturate(0.54320678911019607843 * log(temperatureInKelvins - 10.0) - 1.19625408914); + + return retColor; +} + +float Luminance(vec3 color) +{ + float fmin = min(min(color.r, color.g), color.b); + float fmax = max(max(color.r, color.g), color.b); + return (fmax + fmin) / 2.0; +} + +vec3 HUEtoRGB(float H) +{ + float R = abs(H * 6.0 - 3.0) - 1.0; + float G = 2.0 - abs(H * 6.0 - 2.0); + float B = 2.0 - abs(H * 6.0 - 4.0); + return saturate(vec3(R,G,B)); +} + +vec3 HSLtoRGB(in vec3 HSL) +{ + vec3 RGB = HUEtoRGB(HSL.x); + float C = (1.0 - abs(2.0 * HSL.z - 1.0)) * HSL.y; + return (RGB - 0.5) * C + vec3(HSL.z); +} + +vec3 RGBtoHCV(vec3 RGB) +{ + vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0); + vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx); + float C = Q.x - min(Q.w, Q.y); + float H = abs((Q.w - Q.y) / (6.0 * C + EPSILON) + Q.z); + return vec3(H, C, Q.x); +} + +vec3 RGBtoHSL(vec3 RGB) +{ + vec3 HCV = RGBtoHCV(RGB); + float L = HCV.z - HCV.y * 0.5; + float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + EPSILON); + return vec3(HCV.x, S, L); +} + + +vec3 ToneColorCorrection(vec3 Color, vec3 ColorSaturation, vec3 ColorContrast, vec3 ColorGamma, vec3 ColorGain, vec3 ColorOffset) { + //First initialize the colorluma key + float ColorLuma = LumaKey(Color); + //Add the saturation with the above key + Color = max(vec3(0,0,0), mix(ColorLuma.xxx, Color, ColorSaturation)); + //Contrast with slight color correction (0.18 coefficient) + float ContrastCorrectionCoefficient = 0.18; + Color = pow(Color * (1.0 / ContrastCorrectionCoefficient), ColorContrast) * ContrastCorrectionCoefficient; + //Gamma + Color = pow(Color, 1.0 / ColorGamma); + //Gain and Offset + Color = Color.rgb * ColorGain + (ColorOffset - 1); + //Return the color corrected profile + return Color; +} + +vec3 FinalizeColorCorrection(vec3 Color, mat3 ColorSaturation, mat3 ColorContrast, mat3 ColorGamma, mat3 ColorGain, mat3 ColorOffset, vec2 Toneweights) { + + float CCShadowsMax = Toneweights.x; + float CCHighlightsMin = Toneweights.y; + + //First initialize the colorluma key and set color correction weights + float ColorLuma = LumaKey(Color); + float CCWeightShadows = 1 - smoothstep(0, CCShadowsMax, ColorLuma); + float CCWeightHighlights = smoothstep(CCHighlightsMin, 1, ColorLuma); + float CCWeightMidtones = 1 - CCWeightShadows - CCWeightHighlights; + + vec3 CCColorShadows = ToneColorCorrection ( + Color, + ColorSaturation[0], + ColorContrast[0], + ColorGamma[0], + ColorGain[0], + ColorOffset[0] + ); + + vec3 CCColorMidtones = ToneColorCorrection ( + Color, + ColorSaturation[1], + ColorContrast[1], + ColorGamma[1], + ColorGain[1], + ColorOffset[1] + ); + + vec3 CCColorHighlights = ToneColorCorrection ( + Color, + ColorSaturation[2], + ColorContrast[2], + ColorGamma[2], + ColorGain[2], + ColorOffset[2] + ); + + vec3 CombinedCCProfile = CCColorShadows * CCWeightShadows + CCColorMidtones * CCWeightMidtones + CCColorHighlights * CCWeightHighlights; + + return vec3(CombinedCCProfile); +} \ No newline at end of file diff --git a/Shaders/std/conetrace.glsl b/Shaders/std/conetrace.glsl new file mode 100644 index 0000000000..b63fb80ede --- /dev/null +++ b/Shaders/std/conetrace.glsl @@ -0,0 +1,122 @@ + +#ifndef _CONETRACE_GLSL_ +#define _CONETRACE_GLSL_ + +// References +// https://github.com/Friduric/voxel-cone-tracing +// https://github.com/Cigg/Voxel-Cone-Tracing +// https://github.com/GreatBlambo/voxel_cone_tracing/ +// http://simonstechblog.blogspot.com/2013/01/implementing-voxel-cone-tracing.html +// http://leifnode.com/2015/05/voxel-cone-traced-global-illumination/ +// http://www.seas.upenn.edu/%7Epcozzi/OpenGLInsights/OpenGLInsights-SparseVoxelization.pdf +// https://research.nvidia.com/sites/default/files/publications/GIVoxels-pg2011-authors.pdf + +const float MAX_DISTANCE = 1.73205080757 * voxelgiRange; +const float VOXEL_SIZE = (2.0 / voxelgiResolution.x) * voxelgiStep; + +// uniform sampler3D voxels; +// uniform sampler3D voxelsLast; + +// vec3 orthogonal(const vec3 u) { +// // Pass normalized u +// const vec3 v = vec3(0.99146, 0.11664, 0.05832); // Pick any normalized vector +// return abs(dot(u, v)) > 0.99999 ? cross(u, vec3(0.0, 1.0, 0.0)) : cross(u, v); +// } + +vec3 tangent(const vec3 n) { + vec3 t1 = cross(n, vec3(0, 0, 1)); + vec3 t2 = cross(n, vec3(0, 1, 0)); + if (length(t1) > length(t2)) return normalize(t1); + else return normalize(t2); +} + +float traceConeAO(sampler3D voxels, const vec3 origin, vec3 dir, const float aperture, const float maxDist) { + dir = normalize(dir); + float sampleCol = 0.0; + float dist = 1.5 * VOXEL_SIZE * voxelgiOffset; + float diam = dist * aperture; + vec3 samplePos; + while (sampleCol < 1.0 && dist < maxDist) { + samplePos = dir * dist + origin; + float mip = max(log2(diam * voxelgiResolution.x), 0); + float mipSample = textureLod(voxels, samplePos * 0.5 + vec3(0.5), mip).r; + sampleCol += (1 - sampleCol) * mipSample; + dist += max(diam / 2, VOXEL_SIZE); + diam = dist * aperture; + } + return sampleCol; +} + +float traceConeAOShadow(sampler3D voxels, const vec3 origin, vec3 dir, const float aperture, const float maxDist, const float offset) { + dir = normalize(dir); + float sampleCol = 0.0; + float dist = 1.5 * VOXEL_SIZE * voxelgiOffset * 2.5; // + float diam = dist * aperture; + vec3 samplePos; + while (sampleCol < 1.0 && dist < maxDist) { + samplePos = dir * dist + origin; + float mip = max(log2(diam * voxelgiResolution.x), 0); + float mipSample = textureLod(voxels, samplePos * 0.5 + vec3(0.5), mip).r; + sampleCol += (1 - sampleCol) * mipSample; + dist += max(diam / 2, VOXEL_SIZE); + diam = dist * aperture; + } + return sampleCol; +} + +float traceShadow(sampler3D voxels, const vec3 origin, const vec3 dir) { + return traceConeAO(voxels, origin, dir, 0.14 * voxelgiAperture, 2.5 * voxelgiRange); +} + +float traceAO(const vec3 origin, const vec3 normal, sampler3D voxels) { + const float angleMix = 0.5f; + const float aperture = 0.55785173935; + vec3 o1 = normalize(tangent(normal)); + vec3 o2 = normalize(cross(o1, normal)); + vec3 c1 = 0.5f * (o1 + o2); + vec3 c2 = 0.5f * (o1 - o2); + + #ifdef HLSL + const float factor = voxelgiOcc * 0.93; + #else + const float factor = voxelgiOcc * 0.90; + #endif + + #ifdef _VoxelCones1 + return traceConeAO(voxels, origin, normal, aperture, MAX_DISTANCE) * factor; + #endif + + #ifdef _VoxelCones3 + float col = traceConeAO(voxels, origin, normal, aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, o1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -c2, angleMix), aperture, MAX_DISTANCE); + return (col / 3.0) * factor; + #endif + + #ifdef _VoxelCones5 + float col = traceConeAO(voxels, origin, normal, aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, o1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, o2, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -c1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -c2, angleMix), aperture, MAX_DISTANCE); + return (col / 5.0) * factor; + #endif + + #ifdef _VoxelCones9 + float col = traceConeAO(voxels, origin, normal, aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, o1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, o2, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -c1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -c2, angleMix), aperture, MAX_DISTANCE); + + col += traceConeAO(voxels, origin, mix(normal, -o1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, -o2, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, c1, angleMix), aperture, MAX_DISTANCE); + col += traceConeAO(voxels, origin, mix(normal, c2, angleMix), aperture, MAX_DISTANCE); + return (col / 9.0) * factor; + #endif + + return 0.0; +} + +#endif diff --git a/Shaders/std/denoise.glsl b/Shaders/std/denoise.glsl new file mode 100644 index 0000000000..8ab8095e37 --- /dev/null +++ b/Shaders/std/denoise.glsl @@ -0,0 +1,56 @@ +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Copyright (c) 2018-2019 Michele Morrone +// All rights reserved. +// +// https://michelemorrone.eu - https://BrutPitt.com +// +// me@michelemorrone.eu - brutpitt@gmail.com +// twitter: @BrutPitt - github: BrutPitt +// +// https://github.com/BrutPitt/glslSmartDeNoise/ +// +// This software is distributed under the terms of the BSD 2-Clause license +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#ifndef _DENOISE_GLSL_ +#define _DENOISE_GLSL_ + +#define INV_SQRT_OF_2PI 0.39894228040143267793994605993439 +#define INV_PI 0.31830988618379067153776752674503 + +vec4 smartDeNoise(sampler2D tex, vec2 uv, float sigma, float kSigma, float threshold) { + float radius = round(kSigma*sigma); + float radQ = radius * radius; + + float invSigmaQx2 = .5 / (sigma * sigma); // 1.0 / (sigma^2 * 2.0) + float invSigmaQx2PI = INV_PI * invSigmaQx2; // 1.0 / (sqrt(PI) * sigma) + + float invThresholdSqx2 = .5 / (threshold * threshold); // 1.0 / (sigma^2 * 2.0) + float invThresholdSqrt2PI = INV_SQRT_OF_2PI / threshold; // 1.0 / (sqrt(2*PI) * sigma) + + vec4 centrPx = texture(tex,uv); + + float zBuff = 0.0; + vec4 aBuff = vec4(0.0); + vec2 size = vec2(textureSize(tex, 0)); + + for(float x=-radius; x <= radius; x++) { + float pt = sqrt(radQ-x*x); // pt = yRadius: have circular trend + for(float y=-pt; y <= pt; y++) { + vec2 d = vec2(x,y)/size; + + float blurFactor = exp( -dot(d , d) * invSigmaQx2 ) * invSigmaQx2; + + vec4 walkPx = texture(tex,uv+d); + + vec4 dC = walkPx-centrPx; + float deltaFactor = exp( -dot(dC, dC) * invThresholdSqx2) * invThresholdSqrt2PI * blurFactor; + + zBuff += deltaFactor; + aBuff += deltaFactor*walkPx; + } + } + return aBuff/zBuff; +} + +#endif diff --git a/Shaders/std/dof.glsl b/Shaders/std/dof.glsl new file mode 100644 index 0000000000..68cec1a1fa --- /dev/null +++ b/Shaders/std/dof.glsl @@ -0,0 +1,92 @@ +// DoF with bokeh GLSL shader by Martins Upitis (martinsh) (devlog-martinsh.blogspot.com) +// Creative Commons Attribution 3.0 Unported License + +#include "compiled.inc" +#include "std/math.glsl" + +// const float compoDOFDistance = 10.0; // Focal distance value in meters +// const float compoDOFLength = 160.0; // Focal length in mm 18-200 +// const float compoDOFFstop = 128.0; // F-stop value + +const int samples = 6; // Samples on the first ring +const int rings = 6; // Ring count +const vec2 focus = vec2(0.5, 0.5); +const float coc = 0.11; // Circle of confusion size in mm (35mm film = 0.03mm) +const float maxblur = 1.0; +const float threshold = 0.5; // Highlight threshold +const float gain = 2.0; // Highlight gain +const float bias = 0.5; // Bokeh edge bias +const float fringe = 0.7; // Bokeh chromatic aberration/fringing +const float namount = 0.0001; // Dither amount + +vec3 color(vec2 coords, const float blur, const sampler2D tex, const vec2 texStep) { + vec3 col = vec3(0.0); + col.r = textureLod(tex, coords + vec2(0.0, 1.0) * texStep * fringe * blur, 0.0).r; + col.g = textureLod(tex, coords + vec2(-0.866, -0.5) * texStep * fringe * blur, 0.0).g; + col.b = textureLod(tex, coords + vec2(0.866, -0.5) * texStep * fringe * blur, 0.0).b; + + const vec3 lumcoeff = vec3(0.299, 0.587, 0.114); + float lum = dot(col.rgb, lumcoeff); + float thresh = max((lum - threshold) * gain, 0.0); + return col + mix(vec3(0.0), col, thresh * blur); +} + +vec3 dof( + const vec2 texCoord, + const float gdepth, + const sampler2D tex, + const sampler2D gbufferD, + const vec2 texStep, + const vec2 cameraProj, + const bool autoFocus, + const float DOFDistance, + const float DOFLength, + const float DOFFStop) { + + float depth = linearize(gdepth, cameraProj); + float fDepth = 0.0; + + if(autoFocus) { + fDepth = linearize(textureLod(gbufferD, focus, 0.0).r * 2.0 - 1.0, cameraProj); + } else { + fDepth = DOFDistance; + } + + float f = DOFLength; // Focal length in mm + float d = fDepth * 1000.0; // Focal plane in mm + float o = depth * 1000.0; // Depth in mm + float a = (o * f) / (o - f); + float b = (d * f) / (d - f); + float c = (d - f) / (d * DOFFStop * coc); + float blur = abs(a - b) * c; + blur = clamp(blur, 0.0, 1.0); + + vec2 noise = rand2(texCoord) * namount * blur; + float w = (texStep.x) * blur * maxblur + noise.x; + float h = (texStep.y) * blur * maxblur + noise.y; + vec3 col = vec3(0.0); + if (blur < 0.05) { + col = textureLod(tex, texCoord, 0.0).rgb; + } + else { + col = textureLod(tex, texCoord, 0.0).rgb; + float s = 1.0; + int ringsamples; + + for (int i = 1; i <= rings; ++i) { + ringsamples = i * samples; + for (int j = 0 ; j < ringsamples; ++j) { + float step = PI2 / float(ringsamples); + float pw = (cos(float(j) * step) * float(i)); + float ph = (sin(float(j) * step) * float(i)); + float p = 1.0; + // if (pentagon) p = penta(vec2(pw, ph)); + col += color(texCoord + vec2(pw * w, ph * h), blur, tex, texStep) * mix(1.0, (float(i)) / (float(rings)), bias) * p; + s += 1.0 * mix(1.0, (float(i)) / (float(rings)), bias) * p; + } + } + col /= s; + } + return col; +} + diff --git a/Shaders/std/filters.glsl b/Shaders/std/filters.glsl new file mode 100644 index 0000000000..d7bc512820 --- /dev/null +++ b/Shaders/std/filters.glsl @@ -0,0 +1,60 @@ + +vec4 cubicCatmullrom(float x) { + const float s = 0.5; + float x2 = x * x; + float x3 = x2 * x; + vec4 w; + w.x = -s*x3 + 2*s*x2 - s*x + 0; + w.y = (2-s)*x3 + (s-3)*x2 + 1; + w.z = (s-2)*x3 + (3-2*s)*x2 + s*x + 0; + w.w = s*x3 - s*x2 + 0; + return w; +} + +vec4 cubic(float v) { + vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v; + vec4 s = n * n * n; + float x = s.x; + float y = s.y - 4.0 * s.x; + float z = s.z - 4.0 * s.y + 6.0 * s.x; + float w = 6.0 - x - y - z; + return vec4(x, y, z, w) * (1.0/6.0); +} + +vec3 textureBicubic(sampler2D tex, vec2 tc, vec2 texStep) { + // http://www.java-gaming.org/index.php?topic=35123.0 + vec2 texSize = 1.0 / texStep; + tc = tc * texSize - 0.5; + + vec2 fxy = fract(tc); + tc -= fxy; + vec4 xcubic = cubic(fxy.x); + vec4 ycubic = cubic(fxy.y); + + vec4 c = tc.xxyy + vec2(-0.5, 1.5).xyxy; + vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw); + vec4 offset = c + vec4 (xcubic.yw, ycubic.yw) / s; + offset *= texStep.xxyy; + vec3 sample0 = texture(tex, offset.xz).rgb; + vec3 sample1 = texture(tex, offset.yz).rgb; + vec3 sample2 = texture(tex, offset.xw).rgb; + vec3 sample3 = texture(tex, offset.yw).rgb; + + float sx = s.x / (s.x + s.y); + float sy = s.z / (s.z + s.w); + + return mix(mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy); +} + +vec4 textureSS(sampler2D tex, vec2 tc, vec2 texStep) { + vec4 col = texture(tex, tc); + col += texture(tex, tc + vec2(1.5, 0.0) * texStep); + col += texture(tex, tc + vec2(-1.5, 0.0) * texStep); + col += texture(tex, tc + vec2(0.0, 1.5) * texStep); + col += texture(tex, tc + vec2(0.0, -1.5) * texStep); + col += texture(tex, tc + vec2(1.5, 1.5) * texStep); + col += texture(tex, tc + vec2(-1.5, -1.5) * texStep); + col += texture(tex, tc + vec2(1.5, -1.5) * texStep); + col += texture(tex, tc + vec2(-1.5, 1.5) * texStep); + return col / 9.0; +} diff --git a/Shaders/std/gbuffer.glsl b/Shaders/std/gbuffer.glsl new file mode 100644 index 0000000000..07652a50bf --- /dev/null +++ b/Shaders/std/gbuffer.glsl @@ -0,0 +1,153 @@ +#ifndef _GBUFFER_GLSL_ +#define _GBUFFER_GLSL_ + +vec2 octahedronWrap(const vec2 v) { + return (1.0 - abs(v.yx)) * (vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0)); +} + +vec3 getNor(const vec2 enc) { + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + n = normalize(n); + return n; +} + +vec3 getPosView(const vec3 viewRay, const float depth, const vec2 cameraProj) { + float linearDepth = cameraProj.y / (cameraProj.x - depth); + //float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); + return viewRay * linearDepth; +} + +vec3 getPos(const vec3 eye, const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { + // eyeLook, viewRay should be normalized + float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); + float viewZDist = dot(eyeLook, viewRay); + vec3 wposition = eye + viewRay * (linearDepth / viewZDist); + return wposition; +} + +vec3 getPosNoEye(const vec3 eyeLook, const vec3 viewRay, const float depth, const vec2 cameraProj) { + // eyeLook, viewRay should be normalized + float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); + float viewZDist = dot(eyeLook, viewRay); + vec3 wposition = viewRay * (linearDepth / viewZDist); + return wposition; +} + +#if defined(HLSL) || defined(METAL) +vec3 getPos2(const mat4 invVP, const float depth, vec2 coord) { + coord.y = 1.0 - coord.y; +#else +vec3 getPos2(const mat4 invVP, const float depth, const vec2 coord) { +#endif + vec4 pos = vec4(coord * 2.0 - 1.0, depth, 1.0); + pos = invVP * pos; + pos.xyz /= pos.w; + return pos.xyz; +} + +#if defined(HLSL) || defined(METAL) +vec3 getPosView2(const mat4 invP, const float depth, vec2 coord) { + coord.y = 1.0 - coord.y; +#else +vec3 getPosView2(const mat4 invP, const float depth, const vec2 coord) { +#endif + vec4 pos = vec4(coord * 2.0 - 1.0, depth, 1.0); + pos = invP * pos; + pos.xyz /= pos.w; + return pos.xyz; +} + +#if defined(HLSL) || defined(METAL) +vec3 getPos2NoEye(const vec3 eye, const mat4 invVP, const float depth, vec2 coord) { + coord.y = 1.0 - coord.y; +#else +vec3 getPos2NoEye(const vec3 eye, const mat4 invVP, const float depth, const vec2 coord) { +#endif + vec4 pos = vec4(coord * 2.0 - 1.0, depth, 1.0); + pos = invVP * pos; + pos.xyz /= pos.w; + return pos.xyz - eye; +} + +float packFloat(const float f1, const float f2) { + return floor(f1 * 100.0) + min(f2, 1.0 - 1.0 / 100.0); +} + +vec2 unpackFloat(const float f) { + return vec2(floor(f) / 100.0, fract(f)); +} + +float packFloat2(const float f1, const float f2) { + // Higher f1 = less precise f2 + return floor(f1 * 255.0) + min(f2, 1.0 - 1.0 / 100.0); +} + +vec2 unpackFloat2(const float f) { + return vec2(floor(f) / 255.0, fract(f)); +} + +vec4 encodeRGBM(const vec3 rgb) { + const float maxRange = 6.0; + float maxRGB = max(rgb.x, max(rgb.g, rgb.b)); + float m = maxRGB / maxRange; + m = ceil(m * 255.0) / 255.0; + return vec4(rgb / (m * maxRange), m); +} + +vec3 decodeRGBM(const vec4 rgbm) { + const float maxRange = 6.0; + return rgbm.rgb * rgbm.a * maxRange; +} + +uint encNor(vec3 n) { + ivec3 nor = ivec3(n * 255.0f); + uvec3 norSigns; + norSigns.x = (nor.x >> 5) & 0x04000000; + norSigns.y = (nor.y >> 14) & 0x00020000; + norSigns.z = (nor.z >> 23) & 0x00000100; + nor = abs(nor); + uint val = norSigns.x | (nor.x << 18) | norSigns.y | (nor.y << 9) | norSigns.z | nor.z; + return val; +} + +vec3 decNor(uint val) { + uvec3 nor; + nor.x = (val >> 18) & 0x000000ff; + nor.y = (val >> 9) & 0x000000ff; + nor.z = val & 0x000000ff; + uvec3 norSigns; + norSigns.x = (val >> 25) & 0x00000002; + norSigns.y = (val >> 16) & 0x00000002; + norSigns.z = (val >> 7) & 0x00000002; + norSigns = 1 - norSigns; + vec3 normal = vec3(nor) / 255.0f; + normal *= norSigns; + return normal; +} + +/** + Packs a float in [0, 1] and an integer in [0..15] into a single 16 bit float value. +**/ +float packFloatInt16(const float f, const uint i) { + const uint numBitFloat = 12; + const float maxValFloat = float((1 << numBitFloat) - 1); + + const uint bitsInt = i << numBitFloat; + const uint bitsFloat = uint(f * maxValFloat); + + return float(bitsInt | bitsFloat); +} + +void unpackFloatInt16(const float val, out float f, out uint i) { + const uint numBitFloat = 12; + const float maxValFloat = float((1 << numBitFloat) - 1); + + const uint bitsValue = uint(val); + + i = bitsValue >> numBitFloat; + f = (bitsValue & ~(0xF << numBitFloat)) / maxValFloat; +} + +#endif diff --git a/Shaders/std/ies.glsl b/Shaders/std/ies.glsl new file mode 100644 index 0000000000..a7e9f1246e --- /dev/null +++ b/Shaders/std/ies.glsl @@ -0,0 +1,32 @@ + +uniform sampler2D texIES; + +float iesAttenuation(vec3 l) { + + const float PI = 3.1415926535; + // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf + // Sample direction into light space + // vec3 iesSampleDirection = mul(light.worldToLight , -L); + // Cartesian to spherical + // Texture encoded with cos( phi ), scale from -1 - >1 to 0 - >1 + // float phiCoord = (iesSampleDirection.z * 0.5f) + 0.5f; + // float theta = atan2 (iesSampleDirection.y , iesSampleDirection .x); + // float thetaCoord = theta * (1.0 / (PI * 2.0)); + // float iesProfileScale = texture(texIES, vec2(thetaCoord, phiCoord)).r; + // return iesProfileScale; + + // 1D texture + // vec3 pl = normalize(p - lightPos); + // float f = asin(dot(pl, l)) / PI + 0.5; + // return texture(texIES, vec2(f, 0.0)).r; + + // 1D texture + // float cosTheta = dot(lightToPos, lightDir); + // float angle = acos(cosTheta) * (1.0 / PI); + // return texture(texIES, vec2(angle, 0.0), 0.0).r; + + // Based on https://github.com/tobspr/RenderPipeline + float hor = acos(l.z) / PI; + float vert = atan(l.x, l.y) * (1.0 / (PI * 2.0)) + 0.5; + return texture(texIES, vec2(hor, vert)).r; +} diff --git a/Shaders/std/light.glsl b/Shaders/std/light.glsl new file mode 100644 index 0000000000..6e32b202ac --- /dev/null +++ b/Shaders/std/light.glsl @@ -0,0 +1,243 @@ +#ifndef _LIGHT_GLSL_ +#define _LIGHT_GLSL_ + +#include "compiled.inc" +#include "std/brdf.glsl" +#include "std/math.glsl" +#ifdef _ShadowMap +#include "std/shadows.glsl" +#endif +#ifdef _VoxelAOvar +#include "std/conetrace.glsl" +#endif +#ifdef _LTC +#include "std/ltc.glsl" +#endif +#ifdef _LightIES +#include "std/ies.glsl" +#endif +#ifdef _SSRS +#include "std/ssrs.glsl" +#endif +#ifdef _Spot +#include "std/light_common.glsl" +#endif + +#ifdef _ShadowMap + #ifdef _SinglePoint + #ifdef _Spot + #ifndef _LTC + uniform sampler2DShadow shadowMapSpot[1]; + uniform mat4 LWVPSpot[1]; + #endif + #else + uniform samplerCubeShadow shadowMapPoint[1]; + uniform vec2 lightProj; + #endif + #endif + #ifdef _Clusters + #ifdef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlas; + #endif + uniform vec2 lightProj; + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasPoint; + #endif + #else + uniform samplerCubeShadow shadowMapPoint[4]; + #endif + #ifdef _Spot + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasSpot; + #endif + #else + uniform sampler2DShadow shadowMapSpot[maxLightsCluster]; + #endif + uniform mat4 LWVPSpotArray[maxLightsCluster]; + #endif + #endif +#endif + +#ifdef _LTC +uniform vec3 lightArea0; +uniform vec3 lightArea1; +uniform vec3 lightArea2; +uniform vec3 lightArea3; +uniform sampler2D sltcMat; +uniform sampler2D sltcMag; +#ifdef _ShadowMap +#ifndef _Spot + #ifdef _SinglePoint + uniform sampler2DShadow shadowMapSpot[1]; + uniform mat4 LWVPSpot[1]; + #endif + #ifdef _Clusters + uniform sampler2DShadow shadowMapSpot[maxLightsCluster]; + uniform mat4 LWVPSpotArray[maxLightsCluster]; + #endif + #endif +#endif +#endif + +vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, const vec3 lp, const vec3 lightCol, + const vec3 albedo, const float rough, const float spec, const vec3 f0 + #ifdef _ShadowMap + , int index, float bias, bool receiveShadow + #endif + #ifdef _Spot + , bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right + #endif + #ifdef _VoxelAOvar + #ifdef _VoxelShadow + , sampler3D voxels, vec3 voxpos + #endif + #endif + #ifdef _MicroShadowing + , float occ + #endif + #ifdef _SSRS + , sampler2D gbufferD, mat4 invVP, vec3 eye + #endif + ) { + vec3 ld = lp - p; + vec3 l = normalize(ld); + vec3 h = normalize(v + l); + float dotNH = max(0.0, dot(n, h)); + float dotVH = max(0.0, dot(v, h)); + float dotNL = max(0.0, dot(n, l)); + + #ifdef _LTC + float theta = acos(dotNV); + vec2 tuv = vec2(rough, theta / (0.5 * PI)); + tuv = tuv * LUT_SCALE + LUT_BIAS; + vec4 t = textureLod(sltcMat, tuv, 0.0); + mat3 invM = mat3( + vec3(1.0, 0.0, t.y), + vec3(0.0, t.z, 0.0), + vec3(t.w, 0.0, t.x)); + float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3); + ltcspec *= textureLod(sltcMag, tuv, 0.0).a; + float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3); + vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05; + #else + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + + specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec; + #endif + direct *= attenuate(distance(p, lp)); + direct *= lightCol; + + #ifdef _MicroShadowing + direct *= clamp(dotNL + 2.0 * occ * occ - 1.0, 0.0, 1.0); + #endif + + #ifdef _SSRS + direct *= traceShadowSS(l, p, gbufferD, invVP, eye); + #endif + + #ifdef _VoxelAOvar + #ifdef _VoxelShadow + direct *= 1.0 - traceShadow(voxels, voxpos, l); + #endif + #endif + + #ifdef _LTC + #ifdef _ShadowMap + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + if (index == 0) { + vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + } + else if (index == 1) { + vec4 lPos = LWVPSpot[1] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + } + else if (index == 2) { + vec4 lPos = LWVPSpot[2] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + } + else if (index == 3) { + vec4 lPos = LWVPSpot[3] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + } + #endif + } + #endif + return direct; + #endif + + #ifdef _Spot + if (isSpot) { + direct *= spotlightMask(l, spotDir, right, scale, spotSize, spotBlend); + + #ifdef _ShadowMap + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0); + #ifdef _ShadowMapAtlas + direct *= shadowTest( + #ifndef _SingleAtlas + shadowMapAtlasSpot + #else + shadowMapAtlas + #endif + , lPos.xyz / lPos.w, bias + ); + #else + if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + else if (index == 1) direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + else if (index == 2) direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + else if (index == 3) direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + #endif + #endif + } + #endif + return direct; + } + #endif + + #ifdef _LightIES + direct *= iesAttenuation(-l); + #endif + + #ifdef _ShadowMap + if (receiveShadow) { + #ifdef _SinglePoint + #ifndef _Spot + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #endif + #ifdef _Clusters + #ifdef _ShadowMapAtlas + direct *= PCFFakeCube( + #ifndef _SingleAtlas + shadowMapAtlasPoint + #else + shadowMapAtlas + #endif + , ld, -l, bias, lightProj, n, index + ); + #else + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + #endif + } + #endif + + return direct; +} + +#endif diff --git a/Shaders/std/light_common.glsl b/Shaders/std/light_common.glsl new file mode 100644 index 0000000000..2327232cdf --- /dev/null +++ b/Shaders/std/light_common.glsl @@ -0,0 +1,31 @@ +#ifndef _LIGHT_COMMON_GLSL_ +#define _LIGHT_COMMON_GLSL_ + +#ifdef _Spot +float spotlightMask(const vec3 dir, const vec3 spotDir, const vec3 right, const vec2 scale, const float spotSize, const float spotBlend) { + // Project the fragment's light dir to the z axis in the light's local space + float localZ = dot(spotDir, dir); + + if (localZ < 0) { + return 0.0; // Discard opposite cone + } + + vec3 up = cross(spotDir, right); + + // Scale the incoming light direction to treat the spotlight's ellipse as if + // it was 1 unit away from the light source, this way the smoothstep below + // works independently of the distance + vec3 scaledDir = dir.xyz / localZ; + + // Project to right and up vectors to apply axis scale + float localX = dot(scaledDir, right) / scale.x; + float localY = dot(scaledDir, up) / scale.y; + + // Inverse of length of vector from ellipse to light (scaledDir.z == 1.0) + float ellipse = inversesqrt(localX * localX + localY * localY + 1.0); + + return smoothstep(0.0, 1.0, (ellipse - spotSize) / spotBlend); +} +#endif // _Spot + +#endif // _LIGHT_COMMON_GLSL_ diff --git a/Shaders/std/light_mobile.glsl b/Shaders/std/light_mobile.glsl new file mode 100644 index 0000000000..a0ce42e76e --- /dev/null +++ b/Shaders/std/light_mobile.glsl @@ -0,0 +1,135 @@ +#ifndef _LIGHT_MOBILE_GLSL_ +#define _LIGHT_MOBILE_GLSL_ + +#include "compiled.inc" +#include "std/brdf.glsl" +#ifdef _ShadowMap +#include "std/shadows.glsl" +#endif +#ifdef _Spot +#include "std/light_common.glsl" +#endif + +#ifdef _ShadowMap + #ifdef _SinglePoint + #ifdef _Spot + uniform sampler2DShadow shadowMapSpot[1]; + uniform mat4 LWVPSpot[1]; + #else + uniform samplerCubeShadow shadowMapPoint[1]; + uniform vec2 lightProj; + #endif + #endif + #ifdef _Clusters + #ifdef _SingleAtlas + //!uniform sampler2DShadow shadowMapAtlas; + #endif + uniform vec2 lightProj; + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasPoint; + #endif + #else + uniform samplerCubeShadow shadowMapPoint[4]; + #endif + #ifdef _Spot + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasSpot; + #endif + #else + uniform sampler2DShadow shadowMapSpot[maxLightsCluster]; + #endif + uniform mat4 LWVPSpotArray[maxLightsCluster]; + #endif + #endif +#endif + +vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, const vec3 lp, const vec3 lightCol, + const vec3 albedo, const float rough, const float spec, const vec3 f0 + #ifdef _ShadowMap + , int index, float bias, bool receiveShadow + #endif + #ifdef _Spot + , bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right + #endif + ) { + vec3 ld = lp - p; + vec3 l = normalize(ld); + vec3 h = normalize(v + l); + float dotNH = max(0.0, dot(n, h)); + float dotVH = max(0.0, dot(v, h)); + float dotNL = max(0.0, dot(n, l)); + + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + + specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec; + + direct *= lightCol; + direct *= attenuate(distance(p, lp)); + + #ifdef _Spot + if (isSpot) { + direct *= spotlightMask(l, spotDir, right, scale, spotSize, spotBlend); + + #ifdef _ShadowMap + if (receiveShadow) { + #ifdef _SinglePoint + vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0); + direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + #endif + #ifdef _Clusters + vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0); + #ifdef _ShadowMapAtlas + direct *= shadowTest( + #ifndef _SingleAtlas + shadowMapAtlasSpot + #else + shadowMapAtlas + #endif + , lPos.xyz / lPos.w, bias + ); + #else + if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); + else if (index == 1) direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); + else if (index == 2) direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); + else if (index == 3) direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); + #endif + #endif + } + #endif + return direct; + } + #endif + + #ifdef _ShadowMap + + if (receiveShadow) { + #ifdef _SinglePoint + #ifndef _Spot + direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + #endif + #endif + #ifdef _Clusters + #ifdef _ShadowMapAtlas + direct *= PCFFakeCube( + #ifndef _SingleAtlas + shadowMapAtlasPoint + #else + shadowMapAtlas + #endif + , ld, -l, bias, lightProj, n, index + ); + #else + if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); + else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); + else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); + else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); + #endif + #endif + } + #endif + + return direct; +} + +#endif diff --git a/Shaders/std/ltc.glsl b/Shaders/std/ltc.glsl new file mode 100644 index 0000000000..87eb0d9306 --- /dev/null +++ b/Shaders/std/ltc.glsl @@ -0,0 +1,157 @@ +// Linearly Transformed Cosines +// https://eheitzresearch.wordpress.com/415-2/ + +const float LUT_SIZE = 64.0; +const float LUT_SCALE = (LUT_SIZE - 1.0) / LUT_SIZE; +const float LUT_BIAS = 0.5 / LUT_SIZE; + +vec3 L0; +vec3 L1; +vec3 L2; +vec3 L3; +vec3 L4; + +float integrateEdge(vec3 v1, vec3 v2) { + float cosTheta = dot(v1, v2); + float theta = acos(cosTheta); + float res = cross(v1, v2).z * ((theta > 0.001) ? theta / sin(theta) : 1.0); + return res; +} + +int clipQuadToHorizon(/*inout vec3 L[5], out int n*/) { + int n = 0; + // Detect clipping config + int config = 0; + if (L0.z > 0.0) config += 1; + if (L1.z > 0.0) config += 2; + if (L2.z > 0.0) config += 4; + if (L3.z > 0.0) config += 8; + + // Clip + if (config == 0) { + // Clip all + } + else if (config == 1) { // V1 clip V2 V3 V4 + n = 3; + L1 = -L1.z * L0 + L0.z * L1; + L2 = -L3.z * L0 + L0.z * L3; + } + else if (config == 2) { // V2 clip V1 V3 V4 + n = 3; + L0 = -L0.z * L1 + L1.z * L0; + L2 = -L2.z * L1 + L1.z * L2; + } + else if (config == 3) { // V1 V2 clip V3 V4 + n = 4; + L2 = -L2.z * L1 + L1.z * L2; + L3 = -L3.z * L0 + L0.z * L3; + } + else if (config == 4) { // V3 clip V1 V2 V4 + n = 3; + L0 = -L3.z * L2 + L2.z * L3; + L1 = -L1.z * L2 + L2.z * L1; + } + else if (config == 5) { // V1 V3 clip V2 V4) impossible + n = 0; + } + else if (config == 6) { // V2 V3 clip V1 V4 + n = 4; + L0 = -L0.z * L1 + L1.z * L0; + L3 = -L3.z * L2 + L2.z * L3; + } + else if (config == 7) { // V1 V2 V3 clip V4 + n = 5; + L4 = -L3.z * L0 + L0.z * L3; + L3 = -L3.z * L2 + L2.z * L3; + } + else if (config == 8) { // V4 clip V1 V2 V3 + n = 3; + L0 = -L0.z * L3 + L3.z * L0; + L1 = -L2.z * L3 + L3.z * L2; + L2 = L3; + } + else if (config == 9) { // V1 V4 clip V2 V3 + n = 4; + L1 = -L1.z * L0 + L0.z * L1; + L2 = -L2.z * L3 + L3.z * L2; + } + else if (config == 10) { // V2 V4 clip V1 V3) impossible + n = 0; + } + else if (config == 11) { // V1 V2 V4 clip V3 + n = 5; + L4 = L3; + L3 = -L2.z * L3 + L3.z * L2; + L2 = -L2.z * L1 + L1.z * L2; + } + else if (config == 12) { // V3 V4 clip V1 V2 + n = 4; + L1 = -L1.z * L2 + L2.z * L1; + L0 = -L0.z * L3 + L3.z * L0; + } + else if (config == 13) { // V1 V3 V4 clip V2 + n = 5; + L4 = L3; + L3 = L2; + L2 = -L1.z * L2 + L2.z * L1; + L1 = -L1.z * L0 + L0.z * L1; + } + else if (config == 14) { // V2 V3 V4 clip V1 + n = 5; + L4 = -L0.z * L3 + L3.z * L0; + L0 = -L0.z * L1 + L1.z * L0; + } + else if (config == 15) { // V1 V2 V3 V4 + n = 4; + } + + if (n == 3) L3 = L0; + if (n == 4) L4 = L0; + return n; +} + +float ltcEvaluate(vec3 N, vec3 V, float dotNV, vec3 P, mat3 Minv, vec3 points0, vec3 points1, vec3 points2, vec3 points3) { + // Construct orthonormal basis around N + vec3 T1, T2; + T1 = normalize(V - N * dotNV); + T2 = cross(N, T1); + + // Rotate area light in (T1, T2, R) basis + Minv = Minv * transpose(mat3(T1, T2, N)); + + // Polygon (allocate 5 vertices for clipping) + // vec3 L[5]; + L0 = Minv * (points0 - P); + L1 = Minv * (points1 - P); + L2 = Minv * (points2 - P); + L3 = Minv * (points3 - P); + L4 = vec3(0.0); + + // int n; + int n = clipQuadToHorizon(/*L, n*/); + + if (n == 0) return 0.0; + + // Project onto sphere + L0 = normalize(L0); + L1 = normalize(L1); + L2 = normalize(L2); + L3 = normalize(L3); + L4 = normalize(L4); + + // Integrate + float sum = 0.0; + + sum += integrateEdge(L0, L1); + sum += integrateEdge(L1, L2); + sum += integrateEdge(L2, L3); + + if (n >= 4) sum += integrateEdge(L3, L4); + if (n == 5) sum += integrateEdge(L4, L0); + +#ifdef _TwoSidedAreaLight + return abs(sum); +#else + return max(0.0, -sum); +#endif +} diff --git a/Shaders/std/mapping.glsl b/Shaders/std/mapping.glsl new file mode 100644 index 0000000000..3bc80cf68d --- /dev/null +++ b/Shaders/std/mapping.glsl @@ -0,0 +1,41 @@ +/* +https://github.com/JonasFolletete/glsl-triplanar-mapping + +MIT License + +Copyright (c) 2018 Jonas Folletête + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +vec3 blendNormal(vec3 normal) { + vec3 blending = abs(normal); + blending = normalize(max(blending, 0.00001)); + blending /= vec3(blending.x + blending.y + blending.z); + return blending; +} + +vec3 triplanarMapping (sampler2D ImageTexture, vec3 normal, vec3 position) { + vec3 normalBlend = blendNormal(normal); + vec3 xColor = texture(ImageTexture, position.yz).rgb; + vec3 yColor = texture(ImageTexture, position.xz).rgb; + vec3 zColor = texture(ImageTexture, position.xy).rgb; + + return (xColor * normalBlend.x + yColor * normalBlend.y + zColor * normalBlend.z); +} diff --git a/Shaders/std/math.glsl b/Shaders/std/math.glsl new file mode 100644 index 0000000000..a4a641388c --- /dev/null +++ b/Shaders/std/math.glsl @@ -0,0 +1,48 @@ + +#ifndef _MATH_GLSL_ +#define _MATH_GLSL_ + +float hash(const vec2 p) { + float h = dot(p, vec2(127.1, 311.7)); + return fract(sin(h) * 43758.5453123); +} + +vec2 envMapEquirect(const vec3 normal) { + const float PI = 3.1415926535; + const float PI2 = PI * 2.0; + float phi = acos(normal.z); + float theta = atan(-normal.y, normal.x) + PI; + return vec2(theta / PI2, phi / PI); +} + +float rand(const vec2 co) { // Unreliable + return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453); +} + +vec2 rand2(const vec2 coord) { + const float width = 1100.0; + const float height = 500.0; + float noiseX = ((fract(1.0 - coord.s * (width / 2.0)) * 0.25) + (fract(coord.t * (height / 2.0)) * 0.75)) * 2.0 - 1.0; + float noiseY = ((fract(1.0 - coord.s * (width / 2.0)) * 0.75) + (fract(coord.t * (height / 2.0)) * 0.25)) * 2.0 - 1.0; + return vec2(noiseX, noiseY); +} + +float linearize(const float depth, vec2 cameraProj) { + // to viewz + return cameraProj.y / (depth - cameraProj.x); +} + +float attenuate(const float dist) { +// float attenuate(float dist, float constant, float linear, float quadratic) { + return 1.0 / (dist * dist); + // 1.0 / (constant * 1.0) + // 1.0 / (linear * dist) + // 1.0 / (quadratic * dist * dist); +} + +float safe_acos(const float x) { + // acos is undefined if |x| > 1 + return acos(clamp(x, -1.0, 1.0)); +} + +#endif diff --git a/Shaders/std/morph_target.glsl b/Shaders/std/morph_target.glsl new file mode 100644 index 0000000000..b4746238a8 --- /dev/null +++ b/Shaders/std/morph_target.glsl @@ -0,0 +1,53 @@ +uniform sampler2D morphDataPos; +uniform sampler2D morphDataNor; +uniform vec2 morphScaleOffset; +uniform vec2 morphDataDim; +uniform vec4 morphWeights[8]; + +void getMorphedVertex(vec2 uvCoord, inout vec3 A){ + for(int i = 0; i<8; i++ ) + { + vec4 tempCoordY = vec4( uvCoord.y - (i * 4) * morphDataDim.y, + uvCoord.y - (i * 4 + 1) * morphDataDim.y, + uvCoord.y - (i * 4 + 2) * morphDataDim.y, + uvCoord.y - (i * 4 + 3) * morphDataDim.y); + + vec3 morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.x)).rgb * morphScaleOffset.x + morphScaleOffset.y; + A += morphWeights[i].x * morph; + + morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.y)).rgb * morphScaleOffset.x + morphScaleOffset.y; + A += morphWeights[i].y * morph; + + morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.z)).rgb * morphScaleOffset.x + morphScaleOffset.y; + A += morphWeights[i].z * morph; + + morph = texture(morphDataPos, vec2(uvCoord.x, tempCoordY.w)).rgb * morphScaleOffset.x + morphScaleOffset.y; + A += morphWeights[i].w * morph; + } +} + +void getMorphedNormal(vec2 uvCoord, vec3 oldNor, inout vec3 morphNor){ + + for(int i = 0; i<8; i++ ) + { + vec4 tempCoordY = vec4( uvCoord.y - (i * 4) * morphDataDim.y, + uvCoord.y - (i * 4 + 1) * morphDataDim.y, + uvCoord.y - (i * 4 + 2) * morphDataDim.y, + uvCoord.y - (i * 4 + 3) * morphDataDim.y); + + vec3 norm = oldNor + morphWeights[i].x * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.x)).rgb * 2.0 - 1.0); + morphNor += norm; + + norm = oldNor + morphWeights[i].y * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.y)).rgb * 2.0 - 1.0); + morphNor += norm; + + norm = oldNor + morphWeights[i].z * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.z)).rgb * 2.0 - 1.0); + morphNor += norm; + + norm = oldNor + morphWeights[i].w * (texture(morphDataNor, vec2(uvCoord.x, tempCoordY.w)).rgb * 2.0 - 1.0); + morphNor += norm; + + } + + morphNor = normalize(morphNor); +} \ No newline at end of file diff --git a/Shaders/std/normals.glsl b/Shaders/std/normals.glsl new file mode 100644 index 0000000000..3cd08094b0 --- /dev/null +++ b/Shaders/std/normals.glsl @@ -0,0 +1,31 @@ +// http://www.thetenthplanet.de/archives/1180 +mat3 cotangentFrame(const vec3 n, const vec3 p, const vec2 duv1, const vec2 duv2) { + // Get edge vectors of the pixel triangle + vec3 dp1 = dFdx(p); + vec3 dp2 = dFdy(p); + + // Solve the linear system + vec3 dp2perp = cross(dp2, n); + vec3 dp1perp = cross(n, dp1); + vec3 t = dp2perp * duv1.x + dp1perp * duv2.x; + vec3 b = dp2perp * duv1.y + dp1perp * duv2.y; + + // Construct a scale-invariant frame + float invmax = inversesqrt(max(dot(t, t), dot(b, b))); + return mat3(t * invmax, b * invmax, n); +} + +mat3 cotangentFrame(const vec3 n, const vec3 p, const vec2 texCoord) { + return cotangentFrame(n, p, dFdx(texCoord), dFdy(texCoord)); +} + +// vec3 perturbNormal(vec3 n, vec3 v, vec2 texCoord) { + // Assume N, the interpolated vertex normal and V, the view vector (vertex to eye) + // vec3 map = texture(snormal, texCoord).xyz * (255.0 / 127.0) - (128.0 / 127.0); +// WITH_NORMALMAP_2CHANNEL + // map.z = sqrt(1.0 - dot(map.xy, map.xy)); +// WITH_NORMALMAP_GREEN_UP + // map.y = -map.y; + // mat3 TBN = cotangentFrame(n, -v, texCoord); + // return normalize(TBN * map); +// } diff --git a/Shaders/std/resample.glsl b/Shaders/std/resample.glsl new file mode 100644 index 0000000000..ffa738cd00 --- /dev/null +++ b/Shaders/std/resample.glsl @@ -0,0 +1,228 @@ +#ifndef _RESAMPLE_GLSL_ +#define _RESAMPLE_GLSL_ + +float karisWeight(const vec3 value) { + // Using brightness instead of luma + return 1.0 / (1.0 + max(value.r, max(value.g, value.b))); +} + +/** + Downsampling using a 4x4 box filter. +**/ +vec3 downsample_box_filter(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + vec4 delta = texelSize.xyxy * vec4(-0.5, -0.5, 0.5, 0.5); + + vec3 result; + result = textureLod(tex, texCoord + delta.xy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.xw, 0.0).rgb; + result += textureLod(tex, texCoord + delta.zw, 0.0).rgb; + + return result * (1.0 / 4.0); +} + +vec3 downsample_box_filter_anti_flicker(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + vec4 delta = texelSize.xyxy * vec4(-0.5, -0.5, 0.5, 0.5); + + vec3 bl = textureLod(tex, texCoord + delta.xy, 0.0).rgb; + vec3 br = textureLod(tex, texCoord + delta.zy, 0.0).rgb; + vec3 tl = textureLod(tex, texCoord + delta.xw, 0.0).rgb; + vec3 tr = textureLod(tex, texCoord + delta.zw, 0.0).rgb; + + // Weighted averaging technique by Brian Karis to reduce fireflies: + // http://graphicrants.blogspot.com/2013/12/tone-mapping.html + float blWeight = karisWeight(bl); + float brWeight = karisWeight(br); + float tlWeight = karisWeight(tl); + float trWeight = karisWeight(tr); + + return (bl * blWeight + br * brWeight + tl * tlWeight + tr * trWeight) / (blWeight + brWeight + tlWeight + trWeight); +} + +/** + Downsample using the "dual filtering" technique from "Bandwidth-Efficient Rendering" + by Marius Bjørge, Siggraph 2015: + https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf +**/ +vec3 downsample_dual_filter(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + vec3 delta = texelSize.xyx * vec3(0.5, 0.5, -0.5); + + vec3 result; + result = textureLod(tex, texCoord, 0.0).rgb * 4.0; + result += textureLod(tex, texCoord - delta.xy, 0.0).rgb; + result += textureLod(tex, texCoord - delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + return result * (1.0 / 8.0); +} + +vec3 downsample_dual_filter_anti_flicker(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + vec3 delta = texelSize.xyx * vec3(0.5, 0.5, -0.5); + + vec3 c = textureLod(tex, texCoord, 0.0).rgb; + vec3 bl = textureLod(tex, texCoord - delta.xy, 0.0).rgb; + vec3 br = textureLod(tex, texCoord - delta.zy, 0.0).rgb; + vec3 tl = textureLod(tex, texCoord + delta.zy, 0.0).rgb; + vec3 tr = textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + float cWeight = karisWeight(c) * 4.0; + float blWeight = karisWeight(bl); + float brWeight = karisWeight(br); + float tlWeight = karisWeight(tl); + float trWeight = karisWeight(tr); + + return (c * cWeight + bl * blWeight + br * brWeight + tl * tlWeight + tr * trWeight) / (cWeight + blWeight + brWeight + tlWeight + trWeight); +} + +/** + Downsample using the approach from "NEXT GENERATION POST PROCESSING IN CALL OF DUTY: ADVANCED WARFARE" + by Jorge Jimenez, SIGGRAPH 2014: https://advances.realtimerendering.com/s2014/#_NEXT_GENERATION_POST +**/ +vec3 downsample_13_tap(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + /* + | TL T TR | + | tl tr | + | L C R | + | bl br | + | BL B BR | + */ + + vec4 delta = texelSize.xyxy * vec4(1.0, 1.0, -1.0, 0.0); + vec4 deltaHalf = delta * 0.5; + + // TODO investigate if sampling in morton order is faster here + + vec3 TL = textureLod(tex, texCoord + delta.zy, 0.0).rgb; + vec3 T = textureLod(tex, texCoord + delta.wy, 0.0).rgb; + vec3 TR = textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + vec3 L = textureLod(tex, texCoord + delta.zw, 0.0).rgb; + vec3 C = textureLod(tex, texCoord, 0.0).rgb; + vec3 R = textureLod(tex, texCoord + delta.xw, 0.0).rgb; + + vec3 BL = textureLod(tex, texCoord - delta.xy, 0.0).rgb; + vec3 B = textureLod(tex, texCoord - delta.wy, 0.0).rgb; + vec3 BR = textureLod(tex, texCoord - delta.zy, 0.0).rgb; + + vec3 tl = textureLod(tex, texCoord + deltaHalf.zy, 0.0).rgb; + vec3 tr = textureLod(tex, texCoord + deltaHalf.xy, 0.0).rgb; + vec3 bl = textureLod(tex, texCoord - deltaHalf.xy, 0.0).rgb; + vec3 br = textureLod(tex, texCoord - deltaHalf.zy, 0.0).rgb; + + vec3 result; + result = C * 0.125; + result += (TL + TR + BL + BR) * 0.03125; + result += (T + L + R + B) * 0.0625; + result += (tl + tr + bl + br) * 0.125; + return result; +} + +vec3 downsample_13_tap_anti_flicker(const sampler2D tex, const vec2 texCoord, const vec2 texelSize) { + vec4 delta = texelSize.xyxy * vec4(1.0, 1.0, -1.0, 0.0); + vec4 deltaHalf = delta * 0.5; + + vec3 TL = textureLod(tex, texCoord + delta.zy, 0.0).rgb; + vec3 T = textureLod(tex, texCoord + delta.wy, 0.0).rgb; + vec3 TR = textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + vec3 L = textureLod(tex, texCoord + delta.zw, 0.0).rgb; + vec3 C = textureLod(tex, texCoord, 0.0).rgb; + vec3 R = textureLod(tex, texCoord + delta.xw, 0.0).rgb; + + vec3 BL = textureLod(tex, texCoord - delta.xy, 0.0).rgb; + vec3 B = textureLod(tex, texCoord - delta.wy, 0.0).rgb; + vec3 BR = textureLod(tex, texCoord - delta.zy, 0.0).rgb; + + vec3 tl = textureLod(tex, texCoord + deltaHalf.zy, 0.0).rgb; + vec3 tr = textureLod(tex, texCoord + deltaHalf.xy, 0.0).rgb; + vec3 bl = textureLod(tex, texCoord - deltaHalf.xy, 0.0).rgb; + vec3 br = textureLod(tex, texCoord - deltaHalf.zy, 0.0).rgb; + + // Apply Karis average to groups of four sampled values, adapted from + // Jimenez 2014. The similar but faster implementation from + // https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom + // doesn't work as it doesn't really reduce fireflies :/ + + float TLWeight = karisWeight(TL); + float TWeight = karisWeight(T); + float TRWeight = karisWeight(TR); + + float LWeight = karisWeight(L); + float CWeight = karisWeight(C); + float RWeight = karisWeight(R); + + float BLWeight = karisWeight(BL); + float BWeight = karisWeight(B); + float BRWeight = karisWeight(BR); + + float tlWeight = karisWeight(tl); + float trWeight = karisWeight(tr); + float blWeight = karisWeight(bl); + float brWeight = karisWeight(br); + + vec3 result; + result = 0.125 * (TL * TLWeight + T * TWeight + L * LWeight + C * CWeight) / (TLWeight + TWeight + LWeight + CWeight); + result += 0.125 * (T * TWeight + TR * TRWeight + C * CWeight + R * RWeight) / (TWeight + TRWeight + CWeight + RWeight); + result += 0.125 * (L * LWeight + C * CWeight + BL * BLWeight + B * BWeight) / (LWeight + CWeight + BLWeight + BWeight); + result += 0.125 * (C * CWeight + R * RWeight + B * BWeight + R * RWeight) / (CWeight + RWeight + BWeight + RWeight); + result += 0.5 * (tl * tlWeight + tr * trWeight + bl * blWeight + br * brWeight) / (tlWeight + trWeight + blWeight + brWeight); + + return result; +} + +vec3 upsample_4tap_bilinear(const sampler2D tex, const vec2 texCoord, const vec2 texelSize, const float sampleScale) { + vec3 delta = texelSize.xyx * vec3(1.0, 1.0, -1.0) * sampleScale; + + vec3 result; + result = textureLod(tex, texCoord - delta.xy, 0.0).rgb; + result += textureLod(tex, texCoord - delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + return result * (1.0 / 4.0); +} + +vec3 upsample_dual_filter(const sampler2D tex, const vec2 texCoord, const vec2 texelSize, const float sampleScale) { + vec2 delta = texelSize * sampleScale; + + vec3 result; + result = textureLod(tex, texCoord + vec2(-delta.x * 2.0, 0.0), 0.0).rgb; + result += textureLod(tex, texCoord + vec2(-delta.x, delta.y), 0.0).rgb * 2.0; + result += textureLod(tex, texCoord + vec2(0.0, delta.y * 2.0), 0.0).rgb; + result += textureLod(tex, texCoord + delta, 0.0).rgb * 2.0; + result += textureLod(tex, texCoord + vec2(delta.x * 2.0, 0.0), 0.0).rgb; + result += textureLod(tex, texCoord + vec2(delta.x, -delta.y), 0.0).rgb * 2.0; + result += textureLod(tex, texCoord + vec2(0.0, -delta.y * 2.0), 0.0).rgb; + result += textureLod(tex, texCoord - delta, 0.0).rgb * 2.0; + + return result * (1.0 / 12.0); +} + +/** + 3x3 (9-tap) tent/bartlett filter, which approximates gaussian blur if applied repeatedly: + - Wojciech Jarosz: Fast Image Convolutions + http://elynxsdk.free.fr/ext-docs/Blur/Fast_box_blur.pdf + - Martin Kraus, Magnus Strengert: Pyramid Filters Based on Bilinear Interpolation + https://www.cs.cit.tum.de/fileadmin/w00cfj/cg/Research/Publications/2007/Pyramid_Filters/GRAPP07.pdf +**/ +vec3 upsample_tent_filter_3x3(const sampler2D tex, const vec2 texCoord, const vec2 texelSize, const float sampleScale) { + vec4 delta = texelSize.xyxy * vec4(1.0, 1.0, -1.0, 0.0) * sampleScale; + + vec3 result; + result = textureLod(tex, texCoord - delta.xy, 0.0).rgb; + result += textureLod(tex, texCoord - delta.wy, 0.0).rgb * 2.0; + result += textureLod(tex, texCoord - delta.zy, 0.0).rgb; + + result += textureLod(tex, texCoord + delta.zw, 0.0).rgb * 2.0; + result += textureLod(tex, texCoord , 0.0).rgb * 4.0; + result += textureLod(tex, texCoord + delta.xw, 0.0).rgb * 2.0; + + result += textureLod(tex, texCoord + delta.zy, 0.0).rgb; + result += textureLod(tex, texCoord + delta.wy, 0.0).rgb * 2.0; + result += textureLod(tex, texCoord + delta.xy, 0.0).rgb; + + return result * (1.0 / 16.0); +} + +#endif diff --git a/Shaders/std/shadows.glsl b/Shaders/std/shadows.glsl new file mode 100644 index 0000000000..2fea30c142 --- /dev/null +++ b/Shaders/std/shadows.glsl @@ -0,0 +1,371 @@ +#ifndef _SHADOWS_GLSL_ +#define _SHADOWS_GLSL_ + +#include "compiled.inc" + +#ifdef _CSM +uniform vec4 casData[shadowmapCascades * 4 + 4]; +#endif + +#ifdef _SMSizeUniform +uniform vec2 smSizeUniform; +#endif + +#ifdef _ShadowMap + #ifdef _Clusters + #ifdef _ShadowMapAtlas + uniform vec4 pointLightDataArray[maxLightsCluster * 6]; + #endif + #endif +#endif + +#ifdef _ShadowMapAtlas +// https://www.khronos.org/registry/OpenGL/specs/gl/glspec20.pdf // p:168 +// https://www.gamedev.net/forums/topic/687535-implementing-a-cube-map-lookup-function/5337472/ +vec2 sampleCube(vec3 dir, out int faceIndex) { + vec3 dirAbs = abs(dir); + float ma; + vec2 uv; + if(dirAbs.z >= dirAbs.x && dirAbs.z >= dirAbs.y) { + faceIndex = dir.z < 0.0 ? 5 : 4; + ma = 0.5 / dirAbs.z; + uv = vec2(dir.z < 0.0 ? -dir.x : dir.x, -dir.y); + } + else if(dirAbs.y >= dirAbs.x) { + faceIndex = dir.y < 0.0 ? 3 : 2; + ma = 0.5 / dirAbs.y; + uv = vec2(dir.x, dir.y < 0.0 ? -dir.z : dir.z); + } + else { + faceIndex = dir.x < 0.0 ? 1 : 0; + ma = 0.5 / dirAbs.x; + uv = vec2(dir.x < 0.0 ? dir.z : -dir.z, -dir.y); + } + // downscale uv a little to hide seams + // transform coordinates from clip space to texture space + #ifndef _FlipY + return uv * 0.9976 * ma + 0.5; + #else + #ifdef HLSL + return uv * 0.9976 * ma + 0.5; + #else + return vec2(uv.x * ma, uv.y * -ma) * 0.9976 + 0.5; + #endif + #endif +} +#endif + +float PCF(sampler2DShadow shadowMap, const vec2 uv, const float compare, const vec2 smSize) { + float result = texture(shadowMap, vec3(uv + (vec2(-1.0, -1.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(-1.0, 0.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(-1.0, 1.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(0.0, -1.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv, compare)); + result += texture(shadowMap, vec3(uv + (vec2(0.0, 1.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(1.0, -1.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(1.0, 0.0) / smSize), compare)); + result += texture(shadowMap, vec3(uv + (vec2(1.0, 1.0) / smSize), compare)); + return result / 9.0; +} + +float lpToDepth(vec3 lp, const vec2 lightProj) { + lp = abs(lp); + float zcomp = max(lp.x, max(lp.y, lp.z)); + zcomp = lightProj.x - lightProj.y / zcomp; + return zcomp * 0.5 + 0.5; +} + +float PCFCube(samplerCubeShadow shadowMapCube, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n) { + const float s = shadowmapCubePcfSize; // TODO: incorrect... + float compare = lpToDepth(lp, lightProj) - bias * 1.5; + ml = ml + n * bias * 20; + #ifdef _InvY + ml.y = -ml.y; + #endif + float result = texture(shadowMapCube, vec4(ml, compare)); + result += texture(shadowMapCube, vec4(ml + vec3(s, s, s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(-s, s, s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(s, -s, s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(s, s, -s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(-s, -s, s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(s, -s, -s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(-s, s, -s), compare)); + result += texture(shadowMapCube, vec4(ml + vec3(-s, -s, -s), compare)); + return result / 9.0; +} + +#ifdef _ShadowMapAtlas +// transform "out-of-bounds" coordinates to the correct face/coordinate system +// https://www.khronos.org/opengl/wiki/File:CubeMapAxes.png +vec2 transformOffsetedUV(const int faceIndex, out int newFaceIndex, vec2 uv) { + if (uv.x < 0.0) { + if (faceIndex == 0) { // X+ + newFaceIndex = 4; // Z+ + } + else if (faceIndex == 1) { // X- + newFaceIndex = 5; // Z- + } + else if (faceIndex == 2) { // Y+ + newFaceIndex = 1; // X- + } + else if (faceIndex == 3) { // Y- + newFaceIndex = 1; // X- + } + else if (faceIndex == 4) { // Z+ + newFaceIndex = 1; // X- + } + else { // Z- + newFaceIndex = 0; // X+ + } + uv = vec2(1.0 + uv.x, uv.y); + } + else if (uv.x > 1.0) { + if (faceIndex == 0) { // X+ + newFaceIndex = 5; // Z- + } + else if (faceIndex == 1) { // X- + newFaceIndex = 4; // Z+ + } + else if (faceIndex == 2) { // Y+ + newFaceIndex = 0; // X+ + } + else if (faceIndex == 3) { // Y- + newFaceIndex = 0; // X+ + } + else if (faceIndex == 4) { // Z+ + newFaceIndex = 0; // X+ + } + else { // Z- + newFaceIndex = 1; // X- + } + uv = vec2(1.0 - uv.x, uv.y); + } + else if (uv.y < 0.0) { + if (faceIndex == 0) { // X+ + newFaceIndex = 2; // Y+ + } + else if (faceIndex == 1) { // X- + newFaceIndex = 2; // Y+ + } + else if (faceIndex == 2) { // Y+ + newFaceIndex = 5; // Z- + } + else if (faceIndex == 3) { // Y- + newFaceIndex = 4; // Z+ + } + else if (faceIndex == 4) { // Z+ + newFaceIndex = 2; // Y+ + } + else { // Z- + newFaceIndex = 2; // Y+ + } + uv = vec2(uv.x, 1.0 + uv.y); + } + else if (uv.y > 1.0) { + if (faceIndex == 0) { // X+ + newFaceIndex = 3; // Y- + } + else if (faceIndex == 1) { // X- + newFaceIndex = 3; // Y- + } + else if (faceIndex == 2) { // Y+ + newFaceIndex = 4; // Z+ + } + else if (faceIndex == 3) { // Y- + newFaceIndex = 5; // Z- + } + else if (faceIndex == 4) { // Z+ + newFaceIndex = 3; // Y- + } + else { // Z- + newFaceIndex = 3; // Y- + } + uv = vec2(uv.x, 1.0 - uv.y); + } else { + newFaceIndex = faceIndex; + } + // cover corner cases too + return uv; +} + +float PCFFakeCube(sampler2DShadow shadowMap, const vec3 lp, vec3 ml, const float bias, const vec2 lightProj, const vec3 n, const int index) { + const vec2 smSize = smSizeUniform; // TODO: incorrect... + const float compare = lpToDepth(lp, lightProj) - bias * 1.5; + ml = ml + n * bias * 20; + + int faceIndex = 0; + const int lightIndex = index * 6; + const vec2 uv = sampleCube(ml, faceIndex); + + vec4 pointLightTile = pointLightDataArray[lightIndex + faceIndex]; // x: tile X offset, y: tile Y offset, z: tile size relative to atlas + vec2 uvtiled = pointLightTile.z * uv + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + + float result = texture(shadowMap, vec3(uvtiled, compare)); + // soft shadowing + int newFaceIndex = 0; + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 0.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, 1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, -1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(-1.0, -1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(0.0, 1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, -1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 0.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + uvtiled = transformOffsetedUV(faceIndex, newFaceIndex, vec2(uv + (vec2(1.0, 1.0) / smSize))); + pointLightTile = pointLightDataArray[lightIndex + newFaceIndex]; + uvtiled = pointLightTile.z * uvtiled + pointLightTile.xy; + #ifdef _FlipY + uvtiled.y = 1.0 - uvtiled.y; // invert Y coordinates for direct3d coordinate system + #endif + result += texture(shadowMap, vec3(uvtiled, compare)); + + return result / 9.0; +} +#endif + +float shadowTest(sampler2DShadow shadowMap, const vec3 lPos, const float shadowsBias) { + #ifdef _SMSizeUniform + vec2 smSize = smSizeUniform; + #else + const vec2 smSize = shadowmapSize; + #endif + if (lPos.x < 0.0 || lPos.y < 0.0 || lPos.x > 1.0 || lPos.y > 1.0) return 1.0; + return PCF(shadowMap, lPos.xy, lPos.z - shadowsBias, smSize); +} + +#ifdef _CSM +mat4 getCascadeMat(const float d, out int casi, out int casIndex) { + const int c = shadowmapCascades; + + // Get cascade index + // TODO: use bounding box slice selection instead of sphere + const vec4 ci = vec4(float(c > 0), float(c > 1), float(c > 2), float(c > 3)); + // int ci; + // if (d < casData[c * 4].x) ci = 0; + // else if (d < casData[c * 4].y) ci = 1 * 4; + // else if (d < casData[c * 4].z) ci = 2 * 4; + // else ci = 3 * 4; + // Splits + vec4 comp = vec4( + float(d > casData[c * 4].x), + float(d > casData[c * 4].y), + float(d > casData[c * 4].z), + float(d > casData[c * 4].w)); + casi = int(min(dot(ci, comp), c)); + + // Get cascade mat + casIndex = casi * 4; + + return mat4( + casData[casIndex ], + casData[casIndex + 1], + casData[casIndex + 2], + casData[casIndex + 3]); + + // if (casIndex == 0) return mat4(casData[0], casData[1], casData[2], casData[3]); + // .. +} + +float shadowTestCascade(sampler2DShadow shadowMap, const vec3 eye, const vec3 p, const float shadowsBias) { + #ifdef _SMSizeUniform + vec2 smSize = smSizeUniform; + #else + const vec2 smSize = shadowmapSize * vec2(shadowmapCascades, 1.0); + #endif + const int c = shadowmapCascades; + float d = distance(eye, p); + + int casi; + int casIndex; + mat4 LWVP = getCascadeMat(d, casi, casIndex); + + vec4 lPos = LWVP * vec4(p, 1.0); + lPos.xyz /= lPos.w; + + float visibility = 1.0; + if (lPos.w > 0.0) visibility = PCF(shadowMap, lPos.xy, lPos.z - shadowsBias, smSize); + + // Blend cascade + // https://github.com/TheRealMJP/Shadows + const float blendThres = 0.15; + float nextSplit = casData[c * 4][casi]; + float splitSize = casi == 0 ? nextSplit : nextSplit - casData[c * 4][casi - 1]; + float splitDist = (nextSplit - d) / splitSize; + if (splitDist <= blendThres && casi != c - 1) { + int casIndex2 = casIndex + 4; + mat4 LWVP2 = mat4( + casData[casIndex2 ], + casData[casIndex2 + 1], + casData[casIndex2 + 2], + casData[casIndex2 + 3]); + + vec4 lPos2 = LWVP2 * vec4(p, 1.0); + lPos2.xyz /= lPos2.w; + float visibility2 = 1.0; + if (lPos2.w > 0.0) visibility2 = PCF(shadowMap, lPos2.xy, lPos2.z - shadowsBias, smSize); + + float lerpAmt = smoothstep(0.0, blendThres, splitDist); + return mix(visibility2, visibility, lerpAmt); + } + return visibility; + + // Visualize cascades + // if (ci == 0) albedo.rgb = vec3(1.0, 0.0, 0.0); + // if (ci == 4) albedo.rgb = vec3(0.0, 1.0, 0.0); + // if (ci == 8) albedo.rgb = vec3(0.0, 0.0, 1.0); + // if (ci == 12) albedo.rgb = vec3(1.0, 1.0, 0.0); +} +#endif + +#endif diff --git a/Shaders/std/shirr.glsl b/Shaders/std/shirr.glsl new file mode 100644 index 0000000000..e56dc9782b --- /dev/null +++ b/Shaders/std/shirr.glsl @@ -0,0 +1,30 @@ + +vec3 shIrradiance(const vec3 nor, const vec4 shirr[7]) { + const float c1 = 0.429043; + const float c2 = 0.511664; + const float c3 = 0.743125; + const float c4 = 0.886227; + const float c5 = 0.247708; + // TODO: Use padding for 4th component and pass shirr[].xyz directly + vec3 cl00 = vec3(shirr[0].x, shirr[0].y, shirr[0].z); + vec3 cl1m1 = vec3(shirr[0].w, shirr[1].x, shirr[1].y); + vec3 cl10 = vec3(shirr[1].z, shirr[1].w, shirr[2].x); + vec3 cl11 = vec3(shirr[2].y, shirr[2].z, shirr[2].w); + vec3 cl2m2 = vec3(shirr[3].x, shirr[3].y, shirr[3].z); + vec3 cl2m1 = vec3(shirr[3].w, shirr[4].x, shirr[4].y); + vec3 cl20 = vec3(shirr[4].z, shirr[4].w, shirr[5].x); + vec3 cl21 = vec3(shirr[5].y, shirr[5].z, shirr[5].w); + vec3 cl22 = vec3(shirr[6].x, shirr[6].y, shirr[6].z); + return ( + c1 * cl22 * (nor.y * nor.y - (-nor.z) * (-nor.z)) + + c3 * cl20 * nor.x * nor.x + + c4 * cl00 - + c5 * cl20 + + 2.0 * c1 * cl2m2 * nor.y * (-nor.z) + + 2.0 * c1 * cl21 * nor.y * nor.x + + 2.0 * c1 * cl2m1 * (-nor.z) * nor.x + + 2.0 * c2 * cl11 * nor.y + + 2.0 * c2 * cl1m1 * (-nor.z) + + 2.0 * c2 * cl10 * nor.x + ); +} diff --git a/Shaders/std/skinning.glsl b/Shaders/std/skinning.glsl new file mode 100644 index 0000000000..0b9133a637 --- /dev/null +++ b/Shaders/std/skinning.glsl @@ -0,0 +1,28 @@ +// Geometric Skinning with Approximate Dual Quaternion Blending, Kavan +// Based on https://github.com/tcoppex/aer-engine/blob/master/demos/aura/data/shaders/Skinning.glsl +uniform vec4 skinBones[skinMaxBones * 2]; + +void getSkinningDualQuat(const ivec4 bone, vec4 weight, out vec4 A, inout vec4 B) { + // Retrieve the real and dual part of the dual-quaternions + ivec4 bonei = bone * 2; + mat4 matA = mat4( + skinBones[bonei.x], + skinBones[bonei.y], + skinBones[bonei.z], + skinBones[bonei.w]); + mat4 matB = mat4( + skinBones[bonei.x + 1], + skinBones[bonei.y + 1], + skinBones[bonei.z + 1], + skinBones[bonei.w + 1]); + // Handles antipodality by sticking joints in the same neighbourhood + // weight.xyz *= sign(matA[3] * mat3x4(matA)).xyz; + weight.xyz *= sign(matA[3] * matA).xyz; + // Apply weights + A = matA * weight; // Real part + B = matB * weight; // Dual part + // Normalize + float invNormA = 1.0 / length(A); + A *= invNormA; + B *= invNormA; +} diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl new file mode 100644 index 0000000000..eaac43526a --- /dev/null +++ b/Shaders/std/sky.glsl @@ -0,0 +1,155 @@ +/* Various sky functions + * ===================== + * + * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) + * + * Changes to the original implementation: + * - r and pSun parameters of nishita_atmosphere() are already normalized + * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values + * - Implemented air, dust and ozone density node parameters (see Blender source) + * - Replaced the inner integral calculation with a LUT lookup + * + * Reference for the sun's limb darkening and ozone calculations: + * [Hill] Sebastien Hillaire. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite + * (https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf) + * + * Cycles code used for reference: blender/intern/sky/source/sky_nishita.cpp + * (https://github.com/blender/blender/blob/4429b4b77ef6754739a3c2b4fabd0537999e9bdc/intern/sky/source/sky_nishita.cpp) + */ + +#ifndef _SKY_GLSL_ +#define _SKY_GLSL_ + +#include "std/math.glsl" + +uniform sampler2D nishitaLUT; +uniform vec2 nishitaDensity; + +#ifndef PI + #define PI 3.141592 +#endif +#ifndef HALF_PI + #define HALF_PI 1.570796 +#endif + +#define nishita_iSteps 16 + +// These values are taken from Cycles code if they +// exist there, otherwise they are taken from the example +// in the glsl-atmosphere repo +#define nishita_sun_intensity 22.0 +#define nishita_atmo_radius 6420e3 +#define nishita_rayleigh_scale 8e3 +#define nishita_rayleigh_coeff vec3(5.5e-6, 13.0e-6, 22.4e-6) +#define nishita_mie_scale 1.2e3 +#define nishita_mie_coeff 2e-5 +#define nishita_mie_dir 0.76 // Aerosols anisotropy ("direction") +#define nishita_mie_dir_sq 0.5776 // Squared aerosols anisotropy + +// Values from [Hill: 60] +#define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) + +vec3 nishita_lookupLUT(const float height, const float sunTheta) { + vec2 coords = vec2( + sqrt(height * (1 / nishita_atmo_radius)), + 0.5 + 0.5 * sign(sunTheta - HALF_PI) * sqrt(abs(sunTheta * (1 / HALF_PI) - 1)) + ); + return textureLod(nishitaLUT, coords, 0.0).rgb; +} + +/* See raySphereIntersection() in armory/Sources/renderpath/Nishita.hx */ +vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { + float a = dot(rd, rd); + float b = 2.0 * dot(rd, r0); + float c = dot(r0, r0) - (sr * sr); + float d = (b*b) - 4.0*a*c; + + // If d < 0.0 the ray does not intersect the sphere + return (d < 0.0) ? vec2(1e5,-1e5) : vec2((-b - sqrt(d))/(2.0*a), (-b + sqrt(d))/(2.0*a)); +} + +/* + * r: normalized ray direction + * r0: ray origin + * pSun: normalized sun direction + * rPlanet: planet radius + */ +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { + // Calculate the step size of the primary ray + vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); + if (p.x > p.y) return vec3(0.0); + p.y = min(p.y, nishita_rsi(r0, r, rPlanet).x); + float iStepSize = (p.y - p.x) / float(nishita_iSteps); + + // Primary ray time + float iTime = 0.0; + + // Accumulators for Rayleigh and Mie scattering. + vec3 totalRlh = vec3(0,0,0); + vec3 totalMie = vec3(0,0,0); + + // Optical depth accumulators for the primary ray + float iOdRlh = 0.0; + float iOdMie = 0.0; + + // Calculate the Rayleigh and Mie phases + float mu = dot(r, pSun); + float mumu = mu * mu; + float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu); + float pMie = 3.0 / (8.0 * PI) * ((1.0 - nishita_mie_dir_sq) * (mumu + 1.0)) / (pow(1.0 + nishita_mie_dir_sq - 2.0 * mu * nishita_mie_dir, 1.5) * (2.0 + nishita_mie_dir_sq)); + + // Sample the primary ray + for (int i = 0; i < nishita_iSteps; i++) { + + // Calculate the primary ray sample position and height + vec3 iPos = r0 + r * (iTime + iStepSize * 0.5); + float iHeight = length(iPos) - rPlanet; + + // Calculate the optical depth of the Rayleigh and Mie scattering for this step + float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * nishitaDensity.x * iStepSize; + float odStepMie = exp(-iHeight / nishita_mie_scale) * nishitaDensity.y * iStepSize; + + // Accumulate optical depth + iOdRlh += odStepRlh; + iOdMie += odStepMie; + + // Idea behind this: "Rotate" everything by iPos (-> iPos is the new zenith) and then all calculations for the + // inner integral only depend on the sample height (iHeight) and sunTheta (angle between sun and new zenith). + float sunTheta = safe_acos(dot(normalize(iPos), normalize(pSun))); + vec3 jAttn = nishita_lookupLUT(iHeight, sunTheta); + + // Calculate attenuation + vec3 iAttn = exp(-( + nishita_mie_coeff * iOdMie + + nishita_rayleigh_coeff * iOdRlh + // + 0 for ozone + )); + vec3 attn = iAttn * jAttn; + + // Apply dithering to reduce visible banding + attn *= 0.98 + rand(r.xy) * 0.04; + + // Accumulate scattering + totalRlh += odStepRlh * attn; + totalMie += odStepMie * attn; + + iTime += iStepSize; + } + + return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie); +} + +vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const float intensity) { + // Normalized SDF + float dist = distance(n, light_dir) / disk_size; + + // Darken the edges of the sun + // [Hill: 28, 60] (according to [Nec96]) + float invDist = 1.0 - dist; + float mu = sqrt(invDist * invDist); + vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); + + return 1 + (1.0 - step(1.0, dist)) * nishita_sun_intensity * intensity * limb_darkening; +} + +#endif diff --git a/Shaders/std/ssrs.glsl b/Shaders/std/ssrs.glsl new file mode 100644 index 0000000000..c958661006 --- /dev/null +++ b/Shaders/std/ssrs.glsl @@ -0,0 +1,36 @@ +#ifndef _SSRS_GLSL_ +#define _SSRS_GLSL_ + +#include "std/gbuffer.glsl" + +uniform mat4 VP; + +vec2 getProjectedCoord(vec3 hitCoord) { + vec4 projectedCoord = VP * vec4(hitCoord, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + #if defined(HLSL) || defined(METAL) + projectedCoord.y = 1.0 - projectedCoord.y; + #endif + return projectedCoord.xy; +} + +float getDeltaDepth(vec3 hitCoord, sampler2D gbufferD, mat4 invVP, vec3 eye) { + vec2 texCoord = getProjectedCoord(hitCoord); + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 wpos = getPos2(invVP, depth, texCoord); + float d1 = length(eye - wpos); + float d2 = length(eye - hitCoord); + return d1 - d2; +} + +float traceShadowSS(vec3 dir, vec3 hitCoord, sampler2D gbufferD, mat4 invVP, vec3 eye) { + dir *= ssrsRayStep; + for (int i = 0; i < 4; i++) { + hitCoord += dir; + if (getDeltaDepth(hitCoord, gbufferD, invVP, eye) > 0.0) return 1.0; + } + return 0.0; +} + +#endif diff --git a/Shaders/std/sss.glsl b/Shaders/std/sss.glsl new file mode 100644 index 0000000000..7af0878950 --- /dev/null +++ b/Shaders/std/sss.glsl @@ -0,0 +1,26 @@ + +// Separable SSS Transmittance Function, ref to sss_pass +vec3 SSSSTransmittance(mat4 LWVP, vec3 p, vec3 n, vec3 l, float lightFar, sampler2DShadow shadowMap) { + const float translucency = 1.0; + vec4 shrinkedPos = vec4(p - 0.005 * n, 1.0); + vec4 shadowPos = LWVP * shrinkedPos; + float scale = 8.25 * (1.0 - translucency) / (sssWidth / 10.0); + float d1 = texture(shadowMap, vec3(shadowPos.xy / shadowPos.w, shadowPos.z)).r; // 'd1' has a range of 0..1 + float d2 = shadowPos.z; // 'd2' has a range of 0..'lightFarPlane' + d1 *= lightFar; // So we scale 'd1' accordingly: + float d = scale * abs(d1 - d2); + + float dd = -d * d; + vec3 profile = vec3(0.233, 0.455, 0.649) * exp(dd / 0.0064) + + vec3(0.1, 0.336, 0.344) * exp(dd / 0.0484) + + vec3(0.118, 0.198, 0.0) * exp(dd / 0.187) + + vec3(0.113, 0.007, 0.007) * exp(dd / 0.567) + + vec3(0.358, 0.004, 0.0) * exp(dd / 1.99) + + vec3(0.078, 0.0, 0.0) * exp(dd / 7.41); + return profile * clamp(0.3 + dot(l, -n), 0.0, 1.0); +} + +vec3 SSSSTransmittanceCube(float translucency, vec4 shadowPos, vec3 n, vec3 l, float lightFar) { + // TODO + return vec3(0.0); +} diff --git a/Shaders/std/tonemap.glsl b/Shaders/std/tonemap.glsl new file mode 100644 index 0000000000..a2ce7eb363 --- /dev/null +++ b/Shaders/std/tonemap.glsl @@ -0,0 +1,38 @@ + +vec3 uncharted2Tonemap(const vec3 x) { + const float A = 0.15; + const float B = 0.50; + const float C = 0.10; + const float D = 0.20; + const float E = 0.02; + const float F = 0.30; + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; +} + +vec3 tonemapUncharted2(const vec3 color) { + const float W = 11.2; + const float exposureBias = 2.0; + vec3 curr = uncharted2Tonemap(exposureBias * color); + vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W)); + return curr * whiteScale; +} + +// Based on Filmic Tonemapping Operators http://filmicgames.com/archives/75 +vec3 tonemapFilmic(const vec3 color) { + vec3 x = max(vec3(0.0), color - 0.004); + return (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); +} + +// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ +vec3 acesFilm(const vec3 x) { + const float a = 2.51; + const float b = 0.03; + const float c = 2.43; + const float d = 0.59; + const float e = 0.14; + return clamp((x * (a * x + b)) / (x * (c * x + d ) + e), 0.0, 1.0); +} + +vec3 tonemapReinhard(const vec3 color) { + return color / (color + vec3(1.0)); +} diff --git a/Shaders/std/vr.glsl b/Shaders/std/vr.glsl new file mode 100644 index 0000000000..d016e7bbde --- /dev/null +++ b/Shaders/std/vr.glsl @@ -0,0 +1,26 @@ +uniform mat4 U; // Undistortion +uniform float maxRadSq; + +// GoogleVR Distortion using Vertex Displacement +float distortionFactor(const float rSquared) { + float ret = 0.0; + ret = rSquared * (ret + U[1][1]); + ret = rSquared * (ret + U[0][1]); + ret = rSquared * (ret + U[3][0]); + ret = rSquared * (ret + U[2][0]); + ret = rSquared * (ret + U[1][0]); + ret = rSquared * (ret + U[0][0]); + return ret + 1.0; +} +// Convert point from world space to undistorted camera space +vec4 undistort(const mat4 WV, vec4 pos) { + // Go to camera space + pos = WV * pos; + const float nearClip = 0.1; + if (pos.z <= -nearClip) { // Reminder: Forward is -Z + // Undistort the point's coordinates in XY + float r2 = clamp(dot(pos.xy, pos.xy) / (pos.z * pos.z), 0.0, maxRadSq); + pos.xy *= distortionFactor(r2); + } + return pos; +} diff --git a/Shaders/supersample_resolve/supersample_resolve.frag.glsl b/Shaders/supersample_resolve/supersample_resolve.frag.glsl new file mode 100644 index 0000000000..16e61fc423 --- /dev/null +++ b/Shaders/supersample_resolve/supersample_resolve.frag.glsl @@ -0,0 +1,14 @@ +#version 450 + +#include "std/filters.glsl" + +uniform sampler2D tex; +uniform vec2 screenSizeInv; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + // 4X resolve + fragColor = textureSS(tex, texCoord, screenSizeInv / 4.0); +} diff --git a/Shaders/supersample_resolve/supersample_resolve.json b/Shaders/supersample_resolve/supersample_resolve.json new file mode 100644 index 0000000000..46cb0bf12d --- /dev/null +++ b/Shaders/supersample_resolve/supersample_resolve.json @@ -0,0 +1,19 @@ +{ + "contexts": [ + { + "name": "supersample_resolve", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "screenSizeInv", + "link": "_screenSizeInv" + } + ], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "supersample_resolve.frag.glsl" + } + ] +} diff --git a/Shaders/taa_pass/taa_pass.frag.glsl b/Shaders/taa_pass/taa_pass.frag.glsl new file mode 100644 index 0000000000..cdaf2bbb40 --- /dev/null +++ b/Shaders/taa_pass/taa_pass.frag.glsl @@ -0,0 +1,44 @@ +#version 450 + +#include "compiled.inc" + +uniform sampler2D tex; +uniform sampler2D tex2; +#ifdef _Veloc +uniform sampler2D sveloc; +#endif + +in vec2 texCoord; +out vec4 fragColor; + +const float SMAA_REPROJECTION_WEIGHT_SCALE = 30.0; + +void main() { + vec4 current = textureLod(tex, texCoord, 0.0); + +#ifdef _Veloc + // Velocity is assumed to be calculated for motion blur, so we need to inverse it for reprojection + vec2 velocity = -textureLod(sveloc, texCoord, 0.0).rg; + + #ifdef _InvY + velocity.y = -velocity.y; + #endif + + // Reproject current coordinates and fetch previous pixel + vec4 previous = textureLod(tex2, texCoord + velocity, 0.0); + + // Attenuate the previous pixel if the velocity is different + #ifdef _SMAA + float delta = abs(current.a * current.a - previous.a * previous.a) / 5.0; + #else + const float delta = 0.0; + #endif + float weight = 0.5 * clamp(1.0 - sqrt(delta) * SMAA_REPROJECTION_WEIGHT_SCALE, 0.0, 1.0); + + // Blend the pixels according to the calculated weight: + fragColor = vec4(mix(current.rgb, previous.rgb, weight), 1.0); +#else + vec4 previous = textureLod(tex2, texCoord, 0.0); + fragColor = vec4(mix(current.rgb, previous.rgb, 0.5), 1.0); +#endif +} diff --git a/Shaders/taa_pass/taa_pass.json b/Shaders/taa_pass/taa_pass.json new file mode 100644 index 0000000000..49a8d001bd --- /dev/null +++ b/Shaders/taa_pass/taa_pass.json @@ -0,0 +1,14 @@ +{ + "contexts": [ + { + "name": "taa_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [], + "texture_params": [], + "vertex_shader": "../include/pass.vert.glsl", + "fragment_shader": "taa_pass.frag.glsl" + } + ] +} diff --git a/Shaders/translucent_resolve/translucent_resolve.frag.glsl b/Shaders/translucent_resolve/translucent_resolve.frag.glsl new file mode 100644 index 0000000000..43720319f1 --- /dev/null +++ b/Shaders/translucent_resolve/translucent_resolve.frag.glsl @@ -0,0 +1,28 @@ +// Weighted blended OIT by McGuire and Bavoil +#version 450 + +#include "compiled.inc" + + +uniform sampler2D gbuffer0; // accum +uniform sampler2D gbuffer1; // revealage + +uniform vec2 texSize; + +uniform sampler2D accum; +uniform sampler2D revealage; +in vec2 texCoord; +out vec4 fragColor; + +void main() { + vec4 accum = texelFetch(gbuffer0, ivec2(texCoord * texSize), 0); + float revealage = 1.0 - accum.a; + + // Save the blending and color texture fetch cost + if (revealage == 0.0) { + discard; + } + + float f = texelFetch(gbuffer1, ivec2(texCoord * texSize), 0).r; + fragColor = vec4(accum.rgb / clamp(f, 0.0001, 5000), revealage); +} diff --git a/Shaders/translucent_resolve/translucent_resolve.json b/Shaders/translucent_resolve/translucent_resolve.json new file mode 100644 index 0000000000..f6fdfa5070 --- /dev/null +++ b/Shaders/translucent_resolve/translucent_resolve.json @@ -0,0 +1,24 @@ +{ + "contexts": [ + { + "name": "translucent_resolve", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "source_alpha", + "blend_destination": "inverse_source_alpha", + "blend_operation": "add", + "alpha_blend_source": "source_alpha", + "alpha_blend_destination": "inverse_source_alpha", + "alpha_blend_operation": "add", + "links": [ + { + "name": "texSize", + "link": "_screenSize" + } + ], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "translucent_resolve.frag.glsl" + } + ] +} diff --git a/Shaders/volumetric_light/volumetric_light.frag.glsl b/Shaders/volumetric_light/volumetric_light.frag.glsl new file mode 100644 index 0000000000..19beb25fba --- /dev/null +++ b/Shaders/volumetric_light/volumetric_light.frag.glsl @@ -0,0 +1,166 @@ +// http://sebastien.hillaire.free.fr/index.php?option=com_content&view=article&id=72&Itemid=106 +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" +#include "std/shadows.glsl" +#ifdef _Clusters +#include "std/clusters.glsl" +#endif +#ifdef _Spot +#include "std/light_common.glsl" +#endif + +uniform sampler2D gbufferD; +uniform sampler2D snoise; + +#ifdef _Clusters +uniform vec4 lightsArray[maxLights * 3]; + #ifdef _Spot + uniform vec4 lightsArraySpot[maxLights * 2]; + #endif +uniform sampler2D clustersData; +uniform vec2 cameraPlane; +#endif + +#ifdef _ShadowMap +#ifdef _SinglePoint + #ifdef _Spot + uniform sampler2DShadow shadowMapSpot[1]; + uniform mat4 LWVPSpot[1]; + #else + uniform samplerCubeShadow shadowMapPoint[1]; + uniform vec2 lightProj; + #endif +#endif +#ifdef _Clusters + uniform samplerCubeShadow shadowMapPoint[4]; + uniform vec2 lightProj; + #ifdef _Spot + uniform sampler2DShadow shadowMapSpot[4]; + uniform mat4 LWVPSpot[maxLightsCluster]; + #endif +#endif +#endif + +#ifdef _Sun + #ifdef _ShadowMap + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + uniform sampler2DShadow shadowMapAtlasSun; + #else + uniform sampler2DShadow shadowMapAtlas; + #endif + #else + uniform sampler2DShadow shadowMap; + #endif + uniform float shadowsBias; + #ifdef _CSM + //!uniform vec4 casData[shadowmapCascades * 4 + 4]; + #else + uniform mat4 LWVP; + #endif + #endif // _ShadowMap +#endif + +#ifdef _SinglePoint // Fast path for single light +uniform vec3 pointPos; +uniform vec3 pointCol; + #ifdef _ShadowMap + uniform float pointBias; + #endif + #ifdef _Spot + uniform vec3 spotDir; + uniform vec3 spotRight; + uniform vec4 spotData; + #endif +#endif + +uniform vec2 cameraProj; +uniform vec3 eye; +uniform vec3 eyeLook; + +in vec2 texCoord; +in vec3 viewRay; +out float fragColor; + +const float tScat = 0.08; +const float tAbs = 0.0; +const float tExt = tScat + tAbs; +const float stepLen = 1.0 / volumSteps; +const float lighting = 0.4; + +void rayStep(inout vec3 curPos, inout float curOpticalDepth, inout float scatteredLightAmount, float stepLenWorld, vec3 viewVecNorm) { + curPos += stepLenWorld * viewVecNorm; + const float density = 1.0; + + float l1 = lighting * stepLenWorld * tScat * density; + curOpticalDepth *= exp(-tExt * stepLenWorld * density); + + float visibility = 0.0; + vec4 lPos; + +#ifdef _Sun + #ifdef _CSM + mat4 LWVP = mat4(casData[4], casData[4 + 1], casData[4 + 2], casData[4 + 3]); + #endif + lPos = LWVP * vec4(curPos, 1.0); + lPos.xyz /= lPos.w; + visibility = texture( + #ifdef _ShadowMapAtlas + #ifndef _SingleAtlas + shadowMapAtlasSun + #else + shadowMapAtlas + #endif + #else + shadowMap + #endif + , vec3(lPos.xy, lPos.z - shadowsBias)); +#endif + +#ifdef _SinglePoint + #ifdef _Spot + lPos = LWVPSpot[0] * vec4(curPos, 1.0); + visibility = shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, pointBias); + visibility *= spotlightMask(normalize(pointPos - curPos), spotDir, spotRight, spotData.zw, spotData.x, spotData.y); + #else + vec3 ld = pointPos - curPos; + visibility = PCFCube(shadowMapPoint[0], ld, -normalize(ld), pointBias, lightProj, vec3(0.0)); + #endif +#endif + +#ifdef _Clusters +#endif + + scatteredLightAmount += curOpticalDepth * l1 * visibility; +} + +void main() { + float pixelRayMarchNoise = textureLod(snoise, texCoord * 100, 0.0).r * 2.0 - 1.0; + + float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + vec3 p = getPos(eye, eyeLook, normalize(viewRay), depth, cameraProj); + + vec3 viewVec = p - eye; + float worldPosDist = length(viewVec); + vec3 viewVecNorm = viewVec / worldPosDist; + + float startDepth = 0.1; + startDepth = min(worldPosDist, startDepth); + float endDepth = 20.0; + endDepth = min(worldPosDist, endDepth); + + vec3 curPos = eye + viewVecNorm * startDepth; + float stepLenWorld = stepLen * (endDepth - startDepth); + float curOpticalDepth = exp(-tExt * stepLenWorld); + float scatteredLightAmount = 0.0; + + curPos += stepLenWorld * viewVecNorm * pixelRayMarchNoise; + + for (float l = stepLen; l < 0.99999; l += stepLen) { // Do not do the first and last steps + rayStep(curPos, curOpticalDepth, scatteredLightAmount, stepLenWorld, viewVecNorm); + } + + fragColor = scatteredLightAmount * volumAirTurbidity; +} diff --git a/Shaders/volumetric_light/volumetric_light.json b/Shaders/volumetric_light/volumetric_light.json new file mode 100644 index 0000000000..09a202f517 --- /dev/null +++ b/Shaders/volumetric_light/volumetric_light.json @@ -0,0 +1,150 @@ +{ + "contexts": [ + { + "name": "volumetric_light", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "links": [ + { + "name": "snoise", + "link": "$blue_noise64.png" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "lightsArray", + "link": "_lightsArray", + "ifdef": ["_Clusters"] + }, + { + "name": "lightsArraySpot", + "link": "_lightsArraySpot", + "ifdef": ["_Clusters", "_Spot"] + }, + { + "name": "clustersData", + "link": "_clustersData", + "ifdef": ["_Clusters"] + }, + { + "name": "cameraPlane", + "link": "_cameraPlane", + "ifdef": ["_Clusters"] + }, + { + "name": "sunDir", + "link": "_sunDirection", + "ifdef": ["_Sun"] + }, + { + "name": "sunCol", + "link": "_sunColor", + "ifdef": ["_Sun"] + }, + { + "name": "shadowsBias", + "link": "_sunShadowsBias", + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "LWVP", + "link": "_biasLightWorldViewProjectionMatrixSun", + "ifndef": ["_CSM"], + "ifdef": ["_Sun", "_ShadowMap"] + }, + { + "name": "casData", + "link": "_cascadeData", + "ifdef": ["_Sun", "_ShadowMap", "_CSM"] + }, + { + "name": "smSizeUniform", + "link": "_shadowMapSize", + "ifdef": ["_SMSizeUniform"] + }, + { + "name": "lightProj", + "link": "_lightPlaneProj", + "ifdef": ["_ShadowMap"] + }, + { + "name": "pointPos", + "link": "_pointPosition", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointCol", + "link": "_pointColor", + "ifdef": ["_SinglePoint"] + }, + { + "name": "pointBias", + "link": "_pointShadowsBias", + "ifdef": ["_SinglePoint", "_ShadowMap"] + }, + { + "name": "spotDir", + "link": "_spotDirection", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotData", + "link": "_spotData", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "spotRight", + "link": "_spotRight", + "ifdef": ["_SinglePoint", "_Spot"] + }, + { + "name": "LWVPSpotArray", + "link": "_biasLightWorldViewProjectionMatrixSpotArray", + "ifdef": ["_Clusters", "_ShadowMap", "_Spot"] + }, + { + "name": "LWVPSpot[0]", + "link": "_biasLightWorldViewProjectionMatrixSpot0", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_Spot", "_ShadowMap"] + }, + { + "name": "LWVPSpot[1]", + "link": "_biasLightWorldViewProjectionMatrixSpot1", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_Spot", "_ShadowMap"] + }, + { + "name": "LWVPSpot[2]", + "link": "_biasLightWorldViewProjectionMatrixSpot2", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_Spot", "_ShadowMap"] + }, + { + "name": "LWVPSpot[3]", + "link": "_biasLightWorldViewProjectionMatrixSpot3", + "ifndef": ["_ShadowMapAtlas"], + "ifdef": ["_Spot", "_ShadowMap"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "volumetric_light.frag.glsl" + } + ] +} diff --git a/Shaders/water_pass/water_pass.frag.glsl b/Shaders/water_pass/water_pass.frag.glsl new file mode 100644 index 0000000000..e23379d783 --- /dev/null +++ b/Shaders/water_pass/water_pass.frag.glsl @@ -0,0 +1,200 @@ +#version 450 + +#include "compiled.inc" +#include "std/gbuffer.glsl" +#include "std/math.glsl" + +uniform sampler2D gbufferD; +uniform sampler2D tex; +uniform sampler2D sbase; +uniform sampler2D sdetail; +uniform sampler2D sfoam; + +#ifdef _SSR +uniform mat4 P; +uniform mat3 V3; +#ifdef _CPostprocess +uniform vec3 PPComp9; +uniform vec3 PPComp10; +#endif +#endif + +#ifdef _Rad +uniform sampler2D senvmapRadiance; +#endif + +uniform float time; +uniform vec3 eye; +uniform vec3 eyeLook; +uniform vec2 cameraProj; +uniform vec3 ld; +uniform float envmapStrength; + +in vec2 texCoord; +in vec3 viewRay; +out vec4 fragColor; + +#ifdef _SSR +vec3 hitCoord; +float gdepth; + +const int numBinarySearchSteps = 7; +#define maxSteps (1.0 / ssrRayStep) + +vec2 getProjectedCoord(const vec3 hit) { + vec4 projectedCoord = P * vec4(hit, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + #ifdef _InvY + projectedCoord.y = 1.0 - projectedCoord.y; + #endif + return projectedCoord.xy; +} + +float getDeltaDepth(const vec3 hit) { + gdepth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(viewRay, gdepth, cameraProj); + return viewPos.z - hit.z; +} + +/* +vec4 binarySearch(vec3 dir) { + float d; + for (int i = 0; i < numBinarySearchSteps; i++) { + dir *= 0.5; + hitCoord -= dir; + d = getDeltaDepth(hitCoord); + if (d < 0.0) hitCoord += dir; + } + // Ugly discard of hits too far away + #ifdef _CPostprocess + if (abs(d) > PPComp9.z) return vec4(0.0); + #else + if (abs(d) > ssrSearchDist) return vec4(0.0); + #endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); +} +*/ + +vec4 rayCast(vec3 dir) { + float d; + #ifdef _CPostprocess + dir *= PPComp9.x; + #else + dir *= ssrRayStep; + #endif + for (int i = 0; i < maxSteps; i++) { + hitCoord += dir; + d = getDeltaDepth(hitCoord); + if(d > 0.0 && d < gdepth) return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); + } + return vec4(0.0); +} +#endif + +void main() { + gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (gdepth == 1.0) { + fragColor = vec4(0.0); + return; + } + // Eye below water + if (eye.z < waterLevel) { + fragColor = vec4(0.0); + return; + } + // Displace surface + vec3 vray = normalize(viewRay); + vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj); + float speed = time * 2.0 * waterSpeed; + p.z += sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed) / 50.0 * waterDisplace; + + // Above water + if (p.z > waterLevel) { + fragColor = vec4(0.0); + return; + } + // Hit plane to determine uvs + vec3 v = normalize(eye - p.xyz); + float t = -(dot(eye, vec3(0.0, 0.0, 1.0)) - waterLevel) / dot(v, vec3(0.0, 0.0, 1.0)); + vec3 hit = eye + t * v; + hit.xy *= waterFreq; + hit.z += waterLevel; + + // Sample normal maps + vec2 tcnor0 = hit.xy / 3.0; + vec3 n0 = textureLod(sdetail, tcnor0 + vec2(speed / 60.0, speed / 120.0), 0.0).rgb; + + vec2 tcnor1 = hit.xy / 6.0 + n0.xy / 20.0; + vec3 n1 = textureLod(sbase, tcnor1 + vec2(speed / 40.0, speed / 80.0), 0.0).rgb; + vec3 n2 = normalize(((n1 + n0) / 2.0) * 2.0 - 1.0); + + float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0; + vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj); + vec2 tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + + // Light + float fresnel = 1.0 - max(dot(n2, v), 0.0); + fresnel = pow(fresnel, 30.0) * 0.45; + vec3 r = reflect(-v, n2); + #ifdef _Rad + vec3 reflectedEnv = textureLod(senvmapRadiance, envMapEquirect(r), 0).rgb; + #else + const vec3 reflectedEnv = vec3(0.5); + #endif + vec3 refracted = textureLod(tex, tc, 0.0).rgb; + + #ifdef _SSR + float roughness = 0.1;//unpackFloat(g0.b).y; + //if (roughness == 1.0) { fragColor.rgb = vec3(0.0); return; } + + float spec = 0.9;//fract(textureLod(gbuffer1, texCoord, 0.0).a); + //if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } + + vec3 viewNormal = n2; + vec3 viewPos = getPosView(viewRay, gdepth, cameraProj); + vec3 reflected = normalize(reflect(viewPos, viewNormal)); + hitCoord = viewPos; + + #ifdef _CPostprocess + vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; + #else + vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; + #endif + + // * max(ssrMinRayStep, -viewPos.z) + vec4 coords = rayCast(dir); + + vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); + float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); + + float reflectivity = 1.0 - roughness; + #ifdef _CPostprocess + float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; + #else + float intensity = pow(reflectivity, ssrFalloffExp)*screenEdgeFactor*clamp(-reflected.z, 0.0, 1.0)*clamp((ssrSearchDist - length(viewPos - hitCoord))*(1.0 / ssrSearchDist), 0.0, 1.0)*coords.w; + #endif + intensity = clamp(intensity, 0.0, 1.0); + vec3 reflCol = textureLod(tex, coords.xy, 0.0).rgb; + fragColor.rgb = mix(refracted, reflCol * intensity * 0.5, waterReflect * fresnel * 0.5); + fragColor.rgb = mix(fragColor.rgb, reflectedEnv, waterReflect * fresnel); + #else + fragColor.rgb = mix(refracted, reflectedEnv, waterReflect * fresnel); + #endif + fragColor.rgb *= waterColor; + fragColor.rgb += clamp(pow(max(dot(r, ld), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0), 0.0, 2.0); + fragColor.rgb *= 1.0 - (clamp(-(p.z - waterLevel) * waterDensity, 0.0, 0.9)); + fragColor.a = clamp(abs(p.z - waterLevel) * 5.0, 0.0, 1.0); + + // Foam + float fd = abs(p.z - waterLevel); + if (fd < 0.1) { + // Based on foam by Owen Deery + // http://fire-face.com/personal/water + vec3 foamMask0 = textureLod(sfoam, tcnor0 * 10, 0.0).rgb; + vec3 foamMask1 = textureLod(sfoam, tcnor1 * 11, 0.0).rgb; + vec3 foam = vec3(1.0) - foamMask0.rrr - foamMask1.bbb; + float fac = 1.0 - (fd * (1.0 / 0.1)); + fragColor.rgb = mix(fragColor.rgb, clamp(foam, 0.0, 1.0), clamp(fac, 0.0, 1.0)); + } +} diff --git a/Shaders/water_pass/water_pass.json b/Shaders/water_pass/water_pass.json new file mode 100644 index 0000000000..7e884e0549 --- /dev/null +++ b/Shaders/water_pass/water_pass.json @@ -0,0 +1,76 @@ +{ + "contexts": [ + { + "name": "water_pass", + "depth_write": false, + "compare_mode": "always", + "cull_mode": "none", + "blend_source": "source_alpha", + "blend_destination": "inverse_source_alpha", + "blend_operation": "add", + "alpha_blend_source": "blend_one", + "alpha_blend_destination": "blend_one", + "alpha_blend_operation": "add", + "links": [ + { + "name": "sbase", + "link": "$water_base.png" + }, + { + "name": "sdetail", + "link": "$water_detail.png" + }, + { + "name": "sfoam", + "link": "$water_foam.png" + }, + { + "name": "eye", + "link": "_cameraPosition" + }, + { + "name": "eyeLook", + "link": "_cameraLook" + }, + { + "name": "ld", + "link": "_lightDirection" + }, + { + "name": "invVP", + "link": "_inverseViewProjectionMatrix" + }, + { + "name": "time", + "link": "_time" + }, + { + "name": "envmapStrength", + "link": "_envmapStrength" + }, + { + "name": "cameraProj", + "link": "_cameraPlaneProj" + }, + { + "name": "senvmapRadiance", + "link": "_envmapRadiance", + "ifdef": ["_Rad"] + }, + { + "name": "P", + "link": "_projectionMatrix", + "ifdef": ["_SSR"] + }, + { + "name": "V3", + "link": "_viewMatrix3", + "ifdef": ["_SSR"] + } + ], + "texture_params": [], + "vertex_shader": "../include/pass_viewray.vert.glsl", + "fragment_shader": "water_pass.frag.glsl" + } + ] +} diff --git a/Sources/armory/data/Config.hx b/Sources/armory/data/Config.hx new file mode 100644 index 0000000000..bd79ab8949 --- /dev/null +++ b/Sources/armory/data/Config.hx @@ -0,0 +1,53 @@ +package armory.data; + +class Config { + + public static var raw: TConfig = null; + public static var configLoaded = false; + + public static function load(done: Void->Void) { + try { + iron.data.Data.getBlob("config.arm", function(blob: kha.Blob) { + configLoaded = true; + raw = haxe.Json.parse(blob.toString()); + done(); + }); + } + catch (e: Dynamic) { done(); } + } + + public static function save() { + var path = iron.data.Data.dataPath + "config.arm"; + var bytes = haxe.io.Bytes.ofString(haxe.Json.stringify(raw)); + #if kha_krom + Krom.fileSaveBytes(path, bytes.getData()); + #elseif kha_kore + sys.io.File.saveBytes(path, bytes); + #end + } + + // public static function reset() {} +} + +typedef TConfig = { + @:optional var debug_console: Null; + @:optional var window_mode: Null; // window, fullscreen + @:optional var window_w: Null; + @:optional var window_h: Null; + @:optional var window_resizable: Null; + @:optional var window_maximizable: Null; + @:optional var window_minimizable: Null; + @:optional var window_vsync: Null; + @:optional var window_msaa: Null; + @:optional var window_scale: Null; + @:optional var rp_supersample: Null; + @:optional var rp_shadowmap_cube: Null; // size + @:optional var rp_shadowmap_cascade: Null; // size for single cascade + @:optional var rp_ssgi: Null; + @:optional var rp_ssr: Null; + @:optional var rp_ssrefr: Null; + @:optional var rp_bloom: Null; + @:optional var rp_motionblur: Null; + @:optional var rp_voxels: Null; // voxelao + @:optional var rp_dynres: Null; // dynamic resolution scaling +} diff --git a/Sources/armory/data/ConstData.hx b/Sources/armory/data/ConstData.hx new file mode 100644 index 0000000000..c84816646c --- /dev/null +++ b/Sources/armory/data/ConstData.hx @@ -0,0 +1,23 @@ +package armory.data; + +import haxe.io.Float32Array; +import kha.graphics4.TextureFormat; + +class ConstData { + + #if arm_ltc + public static var ltcMatTex: kha.Image = null; + public static var ltcMagTex: kha.Image = null; + + public static function initLTC() { + + ltcMatTex = kha.Image.fromBytes(Float32Array.fromArray(ltc_mat).view.buffer, 64, 64, TextureFormat.RGBA128); + ltcMagTex = kha.Image.fromBytes(Float32Array.fromArray(ltc_mag).view.buffer, 64, 64, TextureFormat.A32); + } + + // Real-Time Polygonal-Light Shading with Linearly Transformed Cosines + // https://eheitzresearch.wordpress.com/415-2/ + public static var ltc_mat = [0.000200, - 0.000000, 1.000000, - 0.000000, 0.000504, - 0.000000, 1.000000, - 0.000000, 0.002016, - 0.000000, 1.000000, - 0.000000, 0.004535, - 0.000000, 1.000000, - 0.000000, 0.008063, - 0.000000, 1.000000, - 0.000000, 0.012598, - 0.000000, 1.000000, - 0.000000, 0.018141, - 0.000000, 1.000000, - 0.000000, 0.024692, - 0.000000, 1.000000, - 0.000000, 0.032252, - 0.000000, 1.000000, - 0.000000, 0.040821, - 0.000000, 1.000000, - 0.000000, 0.050400, - 0.000000, 1.000000, - 0.000000, 0.060989, - 0.000000, 1.000000, - 0.000000, 0.072591, - 0.000000, 1.000000, - 0.000000, 0.085206, - 0.000000, 1.000000, - 0.000000, 0.098836, - 0.000000, 1.000000, - 0.000000, 0.113483, - 0.000000, 1.000000, - 0.000000, 0.129147, - 0.000000, 1.000000, - 0.000000, 0.145828, - 0.000000, 1.000000, - 0.000000, 0.163499, - 0.000000, 1.000000, - 0.000000, 0.181972, - 0.000000, 1.000000, - 0.000000, 0.199498, - 0.000000, 1.000000, - 0.000000, 0.220031, - 0.000000, 1.000000, - 0.000000, 0.241588, - 0.000000, 1.000000, - 0.000000, 0.264120, - 0.000000, 1.000000, - 0.000000, 0.287521, - 0.000000, 1.000000, - 0.000000, 0.311478, - 0.000000, 1.000000, - 0.000000, 0.335127, - 0.000000, 1.000000, - 0.000000, 0.359811, - 0.000000, 1.000000, - 0.000000, 0.386446, - 0.000000, 1.000000, - 0.000000, 0.413161, - 0.000000, 1.000000, - 0.000000, 0.439142, - 0.000000, 1.000000, - 0.000000, 0.467039, - 0.000000, 1.000000, - 0.000000, 0.495170, - 0.000000, 1.000000, - 0.000000, 0.522324, - 0.000000, 1.000000, - 0.000000, 0.551482, - 0.000000, 1.000000, - 0.000000, 0.579621, - 0.000000, 1.000000, - 0.000000, 0.608255, - 0.000000, 1.000000, - 0.000000, 0.636515, - 0.000000, 1.000000, - 0.000000, 0.664835, - 0.000000, 1.000000, - 0.000000, 0.692549, - 0.000000, 1.000000, - 0.000000, 0.720375, - 0.000000, 1.000000, - 0.000000, 0.747238, - 0.000000, 1.000000, - 0.000000, 0.773956, - 0.000000, 1.000000, - 0.000000, 0.799879, - 0.000000, 1.000000, - 0.000000, 0.824889, - 0.000000, 1.000000, - 0.000000, 0.849357, - 0.000000, 1.000000, - 0.000000, 0.873016, - 0.000000, 1.000000, - 0.000000, 0.895670, - 0.000000, 1.000000, - 0.000000, 0.917194, - 0.000000, 1.000000, - 0.000000, 0.937978, - 0.000000, 1.000000, - 0.000000, 0.957872, - 0.000000, 1.000000, - 0.000000, 0.976736, - 0.000000, 1.000000, - 0.000000, 0.994433, - 0.000000, 1.000000, - 0.000000, 1.011206, - 0.000000, 1.000000, - 0.000000, 1.026820, - 0.000000, 1.000000, - 0.000000, 1.041720, - 0.000000, 1.000000, - 0.000000, 1.055657, - 0.000000, 1.000000, - 0.000000, 1.068642, - 0.000000, 1.000000, - 0.000000, 1.080646, - 0.000000, 1.000000, - 0.000000, 1.091637, - 0.000000, 1.000000, - 0.000000, 1.101837, - 0.000000, 1.000000, - 0.000000, 1.111292, - 0.000000, 1.000000, - 0.000000, 1.120025, - 0.000000, 1.000000, - 0.000000, 1.127918, - 0.000000, 1.000000, - 0.000000, 0.000200, - 0.000005, 1.000623, 0.024938, 0.000504, - 0.000013, 1.000643, 0.024938, 0.002016, - 0.000050, 1.000618, 0.024938, 0.004535, - 0.000113, 1.000621, 0.024938, 0.008063, - 0.000201, 1.000746, 0.024938, 0.012596, - 0.000314, 1.000463, 0.024937, 0.018140, - 0.000452, 1.000511, 0.024939, 0.024693, - 0.000616, 1.000541, 0.024938, 0.032253, - 0.000804, 1.000684, 0.024938, 0.040815, - 0.001018, 1.000524, 0.024940, 0.050399, - 0.001257, 1.000582, 0.024937, 0.060989, - 0.001521, 1.000655, 0.024937, 0.072591, - 0.001810, 1.000608, 0.024938, 0.085204, - 0.002125, 1.000622, 0.024939, 0.098835, - 0.002465, 1.000632, 0.024937, 0.113483, - 0.002830, 1.000640, 0.024939, 0.129143, - 0.003220, 1.000568, 0.024938, 0.145830, - 0.003633, 1.000635, 0.024938, 0.163497, - 0.004062, 1.000626, 0.024938, 0.181956, - 0.004424, 1.000612, 0.024924, 0.199791, - 0.004593, 1.000627, 0.024890, 0.220029, - 0.005480, 1.000594, 0.024935, 0.241586, - 0.006010, 1.000616, 0.024933, 0.264115, - 0.006550, 1.000607, 0.024927, 0.287514, - 0.007072, 1.000595, 0.024909, 0.311455, - 0.007472, 1.000616, 0.024872, 0.335083, - 0.007491, 1.000589, 0.024755, 0.359805, - 0.008810, 1.000601, 0.024877, 0.386438, - 0.009282, 1.000640, 0.024824, 0.413131, - 0.009534, 1.000599, 0.024708, 0.439249, - 0.009701, 1.000497, 0.024573, 0.466997, - 0.010878, 1.000467, 0.024652, 0.495138, - 0.010959, 1.000539, 0.024455, 0.522654, - 0.011386, 1.000518, 0.024318, 0.551415, - 0.012022, 1.000533, 0.024216, 0.579610, - 0.011805, 1.000495, 0.023867, 0.608185, - 0.012773, 1.000474, 0.023834, 0.636492, - 0.012377, 1.000488, 0.023327, 0.664826, - 0.013172, 1.000576, 0.023205, 0.692674, - 0.012847, 1.000505, 0.022708, 0.720341, - 0.013141, 1.000424, 0.022349, 0.747373, - 0.013227, 1.000449, 0.021871, 0.773980, - 0.012739, 1.000478, 0.021171, 0.799839, - 0.012999, 1.000396, 0.020606, 0.825113, - 0.012727, 1.000425, 0.020006, 0.849579, - 0.012170, 1.000469, 0.019089, 0.873046, - 0.011855, 1.000411, 0.018291, 0.895777, - 0.011711, 1.000426, 0.017534, 0.917518, - 0.011107, 1.000373, 0.016542, 0.938264, - 0.010439, 1.000322, 0.015512, 0.958032, - 0.009807, 1.000324, 0.014491, 0.976838, - 0.009268, 1.000341, 0.013468, 0.994631, - 0.008662, 1.000318, 0.012376, 1.011434, - 0.007923, 1.000289, 0.011187, 1.027169, - 0.007132, 1.000216, 0.010078, 1.041929, - 0.006332, 1.000096, 0.008924, 1.055767, - 0.005554, 1.000156, 0.007770, 1.068595, - 0.004811, 1.000084, 0.006611, 1.080612, - 0.003950, 1.000047, 0.005485, 1.091785, - 0.003174, 1.000109, 0.004352, 1.101998, - 0.002363, 1.000029, 0.003180, 1.111423, - 0.001552, 0.999985, 0.002091, 1.120007, - 0.000786, 0.999947, 0.000991, 1.127918, 0.000004, 1.000000, - 0.000004, 0.000200, - 0.000010, 1.002495, 0.049907, 0.000504, - 0.000025, 1.002476, 0.049908, 0.002016, - 0.000101, 1.002500, 0.049908, 0.004535, - 0.000226, 1.002487, 0.049908, 0.008062, - 0.000402, 1.002364, 0.049908, 0.012598, - 0.000629, 1.002412, 0.049908, 0.018140, - 0.000905, 1.002379, 0.049908, 0.024691, - 0.001232, 1.002490, 0.049907, 0.032251, - 0.001610, 1.002398, 0.049908, 0.040821, - 0.002037, 1.002392, 0.049908, 0.050398, - 0.002515, 1.002431, 0.049907, 0.060989, - 0.003044, 1.002475, 0.049908, 0.072592, - 0.003623, 1.002546, 0.049907, 0.085204, - 0.004252, 1.002467, 0.049907, 0.098832, - 0.004932, 1.002450, 0.049908, 0.113481, - 0.005663, 1.002482, 0.049907, 0.129145, - 0.006443, 1.002443, 0.049907, 0.145825, - 0.007271, 1.002495, 0.049906, 0.163491, - 0.008128, 1.002475, 0.049903, 0.181911, - 0.008826, 1.002459, 0.049879, 0.200065, - 0.009285, 1.002443, 0.049824, 0.220025, - 0.010966, 1.002450, 0.049897, 0.241581, - 0.012025, 1.002463, 0.049893, 0.264099, - 0.013105, 1.002395, 0.049881, 0.287493, - 0.014145, 1.002390, 0.049855, 0.311399, - 0.014925, 1.002414, 0.049769, 0.335096, - 0.015239, 1.002363, 0.049591, 0.359815, - 0.017559, 1.002415, 0.049777, 0.386365, - 0.018554, 1.002354, 0.049675, 0.413017, - 0.019043, 1.002297, 0.049444, 0.439519, - 0.019815, 1.002284, 0.049253, 0.466938, - 0.021741, 1.002307, 0.049327, 0.494999, - 0.021887, 1.002181, 0.048922, 0.522922, - 0.022844, 1.002107, 0.048677, 0.551270, - 0.024014, 1.002101, 0.048478, 0.579771, - 0.024156, 1.002060, 0.047904, 0.608156, - 0.025317, 1.002077, 0.047594, 0.636662, - 0.025321, 1.001975, 0.046876, 0.664846, - 0.026018, 1.001992, 0.046354, 0.692877, - 0.026041, 1.001846, 0.045504, 0.720316, - 0.026252, 1.001846, 0.044655, 0.747658, - 0.026159, 1.001931, 0.043670, 0.774252, - 0.026086, 1.001845, 0.042515, 0.800179, - 0.025653, 1.001794, 0.041211, 0.825525, - 0.025170, 1.001787, 0.039823, 0.850013, - 0.024788, 1.001806, 0.038409, 0.873593, - 0.023992, 1.001688, 0.036767, 0.896343, - 0.022985, 1.001666, 0.034900, 0.918062, - 0.022005, 1.001548, 0.033010, 0.938928, - 0.021110, 1.001503, 0.031143, 0.958667, - 0.019893, 1.001341, 0.029059, 0.977457, - 0.018546, 1.001194, 0.026888, 0.995243, - 0.017152, 1.001095, 0.024713, 1.012023, - 0.015750, 1.001100, 0.022496, 1.027614, - 0.014289, 1.000851, 0.020153, 1.042389, - 0.012688, 1.000724, 0.017839, 1.056161, - 0.011118, 1.000572, 0.015529, 1.068968, - 0.009540, 1.000407, 0.013240, 1.080866, - 0.007963, 1.000258, 0.010940, 1.091944, - 0.006416, 1.000254, 0.008716, 1.102104, - 0.004771, 1.000175, 0.006434, 1.111571, - 0.003056, 1.000148, 0.004169, 1.120084, - 0.001458, 1.000050, 0.002033, 1.127981, 0.000021, 0.999987, - 0.000027, 0.000200, - 0.000015, 1.005620, 0.074940, 0.000504, - 0.000038, 1.005650, 0.074939, 0.002016, - 0.000151, 1.005613, 0.074939, 0.004535, - 0.000340, 1.005618, 0.074939, 0.008062, - 0.000604, 1.005614, 0.074939, 0.012597, - 0.000944, 1.005616, 0.074940, 0.018141, - 0.001359, 1.005558, 0.074939, 0.024695, - 0.001851, 1.005495, 0.074940, 0.032253, - 0.002417, 1.005616, 0.074939, 0.040822, - 0.003059, 1.005591, 0.074940, 0.050399, - 0.003777, 1.005596, 0.074940, 0.060989, - 0.004570, 1.005599, 0.074939, 0.072591, - 0.005440, 1.005616, 0.074940, 0.085203, - 0.006385, 1.005616, 0.074939, 0.098833, - 0.007406, 1.005595, 0.074938, 0.113481, - 0.008502, 1.005605, 0.074938, 0.129147, - 0.009674, 1.005605, 0.074937, 0.145817, - 0.010916, 1.005513, 0.074937, 0.163485, - 0.012199, 1.005579, 0.074928, 0.181824, - 0.013172, 1.005552, 0.074885, 0.200274, - 0.014100, 1.005524, 0.074825, 0.220017, - 0.016464, 1.005529, 0.074928, 0.241568, - 0.018052, 1.005490, 0.074914, 0.264084, - 0.019671, 1.005457, 0.074898, 0.287450, - 0.021217, 1.005431, 0.074860, 0.311281, - 0.022341, 1.005395, 0.074717, 0.335228, - 0.023296, 1.005320, 0.074526, 0.360047, - 0.025965, 1.005302, 0.074649, 0.386273, - 0.027808, 1.005285, 0.074575, 0.412855, - 0.028504, 1.005167, 0.074237, 0.439705, - 0.030007, 1.005129, 0.074013, 0.466975, - 0.032263, 1.005082, 0.073967, 0.494874, - 0.032931, 1.004960, 0.073475, 0.523066, - 0.034348, 1.004834, 0.073084, 0.551198, - 0.035739, 1.004806, 0.072657, 0.579889, - 0.036575, 1.004687, 0.072029, 0.608282, - 0.037434, 1.004605, 0.071309, 0.636812, - 0.038323, 1.004589, 0.070507, 0.665010, - 0.038676, 1.004403, 0.069424, 0.693063, - 0.039237, 1.004340, 0.068370, 0.720750, - 0.039332, 1.004224, 0.066988, 0.747911, - 0.039179, 1.004117, 0.065447, 0.774576, - 0.039110, 1.004035, 0.063838, 0.800737, - 0.038542, 1.004027, 0.061923, 0.825966, - 0.037966, 1.003825, 0.059859, 0.850534, - 0.036943, 1.003786, 0.057529, 0.874289, - 0.035853, 1.003560, 0.055081, 0.897152, - 0.034730, 1.003549, 0.052476, 0.919029, - 0.033242, 1.003454, 0.049647, 0.939851, - 0.031508, 1.003215, 0.046670, 0.959599, - 0.029695, 1.002916, 0.043588, 0.978293, - 0.027845, 1.002720, 0.040401, 0.996085, - 0.025775, 1.002445, 0.037060, 1.012768, - 0.023607, 1.002133, 0.033726, 1.028404, - 0.021374, 1.001822, 0.030217, 1.043150, - 0.019108, 1.001602, 0.026820, 1.056760, - 0.016823, 1.001274, 0.023372, 1.069471, - 0.014378, 1.000964, 0.019891, 1.081283, - 0.011884, 1.000684, 0.016405, 1.092238, - 0.009398, 1.000514, 0.012950, 1.102384, - 0.007030, 1.000319, 0.009579, 1.111737, - 0.004751, 1.000225, 0.006384, 1.120274, - 0.002404, 1.000046, 0.003192, 1.128182, 0.000031, 1.000020, 0.000033, 0.000200, - 0.000020, 1.010006, 0.100065, 0.000504, - 0.000050, 1.009927, 0.100065, 0.002016, - 0.000202, 1.010026, 0.100064, 0.004535, - 0.000454, 1.010018, 0.100065, 0.008062, - 0.000807, 1.009891, 0.100064, 0.012599, - 0.001261, 1.010175, 0.100064, 0.018141, - 0.001815, 1.010067, 0.100065, 0.024692, - 0.002471, 1.010014, 0.100066, 0.032251, - 0.003227, 1.009950, 0.100065, 0.040818, - 0.004084, 1.009963, 0.100067, 0.050401, - 0.005043, 1.010032, 0.100064, 0.060988, - 0.006102, 1.009979, 0.100064, 0.072588, - 0.007263, 1.009984, 0.100063, 0.085205, - 0.008525, 1.010023, 0.100063, 0.098832, - 0.009888, 1.009960, 0.100062, 0.113479, - 0.011352, 1.009974, 0.100063, 0.129142, - 0.012916, 1.009945, 0.100062, 0.145817, - 0.014573, 1.009924, 0.100058, 0.163468, - 0.016276, 1.009912, 0.100050, 0.181674, - 0.017411, 1.009859, 0.099975, 0.200435, - 0.019002, 1.009842, 0.099932, 0.220005, - 0.021978, 1.009820, 0.100043, 0.241550, - 0.024096, 1.009778, 0.100031, 0.264058, - 0.026250, 1.009765, 0.100002, 0.287399, - 0.028286, 1.009724, 0.099939, 0.311134, - 0.029698, 1.009596, 0.099748, 0.335350, - 0.031442, 1.009508, 0.099582, 0.360295, - 0.034401, 1.009475, 0.099613, 0.386112, - 0.037030, 1.009329, 0.099558, 0.412733, - 0.038163, 1.009250, 0.099137, 0.439833, - 0.040250, 1.009125, 0.098866, 0.467099, - 0.042583, 1.009011, 0.098626, 0.494828, - 0.044299, 1.008803, 0.098149, 0.523217, - 0.045876, 1.008712, 0.097600, 0.551338, - 0.047440, 1.008509, 0.096929, 0.579917, - 0.048995, 1.008371, 0.096178, 0.608454, - 0.049901, 1.008212, 0.095145, 0.636785, - 0.051224, 1.007963, 0.094151, 0.665220, - 0.051675, 1.007741, 0.092728, 0.693194, - 0.052278, 1.007616, 0.091195, 0.721008, - 0.052406, 1.007327, 0.089384, 0.748196, - 0.052529, 1.007219, 0.087461, 0.774975, - 0.051950, 1.006851, 0.085133, 0.801129, - 0.051456, 1.006732, 0.082628, 0.826668, - 0.050569, 1.006612, 0.079817, 0.851291, - 0.049328, 1.006374, 0.076710, 0.875056, - 0.047988, 1.006183, 0.073481, 0.897872, - 0.046149, 1.005742, 0.069943, 0.919803, - 0.044144, 1.005514, 0.066151, 0.940701, - 0.042095, 1.005153, 0.062247, 0.960580, - 0.039730, 1.004843, 0.058158, 0.979427, - 0.037104, 1.004535, 0.053850, 0.997157, - 0.034369, 1.004023, 0.049403, 1.013777, - 0.031555, 1.003622, 0.044944, 1.029452, - 0.028571, 1.003212, 0.040414, 1.044029, - 0.025416, 1.002698, 0.035723, 1.057586, - 0.022217, 1.002202, 0.031072, 1.070148, - 0.019037, 1.001703, 0.026429, 1.081875, - 0.015936, 1.001322, 0.021896, 1.092789, - 0.012734, 1.001053, 0.017288, 1.102704, - 0.009454, 1.000604, 0.012841, 1.112011, - 0.006199, 1.000387, 0.008446, 1.120590, - 0.003010, 1.000166, 0.004122, 1.128283, 0.000027, 0.999956, - 0.000038, 0.000200, - 0.000025, 1.015664, 0.125315, 0.000504, - 0.000063, 1.015664, 0.125316, 0.002016, - 0.000253, 1.015727, 0.125315, 0.004535, - 0.000568, 1.015695, 0.125314, 0.008063, - 0.001010, 1.015823, 0.125316, 0.012599, - 0.001579, 1.015867, 0.125315, 0.018141, - 0.002273, 1.015758, 0.125316, 0.024691, - 0.003094, 1.015662, 0.125316, 0.032252, - 0.004042, 1.015674, 0.125316, 0.040820, - 0.005115, 1.015678, 0.125316, 0.050400, - 0.006316, 1.015684, 0.125315, 0.060989, - 0.007642, 1.015685, 0.125315, 0.072590, - 0.009096, 1.015703, 0.125314, 0.085203, - 0.010676, 1.015654, 0.125314, 0.098833, - 0.012383, 1.015670, 0.125315, 0.113477, - 0.014215, 1.015635, 0.125312, 0.129138, - 0.016173, 1.015599, 0.125311, 0.145815, - 0.018246, 1.015610, 0.125306, 0.163450, - 0.020360, 1.015564, 0.125294, 0.181595, - 0.021807, 1.015460, 0.125204, 0.200563, - 0.023971, 1.015440, 0.125165, 0.220186, - 0.027280, 1.015412, 0.125250, 0.241528, - 0.030164, 1.015342, 0.125267, 0.264020, - 0.032847, 1.015269, 0.125233, 0.287311, - 0.035345, 1.015232, 0.125138, 0.310993, - 0.037108, 1.015063, 0.124903, 0.335467, - 0.039653, 1.014970, 0.124749, 0.360497, - 0.042914, 1.014819, 0.124702, 0.385986, - 0.046142, 1.014685, 0.124623, 0.412703, - 0.048050, 1.014543, 0.124193, 0.439929, - 0.050527, 1.014315, 0.123833, 0.467163, - 0.052880, 1.014087, 0.123375, 0.494824, - 0.055672, 1.013898, 0.122982, 0.523222, - 0.057388, 1.013647, 0.122166, 0.551557, - 0.059328, 1.013403, 0.121343, 0.579884, - 0.061315, 1.013059, 0.120430, 0.608619, - 0.062531, 1.012745, 0.119140, 0.637014, - 0.063778, 1.012425, 0.117721, 0.665425, - 0.064734, 1.012067, 0.116069, 0.693580, - 0.065315, 1.011712, 0.114146, 0.721194, - 0.065535, 1.011200, 0.111846, 0.748586, - 0.065501, 1.010896, 0.109309, 0.775437, - 0.065091, 1.010576, 0.106504, 0.801554, - 0.064332, 1.010136, 0.103308, 0.827079, - 0.063078, 1.009629, 0.099695, 0.851693, - 0.061728, 1.009233, 0.095946, 0.875586, - 0.059853, 1.008726, 0.091802, 0.898589, - 0.057727, 1.008412, 0.087339, 0.920421, - 0.055377, 1.007767, 0.082687, 0.941533, - 0.052571, 1.007529, 0.077716, 0.961426, - 0.049544, 1.006929, 0.072574, 0.980287, - 0.046400, 1.006393, 0.067217, 0.998080, - 0.042966, 1.005872, 0.061757, 1.014940, - 0.039321, 1.005346, 0.056072, 1.030455, - 0.035585, 1.004609, 0.050410, 1.045078, - 0.031823, 1.004151, 0.044622, 1.058555, - 0.027947, 1.003421, 0.038893, 1.071009, - 0.023891, 1.002704, 0.032977, 1.082594, - 0.019822, 1.002023, 0.027290, 1.093265, - 0.015765, 1.001403, 0.021543, 1.103132, - 0.011790, 1.000944, 0.016072, 1.112348, - 0.007784, 1.000550, 0.010511, 1.120845, - 0.003849, 1.000224, 0.005174, 1.128573, 0.000057, 0.999975, - 0.000039, 0.000200, - 0.000030, 1.022609, 0.150725, 0.000504, - 0.000076, 1.022728, 0.150725, 0.002016, - 0.000304, 1.022728, 0.150725, 0.004535, - 0.000684, 1.022733, 0.150725, 0.008062, - 0.001215, 1.022715, 0.150725, 0.012598, - 0.001899, 1.022720, 0.150725, 0.018141, - 0.002734, 1.022659, 0.150725, 0.024694, - 0.003722, 1.022801, 0.150724, 0.032254, - 0.004861, 1.022779, 0.150726, 0.040815, - 0.006152, 1.022693, 0.150724, 0.050400, - 0.007596, 1.022716, 0.150725, 0.060990, - 0.009192, 1.022733, 0.150725, 0.072587, - 0.010939, 1.022630, 0.150723, 0.085203, - 0.012839, 1.022676, 0.150725, 0.098828, - 0.014891, 1.022659, 0.150725, 0.113473, - 0.017095, 1.022589, 0.150720, 0.129137, - 0.019449, 1.022572, 0.150716, 0.145803, - 0.021938, 1.022508, 0.150712, 0.163417, - 0.024443, 1.022471, 0.150691, 0.181580, - 0.026329, 1.022406, 0.150600, 0.200667, - 0.028997, 1.022336, 0.150553, 0.220429, - 0.032584, 1.022296, 0.150610, 0.241497, - 0.036260, 1.022202, 0.150658, 0.263975, - 0.039465, 1.022119, 0.150619, 0.287210, - 0.042385, 1.021988, 0.150490, 0.310935, - 0.044758, 1.021771, 0.150241, 0.335556, - 0.047922, 1.021658, 0.150076, 0.360667, - 0.051493, 1.021437, 0.149931, 0.386028, - 0.054931, 1.021228, 0.149754, 0.412665, - 0.058007, 1.021023, 0.149400, 0.439951, - 0.060813, 1.020723, 0.148913, 0.467262, - 0.063461, 1.020332, 0.148319, 0.494972, - 0.066738, 1.020097, 0.147798, 0.523153, - 0.068976, 1.019630, 0.146903, 0.551700, - 0.071268, 1.019245, 0.145863, 0.580046, - 0.073439, 1.018797, 0.144695, 0.608649, - 0.075193, 1.018201, 0.143237, 0.637239, - 0.076536, 1.017746, 0.141463, 0.665388, - 0.077771, 1.017111, 0.139462, 0.693755, - 0.078344, 1.016609, 0.137082, 0.721345, - 0.078817, 1.015863, 0.134403, 0.748879, - 0.078512, 1.015390, 0.131252, 0.775560, - 0.078128, 1.014652, 0.127866, 0.801897, - 0.077094, 1.013877, 0.123928, 0.827193, - 0.075863, 1.013021, 0.119733, 0.851990, - 0.073973, 1.012395, 0.115055, 0.875823, - 0.071765, 1.011595, 0.110098, 0.898655, - 0.069241, 1.010862, 0.104722, 0.920915, - 0.066232, 1.010185, 0.098991, 0.941969, - 0.062980, 1.009588, 0.093044, 0.961882, - 0.059507, 1.008777, 0.086925, 0.980952, - 0.055606, 1.008252, 0.080520, 0.998955, - 0.051503, 1.007633, 0.073890, 1.015756, - 0.047292, 1.006908, 0.067302, 1.031571, - 0.042804, 1.006338, 0.060412, 1.046095, - 0.038132, 1.005512, 0.053497, 1.059542, - 0.033380, 1.004592, 0.046569, 1.072006, - 0.028613, 1.003731, 0.039679, 1.083348, - 0.023811, 1.002871, 0.032772, 1.093969, - 0.018930, 1.002068, 0.025894, 1.103697, - 0.014098, 1.001284, 0.019178, 1.112813, - 0.009339, 1.000820, 0.012652, 1.121193, - 0.004661, 1.000324, 0.006226, 1.128930, 0.000052, 0.999988, - 0.000008, 0.000200, - 0.000035, 1.030857, 0.176327, 0.000504, - 0.000089, 1.031137, 0.176326, 0.002016, - 0.000355, 1.031049, 0.176325, 0.004535, - 0.000800, 1.031105, 0.176326, 0.008062, - 0.001422, 1.030973, 0.176326, 0.012598, - 0.002221, 1.031168, 0.176326, 0.018141, - 0.003199, 1.031093, 0.176326, 0.024695, - 0.004354, 1.031297, 0.176326, 0.032253, - 0.005687, 1.031091, 0.176327, 0.040821, - 0.007197, 1.031012, 0.176326, 0.050399, - 0.008886, 1.031068, 0.176325, 0.060987, - 0.010752, 1.030967, 0.176323, 0.072588, - 0.012797, 1.031028, 0.176324, 0.085200, - 0.015019, 1.030985, 0.176322, 0.098829, - 0.017419, 1.030983, 0.176320, 0.113474, - 0.019997, 1.030953, 0.176317, 0.129133, - 0.022748, 1.030891, 0.176312, 0.145800, - 0.025655, 1.030825, 0.176306, 0.163372, - 0.028510, 1.030781, 0.176279, 0.181578, - 0.030914, 1.030683, 0.176187, 0.200761, - 0.034076, 1.030574, 0.176139, 0.220645, - 0.037985, 1.030476, 0.176160, 0.241473, - 0.042391, 1.030384, 0.176238, 0.263922, - 0.046105, 1.030241, 0.176175, 0.287074, - 0.049390, 1.030049, 0.176013, 0.310915, - 0.052511, 1.029839, 0.175776, 0.335604, - 0.056236, 1.029608, 0.175578, 0.360775, - 0.060118, 1.029355, 0.175359, 0.386196, - 0.063907, 1.029052, 0.175083, 0.412599, - 0.067997, 1.028766, 0.174791, 0.439916, - 0.071088, 1.028326, 0.174174, 0.467444, - 0.074247, 1.027890, 0.173487, 0.495132, - 0.077728, 1.027374, 0.172774, 0.523117, - 0.080822, 1.026763, 0.171824, 0.551783, - 0.083228, 1.026205, 0.170554, 0.580234, - 0.085682, 1.025614, 0.169090, 0.608568, - 0.087860, 1.024668, 0.167468, 0.637357, - 0.089346, 1.023939, 0.165283, 0.665507, - 0.090704, 1.022946, 0.162966, 0.693704, - 0.091388, 1.022010, 0.160131, 0.721396, - 0.091783, 1.021085, 0.156957, 0.748676, - 0.091688, 1.019894, 0.153292, 0.775370, - 0.090992, 1.018608, 0.149158, 0.801547, - 0.089881, 1.017646, 0.144551, 0.827013, - 0.088267, 1.016355, 0.139614, 0.851708, - 0.086132, 1.015446, 0.134026, 0.875652, - 0.083707, 1.014321, 0.128101, 0.898703, - 0.080619, 1.013454, 0.121841, 0.920904, - 0.077280, 1.012634, 0.115379, 0.942077, - 0.073484, 1.011770, 0.108355, 0.962245, - 0.069252, 1.010894, 0.101153, 0.981385, - 0.064807, 1.010114, 0.093666, 0.999379, - 0.060080, 1.009294, 0.086007, 1.016494, - 0.055007, 1.008591, 0.078194, 1.032357, - 0.049760, 1.007821, 0.070328, 1.047061, - 0.044468, 1.006871, 0.062358, 1.060675, - 0.038960, 1.006062, 0.054279, 1.073032, - 0.033343, 1.004911, 0.046158, 1.084293, - 0.027699, 1.003791, 0.038111, 1.094724, - 0.022130, 1.002744, 0.030239, 1.104302, - 0.016508, 1.001815, 0.022397, 1.113290, - 0.010846, 1.001083, 0.014747, 1.121649, - 0.005294, 1.000490, 0.007234, 1.129230, 0.000071, 0.999975, - 0.000053, 0.000200, - 0.000040, 1.040431, 0.202155, 0.000504, - 0.000102, 1.040912, 0.202154, 0.002016, - 0.000407, 1.041328, 0.202152, 0.004535, - 0.000917, 1.040877, 0.202154, 0.008063, - 0.001630, 1.040867, 0.202153, 0.012598, - 0.002547, 1.040870, 0.202153, 0.018140, - 0.003667, 1.040808, 0.202153, 0.024692, - 0.004991, 1.040861, 0.202153, 0.032252, - 0.006519, 1.040861, 0.202153, 0.040822, - 0.008252, 1.040864, 0.202153, 0.050397, - 0.010187, 1.040717, 0.202151, 0.060988, - 0.012327, 1.040791, 0.202152, 0.072582, - 0.014669, 1.040640, 0.202149, 0.085198, - 0.017217, 1.040716, 0.202147, 0.098827, - 0.019968, 1.040748, 0.202141, 0.113467, - 0.022921, 1.040632, 0.202142, 0.129129, - 0.026074, 1.040606, 0.202137, 0.145793, - 0.029399, 1.040566, 0.202127, 0.163294, - 0.032524, 1.040459, 0.202078, 0.181589, - 0.035552, 1.040315, 0.201996, 0.200844, - 0.039208, 1.040221, 0.201948, 0.220835, - 0.043489, 1.040047, 0.201945, 0.241471, - 0.048523, 1.039921, 0.202031, 0.263854, - 0.052764, 1.039756, 0.201957, 0.286935, - 0.056387, 1.039497, 0.201743, 0.310902, - 0.060338, 1.039252, 0.201531, 0.335642, - 0.064594, 1.038954, 0.201286, 0.360859, - 0.068772, 1.038582, 0.200983, 0.386419, - 0.073086, 1.038160, 0.200651, 0.412588, - 0.077887, 1.037724, 0.200343, 0.439836, - 0.081391, 1.037182, 0.199618, 0.467538, - 0.085121, 1.036602, 0.198839, 0.495286, - 0.088718, 1.035893, 0.197895, 0.523231, - 0.092514, 1.035121, 0.196887, 0.551730, - 0.095238, 1.034127, 0.195390, 0.580302, - 0.097949, 1.033131, 0.193668, 0.608559, - 0.100418, 1.031962, 0.191773, 0.637224, - 0.102129, 1.030838, 0.189319, 0.665597, - 0.103578, 1.029511, 0.186529, 0.693535, - 0.104652, 1.028263, 0.183303, 0.721325, - 0.104766, 1.026611, 0.179497, 0.748384, - 0.104717, 1.025128, 0.175283, 0.775058, - 0.103846, 1.023385, 0.170493, 0.801387, - 0.102728, 1.022236, 0.165187, 0.826412, - 0.100679, 1.019908, 0.159362, 0.851314, - 0.098451, 1.018839, 0.153059, 0.875100, - 0.095363, 1.017306, 0.146284, 0.898280, - 0.092008, 1.016151, 0.138975, 0.920450, - 0.088095, 1.014880, 0.131361, 0.941727, - 0.083690, 1.013556, 0.123417, 0.962308, - 0.079077, 1.012998, 0.115201, 0.981364, - 0.073894, 1.011841, 0.106711, 0.999798, - 0.068435, 1.011021, 0.098063, 1.016983, - 0.062830, 1.010194, 0.089183, 1.033039, - 0.056914, 1.009292, 0.080190, 1.047994, - 0.050721, 1.008474, 0.071010, 1.061580, - 0.044454, 1.007386, 0.061867, 1.074023, - 0.038145, 1.006135, 0.052711, 1.085470, - 0.031679, 1.004890, 0.043595, 1.095673, - 0.025157, 1.003627, 0.034506, 1.105000, - 0.018702, 1.002331, 0.025468, 1.113795, - 0.012458, 1.001278, 0.016834, 1.122012, - 0.006169, 1.000548, 0.008265, 1.129683, 0.000078, 0.999988, - 0.000072, 0.000200, - 0.000046, 1.052496, 0.228243, 0.000504, - 0.000115, 1.052079, 0.228243, 0.002016, - 0.000460, 1.052079, 0.228241, 0.004535, - 0.001035, 1.052091, 0.228242, 0.008062, - 0.001840, 1.051962, 0.228242, 0.012598, - 0.002875, 1.052087, 0.228242, 0.018141, - 0.004140, 1.052088, 0.228242, 0.024692, - 0.005636, 1.052096, 0.228239, 0.032251, - 0.007361, 1.052029, 0.228243, 0.040820, - 0.009316, 1.052038, 0.228241, 0.050399, - 0.011501, 1.052042, 0.228239, 0.060990, - 0.013917, 1.052046, 0.228238, 0.072586, - 0.016562, 1.051990, 0.228236, 0.085198, - 0.019437, 1.051946, 0.228234, 0.098824, - 0.022542, 1.051879, 0.228229, 0.113467, - 0.025875, 1.051841, 0.228227, 0.129121, - 0.029430, 1.051724, 0.228219, 0.145780, - 0.033170, 1.051672, 0.228205, 0.163222, - 0.036567, 1.051556, 0.228143, 0.181604, - 0.040245, 1.051382, 0.228069, 0.200913, - 0.044395, 1.051230, 0.228010, 0.221005, - 0.049088, 1.051062, 0.227988, 0.241667, - 0.054506, 1.050881, 0.228044, 0.263777, - 0.059437, 1.050643, 0.227986, 0.286841, - 0.063590, 1.050312, 0.227755, 0.310879, - 0.068224, 1.050009, 0.227525, 0.335650, - 0.072986, 1.049597, 0.227253, 0.360869, - 0.077435, 1.049121, 0.226845, 0.386609, - 0.082385, 1.048587, 0.226466, 0.412742, - 0.087570, 1.047987, 0.226059, 0.439789, - 0.091929, 1.047308, 0.225331, 0.467558, - 0.096038, 1.046423, 0.224409, 0.495406, - 0.099938, 1.045481, 0.223288, 0.523417, - 0.104050, 1.044512, 0.222066, 0.551755, - 0.107503, 1.043408, 0.220487, 0.580468, - 0.110234, 1.042016, 0.218451, 0.608904, - 0.112993, 1.040535, 0.216200, 0.637230, - 0.115173, 1.038934, 0.213458, 0.665566, - 0.116433, 1.036961, 0.210158, 0.693413, - 0.117589, 1.035130, 0.206457, 0.721025, - 0.117885, 1.033080, 0.202197, 0.748054, - 0.117606, 1.030752, 0.197296, 0.774631, - 0.116771, 1.028608, 0.191813, 0.800677, - 0.115194, 1.026350, 0.185691, 0.826062, - 0.113138, 1.024472, 0.179053, 0.850590, - 0.110359, 1.022174, 0.171839, 0.874550, - 0.107072, 1.020381, 0.164067, 0.897567, - 0.103268, 1.018777, 0.155959, 0.919609, - 0.098794, 1.016886, 0.147320, 0.941177, - 0.094067, 1.015880, 0.138365, 0.961752, - 0.088670, 1.014616, 0.129051, 0.981518, - 0.082965, 1.013807, 0.119515, 0.999880, - 0.076971, 1.012793, 0.109897, 1.017370, - 0.070518, 1.011894, 0.099872, 1.033661, - 0.063830, 1.010943, 0.089883, 1.048672, - 0.057040, 1.009802, 0.079691, 1.062479, - 0.049917, 1.008670, 0.069458, 1.075052, - 0.042735, 1.007429, 0.059191, 1.086371, - 0.035513, 1.005991, 0.048894, 1.096623, - 0.028359, 1.004468, 0.038770, 1.105871, - 0.021111, 1.002927, 0.028745, 1.114481, - 0.013908, 1.001728, 0.018884, 1.122610, - 0.006843, 1.000740, 0.009264, 1.130165, 0.000062, 0.999983, - 0.000006, 0.000200, - 0.000051, 1.064931, 0.254630, 0.000504, - 0.000128, 1.064668, 0.254630, 0.002016, - 0.000513, 1.064794, 0.254630, 0.004535, - 0.001155, 1.064851, 0.254630, 0.008063, - 0.002053, 1.064966, 0.254630, 0.012598, - 0.003208, 1.064840, 0.254630, 0.018140, - 0.004619, 1.064602, 0.254631, 0.024695, - 0.006288, 1.064965, 0.254632, 0.032251, - 0.008211, 1.064795, 0.254630, 0.040821, - 0.010393, 1.064802, 0.254628, 0.050398, - 0.012830, 1.064758, 0.254627, 0.060987, - 0.015525, 1.064731, 0.254625, 0.072584, - 0.018474, 1.064615, 0.254621, 0.085199, - 0.021682, 1.064672, 0.254619, 0.098826, - 0.025144, 1.064630, 0.254613, 0.113465, - 0.028860, 1.064515, 0.254606, 0.129119, - 0.032823, 1.064416, 0.254598, 0.145767, - 0.036969, 1.064347, 0.254579, 0.163190, - 0.040754, 1.064132, 0.254506, 0.181622, - 0.044989, 1.063951, 0.254437, 0.200981, - 0.049642, 1.063745, 0.254370, 0.221145, - 0.054776, 1.063547, 0.254324, 0.241896, - 0.060538, 1.063289, 0.254346, 0.263684, - 0.066113, 1.063013, 0.254296, 0.286796, - 0.070925, 1.062625, 0.254059, 0.310867, - 0.076187, 1.062216, 0.253817, 0.335644, - 0.081406, 1.061703, 0.253481, 0.360917, - 0.086336, 1.061066, 0.253005, 0.386786, - 0.091790, 1.060454, 0.252558, 0.412921, - 0.097230, 1.059568, 0.252008, 0.439722, - 0.102574, 1.058706, 0.251323, 0.467559, - 0.106972, 1.057682, 0.250239, 0.495605, - 0.111329, 1.056612, 0.248944, 0.523589, - 0.115561, 1.055101, 0.247471, 0.551787, - 0.119732, 1.053745, 0.245777, 0.580426, - 0.122711, 1.051829, 0.243448, 0.608778, - 0.125436, 1.049642, 0.240769, 0.637069, - 0.127993, 1.047749, 0.237739, 0.665251, - 0.129448, 1.045244, 0.233928, 0.692977, - 0.130408, 1.042279, 0.229640, 0.720346, - 0.130931, 1.039693, 0.224829, 0.747365, - 0.130392, 1.036675, 0.219144, 0.773734, - 0.129540, 1.033719, 0.212965, 0.799578, - 0.127689, 1.030774, 0.206047, 0.825002, - 0.125456, 1.028551, 0.198576, 0.849564, - 0.122291, 1.025800, 0.190471, 0.873412, - 0.118720, 1.023657, 0.181739, 0.896628, - 0.114323, 1.021381, 0.172586, 0.918952, - 0.109587, 1.019674, 0.162914, 0.940602, - 0.104093, 1.018126, 0.153039, 0.960917, - 0.098187, 1.016339, 0.142774, 0.980911, - 0.091963, 1.015440, 0.132316, 0.999686, - 0.085159, 1.014377, 0.121453, 1.017538, - 0.078139, 1.013498, 0.110527, 1.033918, - 0.070797, 1.012332, 0.099437, 1.049390, - 0.063129, 1.011368, 0.088157, 1.063402, - 0.055354, 1.010111, 0.076951, 1.076096, - 0.047522, 1.008774, 0.065616, 1.087562, - 0.039447, 1.007202, 0.054310, 1.097591, - 0.031359, 1.005346, 0.042948, 1.106782, - 0.023393, 1.003710, 0.031799, 1.115234, - 0.015461, 1.002116, 0.020943, 1.123166, - 0.007589, 1.000858, 0.010288, 1.130796, 0.000104, 1.000032, - 0.000024, 0.000200, - 0.000056, 1.078780, 0.281356, 0.000504, - 0.000142, 1.079271, 0.281355, 0.002015, - 0.000567, 1.078635, 0.281355, 0.004535, - 0.001276, 1.079164, 0.281356, 0.008064, - 0.002269, 1.079300, 0.281355, 0.012598, - 0.003544, 1.079149, 0.281355, 0.018143, - 0.005104, 1.079329, 0.281355, 0.024691, - 0.006947, 1.079073, 0.281353, 0.032254, - 0.009074, 1.079253, 0.281354, 0.040822, - 0.011484, 1.079176, 0.281353, 0.050399, - 0.014177, 1.079057, 0.281349, 0.060987, - 0.017153, 1.079007, 0.281347, 0.072586, - 0.020412, 1.078998, 0.281343, 0.085203, - 0.023956, 1.078962, 0.281336, 0.098823, - 0.027778, 1.078839, 0.281332, 0.113464, - 0.031882, 1.078783, 0.281325, 0.129114, - 0.036255, 1.078633, 0.281315, 0.145748, - 0.040790, 1.078545, 0.281287, 0.163179, - 0.045024, 1.078311, 0.281208, 0.181649, - 0.049791, 1.078135, 0.281137, 0.201042, - 0.054953, 1.077845, 0.281063, 0.221267, - 0.060551, 1.077576, 0.281006, 0.242114, - 0.066663, 1.077257, 0.280978, 0.263568, - 0.072771, 1.076897, 0.280925, 0.286744, - 0.078349, 1.076405, 0.280689, 0.310840, - 0.084201, 1.075898, 0.280418, 0.335612, - 0.089846, 1.075287, 0.280020, 0.360975, - 0.095394, 1.074482, 0.279513, 0.386932, - 0.101290, 1.073617, 0.278961, 0.413171, - 0.107042, 1.072719, 0.278283, 0.439886, - 0.113083, 1.071698, 0.277547, 0.467535, - 0.118010, 1.070213, 0.276311, 0.495701, - 0.122793, 1.068857, 0.274867, 0.523772, - 0.127278, 1.067037, 0.273153, 0.551849, - 0.131671, 1.064923, 0.271176, 0.580338, - 0.135293, 1.062749, 0.268626, 0.608771, - 0.138065, 1.059944, 0.265569, 0.636756, - 0.140565, 1.056851, 0.262054, 0.664574, - 0.142434, 1.053461, 0.257807, 0.692151, - 0.143237, 1.049910, 0.252930, 0.719376, - 0.143717, 1.046426, 0.247414, 0.745852, - 0.143117, 1.042377, 0.241001, 0.772300, - 0.141975, 1.038789, 0.233797, 0.798050, - 0.140114, 1.035290, 0.226218, 0.823370, - 0.137379, 1.032374, 0.217785, 0.847735, - 0.134119, 1.028853, 0.208748, 0.871897, - 0.129985, 1.026395, 0.198877, 0.894950, - 0.125324, 1.023787, 0.188803, 0.917909, - 0.120007, 1.022073, 0.178493, 0.939567, - 0.114099, 1.020098, 0.167466, 0.960534, - 0.107748, 1.018851, 0.156223, 0.980423, - 0.100748, 1.017362, 0.144716, 0.999334, - 0.093494, 1.015961, 0.133028, 1.017561, - 0.085728, 1.015059, 0.120953, 1.034225, - 0.077627, 1.013888, 0.108943, 1.049937, - 0.069375, 1.012898, 0.096678, 1.064265, - 0.060807, 1.011635, 0.084350, 1.077188, - 0.052052, 1.010095, 0.071964, 1.088637, - 0.043304, 1.008399, 0.059531, 1.098766, - 0.034458, 1.006397, 0.047134, 1.107697, - 0.025637, 1.004354, 0.034887, 1.116055, - 0.016932, 1.002611, 0.022948, 1.123819, - 0.008437, 1.001023, 0.011386, 1.131333, 0.000087, 0.999952, - 0.000097, 0.000200, - 0.000062, 1.095622, 0.308458, 0.000504, - 0.000155, 1.094863, 0.308458, 0.002016, - 0.000622, 1.095169, 0.308458, 0.004535, - 0.001399, 1.095156, 0.308458, 0.008063, - 0.002487, 1.095413, 0.308455, 0.012598, - 0.003886, 1.095147, 0.308458, 0.018141, - 0.005596, 1.095150, 0.308457, 0.024692, - 0.007616, 1.095140, 0.308457, 0.032252, - 0.009947, 1.095098, 0.308456, 0.040822, - 0.012589, 1.095096, 0.308453, 0.050399, - 0.015541, 1.095070, 0.308451, 0.060985, - 0.018803, 1.094922, 0.308448, 0.072583, - 0.022375, 1.094902, 0.308444, 0.085197, - 0.026258, 1.094882, 0.308438, 0.098822, - 0.030448, 1.094775, 0.308429, 0.113460, - 0.034944, 1.094641, 0.308419, 0.129112, - 0.039731, 1.094530, 0.308403, 0.145711, - 0.044610, 1.094332, 0.308365, 0.163178, - 0.049362, 1.094149, 0.308285, 0.181679, - 0.054666, 1.093876, 0.308210, 0.201109, - 0.060336, 1.093603, 0.308132, 0.221388, - 0.066414, 1.093250, 0.308047, 0.242315, - 0.072881, 1.092835, 0.307985, 0.263651, - 0.079453, 1.092391, 0.307902, 0.286720, - 0.085882, 1.091866, 0.307688, 0.310817, - 0.092274, 1.091225, 0.307379, 0.335562, - 0.098306, 1.090346, 0.306906, 0.361043, - 0.104572, 1.089423, 0.306374, 0.387051, - 0.110843, 1.088437, 0.305710, 0.413405, - 0.117062, 1.087228, 0.304906, 0.440122, - 0.123501, 1.085879, 0.304017, 0.467522, - 0.129245, 1.084197, 0.302783, 0.495721, - 0.134285, 1.082284, 0.301104, 0.523925, - 0.139143, 1.080109, 0.299142, 0.551814, - 0.143638, 1.077043, 0.296825, 0.579878, - 0.147774, 1.074071, 0.294071, 0.608316, - 0.150724, 1.070621, 0.290519, 0.636059, - 0.153168, 1.066390, 0.286424, 0.663481, - 0.155139, 1.062069, 0.281559, 0.690753, - 0.155944, 1.057211, 0.276024, 0.717767, - 0.156176, 1.052682, 0.269622, 0.743937, - 0.155783, 1.047747, 0.262532, 0.770214, - 0.154245, 1.043510, 0.254609, 0.795542, - 0.152192, 1.039121, 0.246007, 0.821099, - 0.149256, 1.035962, 0.236663, 0.845452, - 0.145605, 1.032320, 0.226751, 0.869780, - 0.141186, 1.029390, 0.216165, 0.893141, - 0.136137, 1.026485, 0.204937, 0.916034, - 0.130332, 1.024389, 0.193624, 0.938089, - 0.124040, 1.022270, 0.181756, 0.959488, - 0.117011, 1.020457, 0.169339, 0.979594, - 0.109617, 1.018871, 0.156875, 0.998912, - 0.101562, 1.017533, 0.144288, 1.017100, - 0.093164, 1.016445, 0.131370, 1.034413, - 0.084488, 1.015453, 0.118322, 1.050347, - 0.075377, 1.014259, 0.104963, 1.064958, - 0.066108, 1.013057, 0.091722, 1.078045, - 0.056702, 1.011491, 0.078231, 1.089749, - 0.047106, 1.009662, 0.064797, 1.099831, - 0.037467, 1.007417, 0.051315, 1.108789, - 0.027990, 1.005144, 0.038064, 1.116865, - 0.018464, 1.002925, 0.025008, 1.124609, - 0.009068, 1.001221, 0.012250, 1.132040, 0.000093, 0.999984, - 0.000071, 0.000200, - 0.000067, 1.112554, 0.335981, 0.000504, - 0.000169, 1.112660, 0.335981, 0.002016, - 0.000677, 1.112827, 0.335981, 0.004533, - 0.001523, 1.112147, 0.335982, 0.008063, - 0.002709, 1.112882, 0.335979, 0.012598, - 0.004233, 1.112891, 0.335980, 0.018141, - 0.006095, 1.112882, 0.335980, 0.024693, - 0.008296, 1.112877, 0.335978, 0.032252, - 0.010834, 1.112860, 0.335976, 0.040824, - 0.013713, 1.112965, 0.335974, 0.050398, - 0.016927, 1.112753, 0.335971, 0.060991, - 0.020482, 1.112826, 0.335970, 0.072587, - 0.024371, 1.112676, 0.335962, 0.085199, - 0.028597, 1.112593, 0.335955, 0.098822, - 0.033159, 1.112453, 0.335943, 0.113461, - 0.038052, 1.112329, 0.335930, 0.129108, - 0.043255, 1.112144, 0.335910, 0.145665, - 0.048412, 1.111905, 0.335857, 0.163185, - 0.053786, 1.111668, 0.335781, 0.181710, - 0.059608, 1.111345, 0.335696, 0.201166, - 0.065794, 1.110979, 0.335606, 0.221489, - 0.072361, 1.110553, 0.335505, 0.242471, - 0.079184, 1.110112, 0.335396, 0.263900, - 0.086213, 1.109584, 0.335271, 0.286688, - 0.093491, 1.108927, 0.335089, 0.310773, - 0.100406, 1.108091, 0.334737, 0.335573, - 0.106987, 1.107169, 0.334208, 0.361117, - 0.113844, 1.106097, 0.333600, 0.387175, - 0.120463, 1.104826, 0.332828, 0.413665, - 0.127245, 1.103415, 0.331929, 0.440386, - 0.133927, 1.101632, 0.330851, 0.467527, - 0.140496, 1.099563, 0.329538, 0.495630, - 0.145874, 1.096956, 0.327618, 0.523864, - 0.150997, 1.094201, 0.325390, 0.551705, - 0.155713, 1.090342, 0.322688, 0.579383, - 0.159993, 1.086010, 0.319483, 0.607301, - 0.163238, 1.081226, 0.315522, 0.634873, - 0.165667, 1.076065, 0.310840, 0.662028, - 0.167606, 1.070466, 0.305377, 0.688755, - 0.168626, 1.064601, 0.299056, 0.715612, - 0.168578, 1.059269, 0.291963, 0.741604, - 0.167961, 1.053648, 0.284018, 0.767757, - 0.166439, 1.048928, 0.275474, 0.793264, - 0.164023, 1.044343, 0.266056, 0.818165, - 0.160965, 1.039909, 0.255750, 0.843255, - 0.156896, 1.036180, 0.244843, 0.867249, - 0.152262, 1.032303, 0.233464, 0.890994, - 0.146655, 1.029365, 0.221128, 0.913829, - 0.140574, 1.026607, 0.208554, 0.936508, - 0.133640, 1.024512, 0.195772, 0.957720, - 0.126220, 1.022421, 0.182420, 0.978940, - 0.118164, 1.021293, 0.168852, 0.998285, - 0.109558, 1.019444, 0.155261, 1.016764, - 0.100562, 1.017825, 0.141395, 1.034387, - 0.091064, 1.016996, 0.127311, 1.050916, - 0.081468, 1.015945, 0.113089, 1.065652, - 0.071463, 1.014547, 0.098879, 1.079155, - 0.061240, 1.013066, 0.084468, 1.090822, - 0.050980, 1.010788, 0.069940, 1.101100, - 0.040549, 1.008563, 0.055475, 1.109824, - 0.030101, 1.005950, 0.041033, 1.117828, - 0.019884, 1.003453, 0.027022, 1.125443, - 0.009900, 1.001484, 0.013306, 1.132869, 0.000094, 1.000004, - 0.000046, 0.000200, - 0.000073, 1.132849, 0.363970, 0.000504, - 0.000183, 1.132155, 0.363969, 0.002016, - 0.000734, 1.132516, 0.363969, 0.004535, - 0.001651, 1.132256, 0.363969, 0.008062, - 0.002934, 1.132318, 0.363966, 0.012597, - 0.004585, 1.132386, 0.363968, 0.018141, - 0.006602, 1.132457, 0.363967, 0.024693, - 0.008987, 1.132511, 0.363967, 0.032252, - 0.011737, 1.132488, 0.363965, 0.040819, - 0.014853, 1.132241, 0.363959, 0.050398, - 0.018336, 1.132372, 0.363958, 0.060988, - 0.022185, 1.132373, 0.363954, 0.072582, - 0.026396, 1.132137, 0.363943, 0.085195, - 0.030973, 1.132071, 0.363935, 0.098822, - 0.035913, 1.131978, 0.363922, 0.113461, - 0.041209, 1.131801, 0.363905, 0.129116, - 0.046833, 1.131535, 0.363867, 0.145640, - 0.052346, 1.131290, 0.363814, 0.163199, - 0.058275, 1.131046, 0.363734, 0.181742, - 0.064623, 1.130671, 0.363642, 0.201227, - 0.071336, 1.130224, 0.363539, 0.221587, - 0.078396, 1.129758, 0.363419, 0.242625, - 0.085545, 1.129213, 0.363256, 0.264183, - 0.093110, 1.128549, 0.363097, 0.286668, - 0.101206, 1.127767, 0.362939, 0.310745, - 0.108586, 1.126796, 0.362516, 0.335602, - 0.115827, 1.125686, 0.361953, 0.361202, - 0.123212, 1.124451, 0.361275, 0.387298, - 0.130294, 1.122861, 0.360376, 0.413918, - 0.137553, 1.121154, 0.359362, 0.440680, - 0.144577, 1.118825, 0.358069, 0.467667, - 0.151558, 1.116002, 0.356581, 0.495449, - 0.157621, 1.112778, 0.354531, 0.523514, - 0.162844, 1.108842, 0.351915, 0.551250, - 0.167744, 1.104075, 0.348797, 0.578629, - 0.172132, 1.098733, 0.345222, 0.605757, - 0.175733, 1.092224, 0.340665, 0.633392, - 0.178109, 1.086201, 0.335286, 0.660783, - 0.180009, 1.080110, 0.329286, 0.687219, - 0.181105, 1.073419, 0.322319, 0.713873, - 0.181046, 1.067410, 0.314616, 0.740094, - 0.180219, 1.061414, 0.306014, 0.765233, - 0.178559, 1.055287, 0.296704, 0.790885, - 0.175806, 1.049727, 0.286394, 0.815464, - 0.172354, 1.044519, 0.275189, 0.840259, - 0.168048, 1.040375, 0.263441, 0.864285, - 0.162904, 1.036010, 0.250918, 0.888806, - 0.157194, 1.033525, 0.237611, 0.911682, - 0.150486, 1.029490, 0.223809, 0.934481, - 0.143212, 1.026778, 0.209705, 0.956337, - 0.135233, 1.024632, 0.195281, 0.977380, - 0.126650, 1.022737, 0.180878, 0.997427, - 0.117552, 1.021110, 0.166112, 1.016666, - 0.107814, 1.019869, 0.151231, 1.034337, - 0.097814, 1.018543, 0.136375, 1.051082, - 0.087330, 1.017476, 0.121187, 1.066326, - 0.076614, 1.016083, 0.106043, 1.079897, - 0.065793, 1.014227, 0.090566, 1.092136, - 0.054654, 1.012334, 0.074988, 1.102315, - 0.043516, 1.009627, 0.059577, 1.111105, - 0.032509, 1.006808, 0.044202, 1.118861, - 0.021381, 1.003917, 0.028995, 1.126363, - 0.010489, 1.001670, 0.014269, 1.133598, 0.000083, 0.999989, - 0.000035, 0.000200, - 0.000079, 1.155026, 0.392470, 0.000504, - 0.000198, 1.154184, 0.392469, 0.002016, - 0.000791, 1.153990, 0.392469, 0.004535, - 0.001780, 1.154045, 0.392469, 0.008063, - 0.003164, 1.154007, 0.392466, 0.012598, - 0.004944, 1.154022, 0.392469, 0.018141, - 0.007119, 1.154015, 0.392468, 0.024692, - 0.009690, 1.154017, 0.392466, 0.032254, - 0.012656, 1.154069, 0.392465, 0.040826, - 0.016018, 1.153980, 0.392459, 0.050399, - 0.019771, 1.153911, 0.392456, 0.060987, - 0.023919, 1.153860, 0.392447, 0.072588, - 0.028461, 1.153777, 0.392442, 0.085197, - 0.033393, 1.153582, 0.392428, 0.098822, - 0.038716, 1.153434, 0.392412, 0.113462, - 0.044422, 1.153271, 0.392390, 0.129101, - 0.050455, 1.153019, 0.392359, 0.145642, - 0.056392, 1.152721, 0.392283, 0.163223, - 0.062859, 1.152404, 0.392201, 0.181779, - 0.069721, 1.151941, 0.392099, 0.201289, - 0.076968, 1.151422, 0.391978, 0.221678, - 0.084518, 1.150861, 0.391833, 0.242752, - 0.092017, 1.150156, 0.391618, 0.264474, - 0.100184, 1.149402, 0.391421, 0.286768, - 0.108921, 1.148545, 0.391249, 0.310719, - 0.116815, 1.147388, 0.390773, 0.335638, - 0.124785, 1.146042, 0.390168, 0.361240, - 0.132630, 1.144529, 0.389394, 0.387443, - 0.140298, 1.142602, 0.388391, 0.414067, - 0.147913, 1.140361, 0.387199, 0.440904, - 0.155362, 1.137612, 0.385742, 0.467771, - 0.162574, 1.133659, 0.383926, 0.494907, - 0.169312, 1.129246, 0.381715, 0.522801, - 0.174778, 1.124228, 0.378678, 0.550751, - 0.179824, 1.118697, 0.375158, 0.578018, - 0.184284, 1.112019, 0.370851, 0.605291, - 0.188215, 1.105151, 0.365928, 0.632269, - 0.190760, 1.097677, 0.360114, 0.659432, - 0.192457, 1.090816, 0.353498, 0.685839, - 0.193458, 1.083286, 0.346094, 0.711876, - 0.193502, 1.076245, 0.337754, 0.738184, - 0.192371, 1.069684, 0.328412, 0.763723, - 0.190531, 1.063249, 0.318164, 0.789192, - 0.187726, 1.057265, 0.306900, 0.813744, - 0.183783, 1.051177, 0.295021, 0.838408, - 0.179328, 1.045902, 0.282144, 0.862116, - 0.173573, 1.040853, 0.268438, 0.885636, - 0.167350, 1.036515, 0.254108, 0.909342, - 0.160229, 1.033269, 0.239082, 0.931962, - 0.152529, 1.029627, 0.224024, 0.954671, - 0.144080, 1.027507, 0.208393, 0.975707, - 0.135023, 1.024657, 0.192630, 0.996644, - 0.125258, 1.022998, 0.176741, 1.015817, - 0.115089, 1.021234, 0.160926, 1.034301, - 0.104317, 1.020025, 0.145042, 1.051131, - 0.093218, 1.018739, 0.129052, 1.066836, - 0.081828, 1.017419, 0.112905, 1.081027, - 0.070132, 1.015714, 0.096578, 1.093225, - 0.058382, 1.013465, 0.080077, 1.103691, - 0.046527, 1.010853, 0.063580, 1.112431, - 0.034624, 1.007702, 0.047118, 1.120035, - 0.022913, 1.004551, 0.031018, 1.127336, - 0.011284, 1.001924, 0.015283, 1.134510, 0.000170, 0.999937, - 0.000058, 0.000200, - 0.000084, 1.177044, 0.421534, 0.000504, - 0.000212, 1.177312, 0.421533, 0.002016, - 0.000850, 1.177730, 0.421533, 0.004535, - 0.001912, 1.177722, 0.421533, 0.008063, - 0.003399, 1.177844, 0.421529, 0.012598, - 0.005310, 1.177768, 0.421533, 0.018141, - 0.007646, 1.177730, 0.421531, 0.024692, - 0.010407, 1.177663, 0.421530, 0.032252, - 0.013592, 1.177681, 0.421527, 0.040821, - 0.017201, 1.177562, 0.421524, 0.050401, - 0.021234, 1.177445, 0.421516, 0.060988, - 0.025688, 1.177461, 0.421509, 0.072590, - 0.030565, 1.177364, 0.421498, 0.085200, - 0.035860, 1.177205, 0.421482, 0.098823, - 0.041572, 1.177011, 0.421462, 0.113465, - 0.047694, 1.176794, 0.421436, 0.129094, - 0.054122, 1.176504, 0.421396, 0.145652, - 0.060530, 1.176203, 0.421311, 0.163245, - 0.067517, 1.175805, 0.421218, 0.181825, - 0.074919, 1.175271, 0.421108, 0.201360, - 0.082700, 1.174717, 0.420974, 0.221773, - 0.090727, 1.174021, 0.420795, 0.242908, - 0.098719, 1.173173, 0.420536, 0.264742, - 0.107417, 1.172285, 0.420296, 0.287091, - 0.116601, 1.171326, 0.420065, 0.310723, - 0.125265, 1.169907, 0.419582, 0.335685, - 0.133876, 1.168352, 0.418912, 0.361285, - 0.142140, 1.166322, 0.418006, 0.387562, - 0.150436, 1.164136, 0.416899, 0.414175, - 0.158388, 1.161162, 0.415513, 0.441021, - 0.166258, 1.157608, 0.413836, 0.467698, - 0.173720, 1.152519, 0.411702, 0.494730, - 0.180843, 1.147020, 0.409102, 0.522524, - 0.186906, 1.141256, 0.405789, 0.550055, - 0.192004, 1.134114, 0.401759, 0.577512, - 0.196588, 1.127086, 0.397153, 0.604348, - 0.200420, 1.119029, 0.391767, 0.630970, - 0.203320, 1.110308, 0.385573, 0.658023, - 0.204883, 1.102643, 0.378245, 0.684422, - 0.205716, 1.094573, 0.370191, 0.710405, - 0.205767, 1.086405, 0.361231, 0.736417, - 0.204513, 1.078712, 0.351106, 0.761836, - 0.202281, 1.071619, 0.340096, 0.787140, - 0.199395, 1.064873, 0.328139, 0.812197, - 0.195185, 1.058313, 0.315044, 0.836342, - 0.190191, 1.052085, 0.300933, 0.860311, - 0.184343, 1.046705, 0.286411, 0.883597, - 0.177415, 1.041072, 0.270897, 0.906852, - 0.170003, 1.036797, 0.254825, 0.929991, - 0.161592, 1.033264, 0.238176, 0.952478, - 0.152792, 1.030250, 0.221581, 0.974216, - 0.143032, 1.027331, 0.204378, 0.995372, - 0.132922, 1.025135, 0.187470, 1.015330, - 0.122009, 1.023250, 0.170538, 1.034070, - 0.110740, 1.022021, 0.153777, 1.051295, - 0.099016, 1.020271, 0.136916, 1.067460, - 0.086920, 1.018948, 0.119880, 1.082022, - 0.074729, 1.017336, 0.102565, 1.094378, - 0.062036, 1.014820, 0.084994, 1.104998, - 0.049413, 1.011999, 0.067650, 1.113773, - 0.036812, 1.008711, 0.050148, 1.121263, - 0.024274, 1.005141, 0.032976, 1.128420, - 0.012038, 1.002196, 0.016239, 1.135496, 0.000106, 1.000042, - 0.000062, 0.000200, - 0.000090, 1.203048, 0.451217, 0.000504, - 0.000227, 1.203226, 0.451215, 0.002016, - 0.000909, 1.203450, 0.451215, 0.004535, - 0.002046, 1.203569, 0.451215, 0.008062, - 0.003638, 1.203609, 0.451209, 0.012598, - 0.005684, 1.203580, 0.451214, 0.018141, - 0.008185, 1.203515, 0.451212, 0.024694, - 0.011141, 1.203618, 0.451211, 0.032253, - 0.014549, 1.203609, 0.451207, 0.040815, - 0.018409, 1.203302, 0.451203, 0.050401, - 0.022727, 1.203454, 0.451195, 0.060990, - 0.027495, 1.203480, 0.451188, 0.072591, - 0.032713, 1.203220, 0.451172, 0.085203, - 0.038378, 1.203058, 0.451154, 0.098829, - 0.044489, 1.202838, 0.451130, 0.113466, - 0.051031, 1.202530, 0.451098, 0.129084, - 0.057808, 1.202270, 0.451041, 0.145669, - 0.064769, 1.201904, 0.450956, 0.163278, - 0.072278, 1.201411, 0.450853, 0.181880, - 0.080224, 1.200825, 0.450721, 0.201436, - 0.088537, 1.200164, 0.450566, 0.221865, - 0.097009, 1.199335, 0.450351, 0.243083, - 0.105591, 1.198383, 0.450062, 0.265033, - 0.114818, 1.197380, 0.449769, 0.287456, - 0.124372, 1.196137, 0.449438, 0.310758, - 0.133892, 1.194554, 0.448974, 0.335721, - 0.143052, 1.192649, 0.448216, 0.361348, - 0.151868, 1.190233, 0.447202, 0.387573, - 0.160644, 1.187211, 0.445926, 0.414159, - 0.169028, 1.183452, 0.444313, 0.440950, - 0.177169, 1.178562, 0.442315, 0.467998, - 0.185090, 1.173540, 0.439960, 0.494566, - 0.192396, 1.166344, 0.436989, 0.521730, - 0.198915, 1.159283, 0.433439, 0.549405, - 0.204240, 1.151503, 0.428984, 0.576755, - 0.208861, 1.143004, 0.423839, 0.603635, - 0.212734, 1.134099, 0.418012, 0.629979, - 0.215712, 1.124555, 0.411445, 0.656597, - 0.217385, 1.115293, 0.403628, 0.683317, - 0.218093, 1.106460, 0.394639, 0.708990, - 0.217835, 1.097389, 0.385012, 0.734898, - 0.216774, 1.088940, 0.373999, 0.760342, - 0.214120, 1.080385, 0.362128, 0.785517, - 0.210821, 1.072959, 0.349184, 0.809933, - 0.206443, 1.065450, 0.335080, 0.834339, - 0.200942, 1.058701, 0.320257, 0.858793, - 0.194938, 1.052711, 0.304133, 0.882300, - 0.187615, 1.047044, 0.287771, 0.905560, - 0.179626, 1.042083, 0.270571, 0.927916, - 0.170753, 1.037077, 0.252741, 0.950415, - 0.161270, 1.033200, 0.234656, 0.972920, - 0.151239, 1.030418, 0.216652, 0.993893, - 0.140358, 1.027479, 0.198252, 1.014204, - 0.128963, 1.024897, 0.180113, 1.033878, - 0.117128, 1.023648, 0.162282, 1.051754, - 0.104678, 1.022230, 0.144366, 1.067924, - 0.092000, 1.020453, 0.126455, 1.082643, - 0.078837, 1.018518, 0.108194, 1.095503, - 0.065669, 1.016199, 0.089966, 1.106290, - 0.052345, 1.013113, 0.071530, 1.115219, - 0.039024, 1.009636, 0.053158, 1.122587, - 0.025789, 1.005801, 0.034959, 1.129461, - 0.012622, 1.002442, 0.017222, 1.136468, 0.000152, 0.999964, - 0.000065, 0.000200, - 0.000096, 1.231156, 0.481574, 0.000504, - 0.000243, 1.232187, 0.481572, 0.002016, - 0.000971, 1.231948, 0.481572, 0.004535, - 0.002184, 1.231919, 0.481572, 0.008061, - 0.003882, 1.231453, 0.481566, 0.012597, - 0.006066, 1.231800, 0.481572, 0.018142, - 0.008736, 1.231756, 0.481569, 0.024693, - 0.011889, 1.232062, 0.481570, 0.032254, - 0.015528, 1.231915, 0.481563, 0.040822, - 0.019650, 1.231863, 0.481559, 0.050402, - 0.024255, 1.231737, 0.481550, 0.060992, - 0.029342, 1.231678, 0.481537, 0.072592, - 0.034908, 1.231537, 0.481521, 0.085207, - 0.040953, 1.231336, 0.481499, 0.098834, - 0.047469, 1.231071, 0.481469, 0.113474, - 0.054441, 1.230757, 0.481431, 0.129077, - 0.061556, 1.230424, 0.481359, 0.145691, - 0.069091, 1.230022, 0.481269, 0.163321, - 0.077151, 1.229461, 0.481156, 0.181936, - 0.085636, 1.228718, 0.481011, 0.201516, - 0.094484, 1.228023, 0.480830, 0.221963, - 0.103362, 1.227057, 0.480562, 0.243264, - 0.112628, 1.225997, 0.480247, 0.265291, - 0.122366, 1.224744, 0.479891, 0.287824, - 0.132256, 1.223255, 0.479461, 0.310927, - 0.142614, 1.221348, 0.478978, 0.335749, - 0.152326, 1.218953, 0.478132, 0.361361, - 0.161747, 1.215806, 0.476971, 0.387480, - 0.170879, 1.211853, 0.475477, 0.414231, - 0.179865, 1.207783, 0.473686, 0.441065, - 0.188331, 1.202051, 0.471415, 0.467923, - 0.196454, 1.195463, 0.468647, 0.494526, - 0.204048, 1.187542, 0.465459, 0.521318, - 0.211020, 1.179235, 0.461650, 0.548654, - 0.216520, 1.170110, 0.456868, 0.575778, - 0.221098, 1.160163, 0.451227, 0.602610, - 0.224923, 1.149751, 0.444866, 0.628891, - 0.227895, 1.139169, 0.437577, 0.655635, - 0.230020, 1.129736, 0.429369, 0.682115, - 0.230419, 1.119516, 0.419673, 0.707514, - 0.229789, 1.108277, 0.409143, 0.733169, - 0.228520, 1.099159, 0.397296, 0.758342, - 0.225793, 1.089839, 0.384578, 0.783477, - 0.222049, 1.081428, 0.370323, 0.808497, - 0.217562, 1.073742, 0.355253, 0.832790, - 0.211697, 1.065850, 0.339282, 0.856677, - 0.204989, 1.058834, 0.322181, 0.880662, - 0.197653, 1.053291, 0.304610, 0.903474, - 0.188858, 1.046822, 0.286042, 0.926313, - 0.179746, 1.041663, 0.267224, 0.948458, - 0.169542, 1.036532, 0.247978, 0.970873, - 0.159005, 1.033008, 0.228535, 0.992958, - 0.147658, 1.029844, 0.208819, 1.013413, - 0.135771, 1.026930, 0.189486, 1.033483, - 0.123256, 1.025545, 0.170422, 1.051872, - 0.110401, 1.023935, 0.152075, 1.068396, - 0.096860, 1.022092, 0.133169, 1.083731, - 0.083259, 1.020221, 0.114022, 1.096849, - 0.069266, 1.017663, 0.094772, 1.107864, - 0.055203, 1.014524, 0.075432, 1.116600, - 0.041097, 1.010514, 0.055980, 1.123871, - 0.027083, 1.006313, 0.036839, 1.130718, - 0.013510, 1.002778, 0.018156, 1.137649, 0.000154, 1.000033, - 0.000028, 0.000200, - 0.000103, 1.264025, 0.512670, 0.000504, - 0.000258, 1.262437, 0.512667, 0.002016, - 0.001033, 1.262691, 0.512668, 0.004535, - 0.002325, 1.262834, 0.512667, 0.008063, - 0.004133, 1.262783, 0.512659, 0.012598, - 0.006458, 1.262803, 0.512666, 0.018141, - 0.009299, 1.262720, 0.512665, 0.024683, - 0.012652, 1.262061, 0.512655, 0.032257, - 0.016532, 1.262858, 0.512656, 0.040826, - 0.020919, 1.262709, 0.512649, 0.050403, - 0.025820, 1.262685, 0.512639, 0.060993, - 0.031233, 1.262544, 0.512625, 0.072597, - 0.037157, 1.262435, 0.512607, 0.085211, - 0.043587, 1.262209, 0.512581, 0.098842, - 0.050520, 1.261907, 0.512544, 0.113484, - 0.057926, 1.261575, 0.512500, 0.129097, - 0.065460, 1.261293, 0.512420, 0.145727, - 0.073543, 1.260736, 0.512316, 0.163375, - 0.082134, 1.260117, 0.512190, 0.182011, - 0.091173, 1.259299, 0.512024, 0.201598, - 0.100540, 1.258381, 0.511810, 0.222084, - 0.109931, 1.257293, 0.511505, 0.243446, - 0.119838, 1.256050, 0.511151, 0.265574, - 0.130090, 1.254607, 0.510724, 0.288230, - 0.140421, 1.252808, 0.510191, 0.311336, - 0.151343, 1.250489, 0.509627, 0.335719, - 0.161689, 1.247279, 0.508688, 0.361314, - 0.171748, 1.243467, 0.507393, 0.387541, - 0.181399, 1.239145, 0.505758, 0.414204, - 0.190768, 1.233760, 0.503676, 0.441092, - 0.199659, 1.227433, 0.501129, 0.467789, - 0.207934, 1.219247, 0.498078, 0.494454, - 0.215747, 1.210441, 0.494630, 0.520950, - 0.222869, 1.200559, 0.490467, 0.547802, - 0.228881, 1.189872, 0.485444, 0.575563, - 0.233760, 1.180081, 0.479268, 0.602426, - 0.237566, 1.168544, 0.472272, 0.628772, - 0.240447, 1.156546, 0.464390, 0.654963, - 0.242427, 1.145123, 0.455345, 0.681384, - 0.242980, 1.134322, 0.444885, 0.707173, - 0.242150, 1.122665, 0.433338, 0.732477, - 0.240435, 1.111733, 0.420647, 0.757567, - 0.237806, 1.101271, 0.406799, 0.782341, - 0.233503, 1.091341, 0.391761, 0.806690, - 0.228346, 1.082042, 0.375576, 0.830804, - 0.222386, 1.073504, 0.358545, 0.854940, - 0.215141, 1.065880, 0.340431, 0.878709, - 0.207207, 1.058850, 0.321690, 0.901928, - 0.198273, 1.052588, 0.301930, 0.924845, - 0.188476, 1.046521, 0.281513, 0.946932, - 0.177996, 1.040966, 0.261234, 0.969256, - 0.166644, 1.036670, 0.240356, 0.991323, - 0.154968, 1.032694, 0.219748, 1.013013, - 0.142425, 1.030061, 0.199103, 1.032845, - 0.129456, 1.027254, 0.178936, 1.051887, - 0.115763, 1.025497, 0.159243, 1.069179, - 0.101851, 1.023807, 0.139560, 1.084499, - 0.087357, 1.021441, 0.119607, 1.097921, - 0.072796, 1.018780, 0.099501, 1.109281, - 0.058037, 1.015566, 0.079211, 1.118194, - 0.043226, 1.011494, 0.058873, 1.125351, - 0.028633, 1.007089, 0.038736, 1.132002, - 0.013996, 1.003014, 0.019063, 1.138951, 0.000132, 1.000036, - 0.000007, 0.000200, - 0.000109, 1.296791, 0.544571, 0.000504, - 0.000274, 1.296055, 0.544568, 0.002016, - 0.001098, 1.297239, 0.544568, 0.004535, - 0.002470, 1.296600, 0.544568, 0.008062, - 0.004390, 1.296368, 0.544559, 0.012597, - 0.006860, 1.296454, 0.544566, 0.018141, - 0.009878, 1.296522, 0.544565, 0.024693, - 0.013444, 1.296536, 0.544560, 0.032256, - 0.017559, 1.296638, 0.544557, 0.040824, - 0.022218, 1.296491, 0.544547, 0.050408, - 0.027426, 1.296552, 0.544532, 0.060997, - 0.033173, 1.296283, 0.544518, 0.072600, - 0.039463, 1.296113, 0.544496, 0.085220, - 0.046292, 1.295894, 0.544466, 0.098851, - 0.053648, 1.295545, 0.544422, 0.113496, - 0.061487, 1.295201, 0.544371, 0.129112, - 0.069467, 1.294754, 0.544273, 0.145765, - 0.078092, 1.294209, 0.544160, 0.163431, - 0.087231, 1.293534, 0.544017, 0.182088, - 0.096837, 1.292580, 0.543828, 0.201698, - 0.106713, 1.291586, 0.543585, 0.222231, - 0.116699, 1.290325, 0.543238, 0.243653, - 0.127208, 1.288888, 0.542836, 0.265855, - 0.137949, 1.287131, 0.542329, 0.288623, - 0.148847, 1.284936, 0.541700, 0.311830, - 0.160204, 1.282109, 0.540997, 0.335728, - 0.171324, 1.278036, 0.540045, 0.361403, - 0.181915, 1.273912, 0.538603, 0.387647, - 0.192124, 1.268881, 0.536741, 0.414217, - 0.201807, 1.262363, 0.534432, 0.441090, - 0.211093, 1.254755, 0.531623, 0.467823, - 0.219678, 1.245456, 0.528314, 0.494361, - 0.227581, 1.234953, 0.524391, 0.521264, - 0.235087, 1.224839, 0.519902, 0.547881, - 0.241508, 1.213175, 0.514574, 0.574965, - 0.246315, 1.200505, 0.507837, 0.601847, - 0.250061, 1.187901, 0.500286, 0.628207, - 0.252822, 1.174601, 0.491502, 0.654445, - 0.254691, 1.161944, 0.481726, 0.680175, - 0.255318, 1.149305, 0.470727, 0.706168, - 0.254257, 1.136708, 0.458045, 0.731458, - 0.252100, 1.124047, 0.444438, 0.756378, - 0.249115, 1.112942, 0.429611, 0.781311, - 0.244899, 1.101800, 0.413501, 0.805755, - 0.239225, 1.091662, 0.395889, 0.829867, - 0.232830, 1.082291, 0.377860, 0.853067, - 0.225193, 1.072820, 0.358704, 0.877084, - 0.216648, 1.065415, 0.338413, 0.900123, - 0.207390, 1.058403, 0.317596, 0.923370, - 0.197095, 1.051412, 0.296301, 0.946021, - 0.186084, 1.045877, 0.274498, 0.967669, - 0.174262, 1.040316, 0.252565, 0.989761, - 0.161814, 1.035489, 0.230312, 1.012163, - 0.149076, 1.032540, 0.208746, 1.032547, - 0.135299, 1.029598, 0.187180, 1.052032, - 0.121277, 1.027355, 0.166482, 1.069907, - 0.106582, 1.025622, 0.145939, 1.085563, - 0.091589, 1.023244, 0.125362, 1.099447, - 0.076263, 1.020661, 0.104087, 1.110848, - 0.060825, 1.017035, 0.083036, 1.119923, - 0.045319, 1.012675, 0.061719, 1.126805, - 0.029852, 1.007668, 0.040583, 1.133282, - 0.014846, 1.003335, 0.019969, 1.140128, 0.000149, 1.000024, - 0.000037, 0.000200, - 0.000116, 1.334863, 0.577350, 0.000504, - 0.000291, 1.333350, 0.577348, 0.002015, - 0.001164, 1.332853, 0.577347, 0.004535, - 0.002618, 1.333295, 0.577347, 0.008062, - 0.004655, 1.333189, 0.577336, 0.012598, - 0.007273, 1.333309, 0.577345, 0.018141, - 0.010472, 1.333274, 0.577342, 0.024694, - 0.014253, 1.333231, 0.577339, 0.032254, - 0.018614, 1.333265, 0.577332, 0.040827, - 0.023556, 1.333261, 0.577321, 0.050400, - 0.029069, 1.332893, 0.577309, 0.061000, - 0.035166, 1.332998, 0.577288, 0.072608, - 0.041833, 1.332901, 0.577263, 0.085227, - 0.049067, 1.332603, 0.577226, 0.098864, - 0.056860, 1.332264, 0.577177, 0.113507, - 0.065114, 1.331825, 0.577109, 0.129146, - 0.073610, 1.331311, 0.577005, 0.145808, - 0.082766, 1.330639, 0.576872, 0.163494, - 0.092458, 1.329878, 0.576709, 0.182176, - 0.102639, 1.328889, 0.576501, 0.201804, - 0.112983, 1.327710, 0.576207, 0.222394, - 0.123650, 1.326256, 0.575823, 0.243881, - 0.134780, 1.324593, 0.575363, 0.266122, - 0.145931, 1.322426, 0.574751, 0.289043, - 0.157500, 1.319837, 0.574033, 0.312330, - 0.169208, 1.316301, 0.573181, 0.336120, - 0.181125, 1.312251, 0.572188, 0.361506, - 0.192232, 1.307003, 0.570631, 0.387757, - 0.202981, 1.301068, 0.568558, 0.414365, - 0.213160, 1.293695, 0.566027, 0.440986, - 0.222617, 1.283958, 0.562942, 0.467943, - 0.231583, 1.274057, 0.559219, 0.494821, - 0.239881, 1.262864, 0.554913, 0.521486, - 0.247336, 1.250633, 0.549953, 0.547884, - 0.253921, 1.237448, 0.544251, 0.574582, - 0.259099, 1.223164, 0.537120, 0.601342, - 0.262695, 1.208784, 0.528650, 0.627861, - 0.265337, 1.194424, 0.518978, 0.653745, - 0.266872, 1.179361, 0.508525, 0.679348, - 0.267403, 1.165010, 0.496705, 0.705068, - 0.266429, 1.151693, 0.482926, 0.730312, - 0.263829, 1.137584, 0.468519, 0.755576, - 0.260491, 1.125328, 0.452213, 0.780371, - 0.256166, 1.113759, 0.435127, 0.804632, - 0.250079, 1.101656, 0.416833, 0.828983, - 0.243181, 1.091235, 0.397009, 0.852585, - 0.235383, 1.081475, 0.376647, 0.875237, - 0.226031, 1.071806, 0.355506, 0.899152, - 0.216343, 1.064453, 0.333133, 0.922121, - 0.205772, 1.057161, 0.311073, 0.944523, - 0.193980, 1.050447, 0.287781, 0.967313, - 0.181920, 1.044531, 0.264350, 0.989042, - 0.168822, 1.039312, 0.241128, 1.010881, - 0.155350, 1.035298, 0.218138, 1.032368, - 0.141231, 1.032073, 0.195579, 1.052254, - 0.126521, 1.029395, 0.173399, 1.070207, - 0.111243, 1.026938, 0.151866, 1.086528, - 0.095617, 1.024957, 0.130711, 1.100670, - 0.079687, 1.021924, 0.108865, 1.112461, - 0.063593, 1.018281, 0.086760, 1.121588, - 0.047313, 1.013747, 0.064575, 1.128522, - 0.031385, 1.008433, 0.042499, 1.134759, - 0.015356, 1.003569, 0.020840, 1.141448, 0.000114, 0.999978, - 0.000056, 0.000200, - 0.000122, 1.372763, 0.611086, 0.000503, - 0.000308, 1.371456, 0.611084, 0.002016, - 0.001232, 1.373440, 0.611084, 0.004535, - 0.002771, 1.373387, 0.611083, 0.008061, - 0.004926, 1.372916, 0.611083, 0.012601, - 0.007700, 1.373956, 0.611084, 0.018142, - 0.011084, 1.373419, 0.611078, 0.024695, - 0.015087, 1.373492, 0.611074, 0.032255, - 0.019701, 1.373360, 0.611066, 0.040827, - 0.024930, 1.373327, 0.611055, 0.050408, - 0.030769, 1.373222, 0.611037, 0.061004, - 0.037217, 1.373079, 0.611014, 0.072613, - 0.044270, 1.372895, 0.610982, 0.085238, - 0.051923, 1.372624, 0.610941, 0.098878, - 0.060161, 1.372252, 0.610883, 0.113522, - 0.068785, 1.371785, 0.610798, 0.129176, - 0.077863, 1.371103, 0.610683, 0.145876, - 0.087593, 1.370541, 0.610537, 0.163570, - 0.097847, 1.369496, 0.610349, 0.182283, - 0.108592, 1.368477, 0.610109, 0.201930, - 0.119420, 1.366980, 0.609763, 0.222570, - 0.130789, 1.365375, 0.609343, 0.244123, - 0.142514, 1.363456, 0.608815, 0.266437, - 0.154232, 1.360916, 0.608114, 0.289467, - 0.166370, 1.357909, 0.607291, 0.312861, - 0.178505, 1.353588, 0.606272, 0.336736, - 0.190980, 1.349211, 0.605153, 0.361740, - 0.202859, 1.343319, 0.603548, 0.387878, - 0.213997, 1.335908, 0.601268, 0.414357, - 0.224584, 1.326676, 0.598499, 0.441442, - 0.234664, 1.317331, 0.595066, 0.468409, - 0.243875, 1.305818, 0.590996, 0.494999, - 0.252121, 1.291863, 0.586293, 0.521730, - 0.259714, 1.278212, 0.580840, 0.547894, - 0.266242, 1.262656, 0.574494, 0.573865, - 0.271578, 1.246364, 0.567007, 0.601124, - 0.275503, 1.231274, 0.557771, 0.627606, - 0.277954, 1.215252, 0.547255, 0.654004, - 0.279404, 1.199977, 0.535766, 0.679554, - 0.279632, 1.183995, 0.522792, 0.704280, - 0.278457, 1.167428, 0.508488, 0.729830, - 0.275706, 1.152760, 0.492425, 0.754376, - 0.271640, 1.137942, 0.475285, 0.779209, - 0.266911, 1.125222, 0.456679, 0.803562, - 0.260838, 1.112179, 0.437267, 0.827985, - 0.253353, 1.101439, 0.416227, 0.851737, - 0.245027, 1.089890, 0.394728, 0.874850, - 0.235719, 1.080018, 0.372244, 0.897680, - 0.225051, 1.070807, 0.348846, 0.921351, - 0.214051, 1.063180, 0.324961, 0.943818, - 0.202039, 1.056148, 0.300836, 0.966368, - 0.189134, 1.049277, 0.276333, 0.987426, - 0.175613, 1.042176, 0.251862, 1.010162, - 0.161473, 1.038567, 0.227217, 1.031224, - 0.146866, 1.034102, 0.203582, 1.052317, - 0.131644, 1.031600, 0.180629, 1.070879, - 0.115909, 1.028913, 0.158165, 1.087407, - 0.099638, 1.026193, 0.135905, 1.102159, - 0.083091, 1.023567, 0.113394, 1.114006, - 0.066178, 1.019567, 0.090325, 1.123374, - 0.049430, 1.014856, 0.067302, 1.130310, - 0.032557, 1.009141, 0.044264, 1.136334, - 0.016157, 1.003984, 0.021807, 1.142961, 0.000172, 0.999951, - 0.000077, 0.000200, - 0.000129, 1.416584, 0.645866, 0.000504, - 0.000326, 1.417762, 0.645865, 0.002016, - 0.001302, 1.417825, 0.645866, 0.004535, - 0.002929, 1.417142, 0.645865, 0.008062, - 0.005207, 1.416968, 0.645864, 0.012598, - 0.008136, 1.417109, 0.645862, 0.018141, - 0.011715, 1.417001, 0.645859, 0.024690, - 0.015941, 1.416878, 0.645853, 0.032257, - 0.020823, 1.417134, 0.645843, 0.040827, - 0.026347, 1.416983, 0.645829, 0.050411, - 0.032518, 1.416949, 0.645808, 0.061007, - 0.039330, 1.416694, 0.645781, 0.072621, - 0.046783, 1.416599, 0.645746, 0.085249, - 0.054865, 1.416241, 0.645695, 0.098897, - 0.063563, 1.415832, 0.645630, 0.113546, - 0.072607, 1.415264, 0.645529, 0.129220, - 0.082257, 1.414482, 0.645396, 0.145888, - 0.092515, 1.413626, 0.645268, 0.163659, - 0.103393, 1.412710, 0.645018, 0.182385, - 0.114684, 1.411418, 0.644739, 0.202078, - 0.126098, 1.409822, 0.644348, 0.222772, - 0.138145, 1.407948, 0.643872, 0.244370, - 0.150405, 1.405678, 0.643255, 0.266787, - 0.162798, 1.402763, 0.642463, 0.289844, - 0.175434, 1.398863, 0.641504, 0.313540, - 0.188158, 1.394695, 0.640346, 0.337489, - 0.201014, 1.389376, 0.639042, 0.362008, - 0.213719, 1.382439, 0.637412, 0.387990, - 0.225248, 1.373281, 0.634930, 0.414728, - 0.236348, 1.363729, 0.631861, 0.441635, - 0.246701, 1.352304, 0.628155, 0.468588, - 0.256167, 1.339162, 0.623625, 0.495337, - 0.264662, 1.323811, 0.618458, 0.521886, - 0.272207, 1.307630, 0.612373, 0.548355, - 0.278890, 1.291265, 0.605263, 0.574535, - 0.284442, 1.273752, 0.597048, 0.600870, - 0.288389, 1.256171, 0.587401, 0.627715, - 0.290816, 1.238447, 0.576001, 0.653830, - 0.291886, 1.221036, 0.563198, 0.679175, - 0.291629, 1.202283, 0.549249, 0.704539, - 0.290489, 1.185866, 0.533881, 0.729126, - 0.287529, 1.168822, 0.516966, 0.754297, - 0.283184, 1.152934, 0.498501, 0.778678, - 0.277732, 1.137821, 0.478728, 0.802473, - 0.271203, 1.123387, 0.457814, 0.826596, - 0.263494, 1.110573, 0.435865, 0.850835, - 0.254572, 1.099099, 0.412597, 0.874203, - 0.244815, 1.088403, 0.388995, 0.897271, - 0.233993, 1.078085, 0.364487, 0.919667, - 0.221934, 1.068543, 0.339344, 0.943001, - 0.209714, 1.061081, 0.313770, 0.965688, - 0.196367, 1.054023, 0.287928, 0.987598, - 0.182263, 1.047247, 0.262157, 1.009280, - 0.167775, 1.041376, 0.236855, 1.031762, - 0.152530, 1.037647, 0.211847, 1.051965, - 0.136809, 1.033396, 0.187546, 1.071699, - 0.120418, 1.031021, 0.164186, 1.088881, - 0.103618, 1.028403, 0.141184, 1.103482, - 0.086271, 1.024987, 0.117665, 1.115646, - 0.068973, 1.020884, 0.093896, 1.125258, - 0.051285, 1.015966, 0.069978, 1.132045, - 0.033998, 1.009990, 0.046126, 1.138004, - 0.016696, 1.004270, 0.022635, 1.144463, 0.000089, 0.999987, - 0.000016, 0.000200, - 0.000136, 1.463614, 0.681786, 0.000504, - 0.000344, 1.465345, 0.681785, 0.002015, - 0.001374, 1.464172, 0.681783, 0.004535, - 0.003092, 1.464846, 0.681784, 0.008062, - 0.005496, 1.464783, 0.681784, 0.012598, - 0.008588, 1.464883, 0.681781, 0.018141, - 0.012366, 1.464740, 0.681777, 0.024692, - 0.016829, 1.464665, 0.681770, 0.032258, - 0.021980, 1.464720, 0.681760, 0.040829, - 0.027811, 1.464625, 0.681742, 0.050415, - 0.034324, 1.464571, 0.681720, 0.061013, - 0.041513, 1.464346, 0.681688, 0.072628, - 0.049375, 1.464131, 0.681644, 0.085264, - 0.057903, 1.463847, 0.681588, 0.098918, - 0.067067, 1.463369, 0.681509, 0.113568, - 0.076570, 1.462549, 0.681389, 0.129265, - 0.086782, 1.461703, 0.681239, 0.145997, - 0.097637, 1.460840, 0.681047, 0.163751, - 0.109101, 1.459737, 0.680806, 0.182505, - 0.120922, 1.458231, 0.680480, 0.202241, - 0.133007, 1.456393, 0.680042, 0.222987, - 0.145693, 1.454258, 0.679503, 0.244638, - 0.158488, 1.451543, 0.678792, 0.267132, - 0.171585, 1.448115, 0.677907, 0.290365, - 0.184746, 1.443992, 0.676796, 0.314178, - 0.198101, 1.439271, 0.675498, 0.338289, - 0.211370, 1.432830, 0.673922, 0.362543, - 0.224489, 1.424163, 0.672151, 0.388470, - 0.236914, 1.415160, 0.669601, 0.415105, - 0.248342, 1.403811, 0.666255, 0.441925, - 0.258957, 1.390149, 0.662166, 0.468668, - 0.268556, 1.374104, 0.657229, 0.495720, - 0.277359, 1.358102, 0.651347, 0.522574, - 0.285078, 1.340754, 0.644598, 0.548981, - 0.291718, 1.322033, 0.636820, 0.574946, - 0.297087, 1.302148, 0.627812, 0.600744, - 0.301079, 1.282130, 0.617485, 0.627565, - 0.303566, 1.263339, 0.605047, 0.653598, - 0.304330, 1.242712, 0.591167, 0.679239, - 0.303820, 1.223212, 0.576025, 0.704043, - 0.302064, 1.203763, 0.559649, 0.728796, - 0.299095, 1.185434, 0.541271, 0.753581, - 0.294392, 1.167630, 0.521800, 0.778577, - 0.288603, 1.151930, 0.500628, 0.802550, - 0.281604, 1.136072, 0.478434, 0.825803, - 0.273472, 1.121673, 0.455384, 0.849768, - 0.264011, 1.108491, 0.430811, 0.873250, - 0.253653, 1.096550, 0.405524, 0.896725, - 0.242642, 1.085905, 0.380038, 0.919158, - 0.230191, 1.075091, 0.353482, 0.942236, - 0.217145, 1.066848, 0.326605, 0.965031, - 0.203555, 1.059310, 0.299842, 0.987048, - 0.188777, 1.051749, 0.272859, 1.008718, - 0.173613, 1.044999, 0.246040, 1.031097, - 0.157972, 1.040066, 0.219826, 1.052493, - 0.141589, 1.035951, 0.194278, 1.071773, - 0.124814, 1.032520, 0.169830, 1.089646, - 0.107321, 1.029803, 0.146135, 1.104932, - 0.089726, 1.026612, 0.122127, 1.117687, - 0.071433, 1.022391, 0.097461, 1.127188, - 0.053395, 1.017113, 0.072556, 1.134010, - 0.035151, 1.010934, 0.047749, 1.139746, - 0.017427, 1.004633, 0.023530, 1.146205, 0.000151, 1.000020, - 0.000106, 0.000200, - 0.000144, 1.517643, 0.718949, 0.000504, - 0.000362, 1.516387, 0.718947, 0.002016, - 0.001449, 1.516742, 0.718946, 0.004536, - 0.003261, 1.517196, 0.718946, 0.008063, - 0.005796, 1.516806, 0.718945, 0.012598, - 0.009057, 1.516986, 0.718943, 0.018140, - 0.013039, 1.516603, 0.718937, 0.024694, - 0.017747, 1.516739, 0.718929, 0.032260, - 0.023178, 1.516994, 0.718917, 0.040831, - 0.029325, 1.516649, 0.718896, 0.050419, - 0.036192, 1.516594, 0.718870, 0.061019, - 0.043770, 1.516327, 0.718833, 0.072638, - 0.052056, 1.516054, 0.718782, 0.085274, - 0.061039, 1.515628, 0.718714, 0.098938, - 0.070676, 1.515199, 0.718623, 0.113607, - 0.080679, 1.514222, 0.718483, 0.129329, - 0.091485, 1.513354, 0.718316, 0.146077, - 0.102931, 1.512301, 0.718096, 0.163856, - 0.114986, 1.510977, 0.717818, 0.182640, - 0.127305, 1.509225, 0.717432, 0.202432, - 0.140147, 1.507152, 0.716939, 0.223229, - 0.153468, 1.504780, 0.716331, 0.244943, - 0.166875, 1.501612, 0.715527, 0.267559, - 0.180658, 1.497898, 0.714523, 0.290926, - 0.194405, 1.493208, 0.713266, 0.314863, - 0.208302, 1.487388, 0.711758, 0.339053, - 0.222020, 1.479677, 0.709982, 0.363627, - 0.235683, 1.470950, 0.707958, 0.388887, - 0.248723, 1.459907, 0.705346, 0.415474, - 0.260563, 1.446579, 0.701644, 0.442065, - 0.271352, 1.429962, 0.697134, 0.469418, - 0.281541, 1.414343, 0.691665, 0.496419, - 0.290429, 1.395681, 0.685227, 0.523071, - 0.298032, 1.375347, 0.677815, 0.549641, - 0.304679, 1.354816, 0.669063, 0.575489, - 0.309902, 1.332505, 0.659071, 0.601108, - 0.313771, 1.309752, 0.647799, 0.627199, - 0.316225, 1.288381, 0.634856, 0.653243, - 0.316679, 1.265785, 0.619627, 0.678960, - 0.315816, 1.244333, 0.603244, 0.704055, - 0.313776, 1.223315, 0.585191, 0.728713, - 0.310417, 1.203142, 0.565969, 0.753301, - 0.305786, 1.184323, 0.545347, 0.777890, - 0.299262, 1.166070, 0.522753, 0.802354, - 0.291830, 1.149599, 0.499017, 0.826005, - 0.283281, 1.133655, 0.474335, 0.848920, - 0.273512, 1.118132, 0.449019, 0.872765, - 0.262525, 1.105606, 0.422329, 0.895950, - 0.250769, 1.093539, 0.395057, 0.918816, - 0.238257, 1.082388, 0.367709, 0.941089, - 0.224381, 1.072484, 0.339350, 0.964514, - 0.210289, 1.064054, 0.311239, 0.987128, - 0.195488, 1.056645, 0.283272, 1.009064, - 0.179491, 1.049549, 0.255163, 1.030163, - 0.163172, 1.042741, 0.227757, 1.052502, - 0.146457, 1.038270, 0.200970, 1.072971, - 0.129054, 1.035014, 0.175767, 1.091223, - 0.111285, 1.032231, 0.151118, 1.106518, - 0.092617, 1.028211, 0.126196, 1.119235, - 0.074168, 1.023686, 0.100828, 1.129311, - 0.055212, 1.018311, 0.075240, 1.135983, - 0.036571, 1.011485, 0.049558, 1.141648, - 0.017954, 1.004952, 0.024273, 1.147938, 0.000125, 1.000009, - 0.000048, 0.000199, - 0.000151, 1.566887, 0.757466, 0.000504, - 0.000382, 1.574111, 0.757466, 0.002016, - 0.001527, 1.573735, 0.757466, 0.004535, - 0.003435, 1.573737, 0.757466, 0.008062, - 0.006107, 1.573782, 0.757464, 0.012599, - 0.009542, 1.573796, 0.757460, 0.018142, - 0.013739, 1.573710, 0.757455, 0.024694, - 0.018697, 1.573562, 0.757446, 0.032259, - 0.024418, 1.573667, 0.757429, 0.040834, - 0.030895, 1.573555, 0.757407, 0.050422, - 0.038127, 1.573383, 0.757376, 0.061025, - 0.046108, 1.573086, 0.757332, 0.072650, - 0.054835, 1.572833, 0.757274, 0.085296, - 0.064294, 1.572395, 0.757195, 0.098962, - 0.074376, 1.571729, 0.757087, 0.113649, - 0.084955, 1.570571, 0.756925, 0.129389, - 0.096334, 1.569582, 0.756729, 0.146167, - 0.108406, 1.568444, 0.756481, 0.163973, - 0.121056, 1.566905, 0.756158, 0.182798, - 0.133970, 1.564939, 0.755715, 0.202650, - 0.147522, 1.562666, 0.755167, 0.223502, - 0.161466, 1.559877, 0.754465, 0.245269, - 0.175539, 1.556008, 0.753552, 0.268010, - 0.189957, 1.552013, 0.752420, 0.291474, - 0.204361, 1.546509, 0.751008, 0.315527, - 0.218714, 1.539575, 0.749266, 0.339954, - 0.233029, 1.530968, 0.747232, 0.364649, - 0.247149, 1.520994, 0.744906, 0.389520, - 0.260672, 1.507748, 0.742123, 0.415717, - 0.272873, 1.491777, 0.738187, 0.442862, - 0.284317, 1.475658, 0.733189, 0.469939, - 0.294552, 1.456572, 0.727165, 0.496916, - 0.303517, 1.435237, 0.720043, 0.523480, - 0.311061, 1.412192, 0.711640, 0.550092, - 0.317596, 1.389033, 0.702174, 0.576384, - 0.322921, 1.365086, 0.691225, 0.602280, - 0.326806, 1.341317, 0.678841, 0.627676, - 0.329057, 1.316518, 0.664815, 0.653458, - 0.329372, 1.291877, 0.648548, 0.679227, - 0.328067, 1.268126, 0.630676, 0.704476, - 0.325585, 1.244424, 0.611585, 0.729232, - 0.321775, 1.223010, 0.590803, 0.753405, - 0.316713, 1.201297, 0.568653, 0.777274, - 0.309858, 1.181071, 0.544763, 0.801882, - 0.301866, 1.162826, 0.519747, 0.826030, - 0.292861, 1.145704, 0.493531, 0.849359, - 0.282794, 1.129629, 0.466900, 0.871837, - 0.271197, 1.114155, 0.439230, 0.895896, - 0.258954, 1.102334, 0.410570, 0.918951, - 0.245878, 1.090163, 0.381314, 0.941148, - 0.231897, 1.078738, 0.352268, 0.963464, - 0.216743, 1.068862, 0.322688, 0.986628, - 0.201486, 1.061077, 0.293523, 1.009289, - 0.185521, 1.053561, 0.264125, 1.030659, - 0.168429, 1.046627, 0.235706, 1.052382, - 0.151210, 1.040953, 0.208022, 1.073476, - 0.133289, 1.036534, 0.181245, 1.092237, - 0.114768, 1.033580, 0.155661, 1.108200, - 0.095917, 1.029997, 0.130223, 1.121435, - 0.076492, 1.025374, 0.104098, 1.131382, - 0.057204, 1.019485, 0.077776, 1.137994, - 0.037747, 1.012188, 0.051250, 1.143441, - 0.018673, 1.005309, 0.025245, 1.149714, 0.000216, 1.000004, - 0.000120, 0.000200, - 0.000159, 1.633988, 0.797469, 0.000504, - 0.000402, 1.636076, 0.797469, 0.002016, - 0.001607, 1.635679, 0.797467, 0.004535, - 0.003617, 1.636040, 0.797468, 0.008063, - 0.006430, 1.636159, 0.797467, 0.012599, - 0.010046, 1.636128, 0.797462, 0.018141, - 0.014464, 1.635730, 0.797457, 0.024696, - 0.019685, 1.635836, 0.797445, 0.032259, - 0.025705, 1.635719, 0.797426, 0.040835, - 0.032523, 1.635610, 0.797401, 0.050425, - 0.040135, 1.635460, 0.797363, 0.061033, - 0.048536, 1.635182, 0.797313, 0.072661, - 0.057718, 1.634817, 0.797243, 0.085315, - 0.067666, 1.634314, 0.797150, 0.098985, - 0.078179, 1.633350, 0.797016, 0.113699, - 0.089383, 1.632253, 0.796839, 0.129456, - 0.101364, 1.631025, 0.796623, 0.146275, - 0.114081, 1.629867, 0.796331, 0.164108, - 0.127318, 1.628043, 0.795956, 0.182983, - 0.140901, 1.625813, 0.795458, 0.202891, - 0.155174, 1.623149, 0.794834, 0.223787, - 0.169654, 1.619686, 0.794015, 0.245678, - 0.184540, 1.615694, 0.793013, 0.268495, - 0.199543, 1.610812, 0.791727, 0.292093, - 0.214639, 1.604629, 0.790107, 0.316184, - 0.229499, 1.596061, 0.788154, 0.340986, - 0.244407, 1.587195, 0.785797, 0.365808, - 0.258907, 1.575031, 0.783093, 0.390528, - 0.272746, 1.559448, 0.779970, 0.416510, - 0.285845, 1.543294, 0.775852, 0.443443, - 0.297404, 1.523476, 0.770323, 0.470442, - 0.307757, 1.501515, 0.763721, 0.497499, - 0.316846, 1.477841, 0.755889, 0.524316, - 0.324561, 1.452427, 0.746662, 0.551212, - 0.331060, 1.427421, 0.736004, 0.577323, - 0.335956, 1.400369, 0.723810, 0.602976, - 0.339501, 1.373093, 0.710184, 0.628357, - 0.341577, 1.345853, 0.695017, 0.653642, - 0.342031, 1.319040, 0.677972, 0.679440, - 0.340342, 1.292490, 0.658877, 0.704744, - 0.337356, 1.267182, 0.638085, 0.729692, - 0.333042, 1.243280, 0.615615, 0.753920, - 0.327504, 1.219751, 0.592054, 0.777695, - 0.320537, 1.197796, 0.566967, 0.801426, - 0.311880, 1.176872, 0.540643, 0.825649, - 0.302211, 1.158160, 0.512906, 0.849282, - 0.291665, 1.141257, 0.484587, 0.872341, - 0.280050, 1.125469, 0.455556, 0.895110, - 0.266978, 1.110222, 0.425652, 0.918841, - 0.253326, 1.097419, 0.395015, 0.941209, - 0.238899, 1.086101, 0.364948, 0.963142, - 0.223523, 1.075023, 0.334151, 0.985996, - 0.207346, 1.065628, 0.303708, 1.008718, - 0.190889, 1.057256, 0.273008, 1.030554, - 0.173517, 1.049720, 0.243221, 1.053085, - 0.155645, 1.043837, 0.214426, 1.074267, - 0.137472, 1.039312, 0.187036, 1.093591, - 0.118385, 1.035457, 0.160512, 1.109850, - 0.098883, 1.031630, 0.134384, 1.123516, - 0.079050, 1.026762, 0.107424, 1.133578, - 0.058977, 1.020640, 0.080317, 1.140289, - 0.039013, 1.013096, 0.052944, 1.145610, - 0.019228, 1.005694, 0.025989, 1.151704, 0.000105, 0.999981, - 0.000019, 0.000200, - 0.000168, 1.704841, 0.839096, 0.000504, - 0.000423, 1.704242, 0.839097, 0.002016, - 0.001691, 1.703821, 0.839091, 0.004534, - 0.003805, 1.703804, 0.839094, 0.008063, - 0.006765, 1.704224, 0.839092, 0.012598, - 0.010570, 1.704013, 0.839087, 0.018142, - 0.015219, 1.703889, 0.839079, 0.024697, - 0.020712, 1.704023, 0.839066, 0.032261, - 0.027046, 1.703836, 0.839045, 0.040837, - 0.034218, 1.703608, 0.839014, 0.050429, - 0.042224, 1.703414, 0.838972, 0.061041, - 0.051061, 1.703148, 0.838912, 0.072676, - 0.060717, 1.702744, 0.838831, 0.085340, - 0.071175, 1.702223, 0.838724, 0.099023, - 0.082182, 1.700984, 0.838567, 0.113759, - 0.094007, 1.699764, 0.838367, 0.129546, - 0.106621, 1.698462, 0.838112, 0.146382, - 0.119956, 1.696938, 0.837782, 0.164260, - 0.133760, 1.694868, 0.837346, 0.183188, - 0.148108, 1.692262, 0.836780, 0.203158, - 0.163075, 1.689251, 0.836073, 0.224147, - 0.178255, 1.685408, 0.835148, 0.246147, - 0.193900, 1.680946, 0.833992, 0.269072, - 0.209553, 1.675277, 0.832546, 0.292718, - 0.225226, 1.667626, 0.830727, 0.317159, - 0.240836, 1.658952, 0.828510, 0.341979, - 0.256103, 1.647624, 0.825843, 0.366844, - 0.270887, 1.633014, 0.822760, 0.392043, - 0.285324, 1.617191, 0.819159, 0.417356, - 0.298817, 1.597501, 0.814788, 0.444093, - 0.310711, 1.575184, 0.808751, 0.471379, - 0.321410, 1.551590, 0.801294, 0.498267, - 0.330421, 1.524134, 0.792711, 0.525401, - 0.338331, 1.496672, 0.782480, 0.551846, - 0.344430, 1.467062, 0.770659, 0.578009, - 0.349047, 1.436943, 0.757348, 0.604054, - 0.352490, 1.407611, 0.742541, 0.629387, - 0.354158, 1.377441, 0.726071, 0.654435, - 0.354422, 1.347651, 0.707524, 0.679845, - 0.352663, 1.318769, 0.687067, 0.704892, - 0.348994, 1.290600, 0.664637, 0.729763, - 0.344105, 1.263997, 0.640663, 0.754345, - 0.338129, 1.239273, 0.615484, 0.778629, - 0.330905, 1.215858, 0.589210, 0.801939, - 0.322113, 1.192318, 0.561550, 0.825723, - 0.311673, 1.171380, 0.532175, 0.849387, - 0.300410, 1.152991, 0.502055, 0.872792, - 0.288328, 1.136139, 0.471308, 0.895083, - 0.275087, 1.119534, 0.440427, 0.918335, - 0.260700, 1.105542, 0.409260, 0.941577, - 0.245717, 1.093070, 0.377142, 0.963992, - 0.230079, 1.081207, 0.345289, 0.986510, - 0.213523, 1.071488, 0.313508, 1.008806, - 0.196157, 1.062011, 0.281962, 1.030724, - 0.178467, 1.053240, 0.251177, 1.053782, - 0.160291, 1.047057, 0.220986, 1.075451, - 0.141308, 1.041842, 0.192256, 1.094947, - 0.121975, 1.037704, 0.165023, 1.111783, - 0.101744, 1.033300, 0.138228, 1.125525, - 0.081476, 1.028234, 0.110679, 1.135873, - 0.060770, 1.021695, 0.082672, 1.142478, - 0.040207, 1.013838, 0.054506, 1.147889, - 0.019908, 1.006166, 0.026938, 1.153852, 0.000204, 0.999983, - 0.000123, 0.000199, - 0.000176, 1.771601, 0.882501, 0.000504, - 0.000445, 1.779195, 0.882504, 0.002016, - 0.001779, 1.779635, 0.882498, 0.004536, - 0.004003, 1.779586, 0.882499, 0.008062, - 0.007115, 1.778613, 0.882496, 0.012598, - 0.011116, 1.778678, 0.882492, 0.018142, - 0.016005, 1.778531, 0.882481, 0.024696, - 0.021782, 1.778556, 0.882466, 0.032262, - 0.028444, 1.778507, 0.882442, 0.040842, - 0.035987, 1.778385, 0.882408, 0.050436, - 0.044404, 1.778034, 0.882364, 0.061053, - 0.053695, 1.777761, 0.882287, 0.072692, - 0.063842, 1.777256, 0.882190, 0.085364, - 0.074821, 1.776518, 0.882067, 0.099064, - 0.086368, 1.775080, 0.881884, 0.113828, - 0.098805, 1.773836, 0.881657, 0.129649, - 0.112090, 1.772370, 0.881361, 0.146518, - 0.126067, 1.770594, 0.880982, 0.164440, - 0.140493, 1.768089, 0.880484, 0.183437, - 0.155646, 1.765301, 0.879843, 0.203468, - 0.171266, 1.761698, 0.879035, 0.224562, - 0.187231, 1.757518, 0.877982, 0.246665, - 0.203540, 1.752318, 0.876667, 0.269652, - 0.219916, 1.745356, 0.875028, 0.293531, - 0.236255, 1.737186, 0.872977, 0.318048, - 0.252410, 1.726709, 0.870448, 0.342963, - 0.268192, 1.713109, 0.867400, 0.368336, - 0.283587, 1.698087, 0.863882, 0.393512, - 0.298186, 1.678638, 0.859724, 0.418602, - 0.311882, 1.655604, 0.854835, 0.445080, - 0.324500, 1.632250, 0.848353, 0.472289, - 0.335295, 1.605069, 0.840218, 0.499128, - 0.344256, 1.573846, 0.830556, 0.525834, - 0.351716, 1.541120, 0.819269, 0.553177, - 0.358241, 1.511385, 0.806222, 0.579480, - 0.362640, 1.477866, 0.791647, 0.605205, - 0.365513, 1.444218, 0.775398, 0.630617, - 0.366822, 1.410954, 0.757144, 0.655730, - 0.366785, 1.379010, 0.737323, 0.680529, - 0.364904, 1.347280, 0.715601, 0.705800, - 0.360990, 1.316416, 0.691547, 0.730550, - 0.355397, 1.286344, 0.666141, 0.754970, - 0.348664, 1.258954, 0.638929, 0.779042, - 0.340774, 1.232965, 0.611015, 0.802839, - 0.331767, 1.209775, 0.581877, 0.825793, - 0.321054, 1.185813, 0.551509, 0.849512, - 0.309016, 1.165080, 0.519698, 0.873120, - 0.296369, 1.147091, 0.487506, 0.895942, - 0.282704, 1.129658, 0.455320, 0.917996, - 0.268007, 1.113463, 0.422605, 0.941281, - 0.252329, 1.100040, 0.389347, 0.964584, - 0.236203, 1.087973, 0.356430, 0.986371, - 0.219209, 1.075983, 0.323089, 1.009522, - 0.201588, 1.066940, 0.290806, 1.031976, - 0.183296, 1.057999, 0.258682, 1.053461, - 0.164509, 1.049542, 0.227722, 1.076121, - 0.145165, 1.043718, 0.197439, 1.096597, - 0.125199, 1.039607, 0.169578, 1.113908, - 0.104921, 1.035528, 0.142222, 1.127939, - 0.083623, 1.029807, 0.113802, 1.138391, - 0.062589, 1.023312, 0.085164, 1.145110, - 0.041376, 1.014806, 0.056186, 1.150141, - 0.020433, 1.006501, 0.027654, 1.156069, 0.000097, 0.999949, - 0.000046, 0.000200, - 0.000185, 1.858268, 0.927857, 0.000504, - 0.000468, 1.861583, 0.927859, 0.002016, - 0.001870, 1.860659, 0.927855, 0.004535, - 0.004208, 1.860963, 0.927867, 0.008063, - 0.007480, 1.860766, 0.927855, 0.012594, - 0.011683, 1.859996, 0.927851, 0.018142, - 0.016828, 1.860739, 0.927839, 0.024698, - 0.022901, 1.860763, 0.927818, 0.032263, - 0.029903, 1.860501, 0.927791, 0.040846, - 0.037834, 1.860431, 0.927751, 0.050440, - 0.046680, 1.859827, 0.927690, 0.061066, - 0.056446, 1.859624, 0.927610, 0.072713, - 0.067109, 1.859039, 0.927505, 0.085393, - 0.078613, 1.858144, 0.927357, 0.099120, - 0.090747, 1.856618, 0.927145, 0.113910, - 0.103850, 1.855221, 0.926884, 0.129755, - 0.117777, 1.853470, 0.926546, 0.146669, - 0.132441, 1.851413, 0.926104, 0.164648, - 0.147565, 1.848498, 0.925530, 0.183708, - 0.163470, 1.845281, 0.924802, 0.203832, - 0.179763, 1.841273, 0.923871, 0.225029, - 0.196564, 1.836481, 0.922691, 0.247221, - 0.213537, 1.830273, 0.921198, 0.270343, - 0.230662, 1.822374, 0.919320, 0.294399, - 0.247740, 1.812975, 0.917008, 0.319040, - 0.264448, 1.800693, 0.914141, 0.344269, - 0.280831, 1.785923, 0.910707, 0.369625, - 0.296478, 1.767203, 0.906585, 0.394925, - 0.311287, 1.744434, 0.901918, 0.420583, - 0.325578, 1.720938, 0.896240, 0.446200, - 0.338384, 1.693005, 0.889335, 0.472969, - 0.349187, 1.660901, 0.880394, 0.500490, - 0.358687, 1.628806, 0.869705, 0.527312, - 0.366042, 1.593001, 0.857145, 0.554207, - 0.372045, 1.557046, 0.842943, 0.580620, - 0.376134, 1.520192, 0.826837, 0.606480, - 0.378636, 1.482947, 0.808891, 0.631815, - 0.379414, 1.445954, 0.789119, 0.657021, - 0.378972, 1.410833, 0.767564, 0.681686, - 0.376728, 1.376575, 0.744338, 0.706498, - 0.372844, 1.342935, 0.718799, 0.731258, - 0.366649, 1.311052, 0.691756, 0.755937, - 0.359354, 1.280478, 0.662683, 0.779259, - 0.350487, 1.250585, 0.632892, 0.803295, - 0.340941, 1.225722, 0.602160, 0.826570, - 0.330174, 1.201003, 0.570520, 0.849954, - 0.317854, 1.178488, 0.537651, 0.873696, - 0.304426, 1.158302, 0.503799, 0.896695, - 0.290120, 1.139886, 0.469645, 0.919149, - 0.275106, 1.122884, 0.435625, 0.942121, - 0.259282, 1.107691, 0.401228, 0.964627, - 0.242123, 1.093661, 0.367086, 0.986614, - 0.224575, 1.081580, 0.332885, 1.009623, - 0.206837, 1.071375, 0.299209, 1.033126, - 0.188092, 1.062241, 0.266187, 1.054954, - 0.168637, 1.052912, 0.233733, 1.077660, - 0.149166, 1.047047, 0.203192, 1.097983, - 0.128587, 1.041607, 0.173918, 1.115586, - 0.107339, 1.036850, 0.145531, 1.130170, - 0.086203, 1.031427, 0.116890, 1.141018, - 0.064171, 1.024395, 0.087388, 1.147681, - 0.042530, 1.015719, 0.057733, 1.152560, - 0.021011, 1.006883, 0.028413, 1.158406, 0.000158, 0.999897, - 0.000106, 0.000200, - 0.000195, 1.950982, 0.975366, 0.000504, - 0.000491, 1.950207, 0.975365, 0.002015, - 0.001966, 1.950675, 0.975362, 0.004535, - 0.004423, 1.951281, 0.975370, 0.008062, - 0.007863, 1.951045, 0.975362, 0.012597, - 0.012285, 1.951199, 0.975356, 0.018145, - 0.017692, 1.951528, 0.975340, 0.024699, - 0.024074, 1.951194, 0.975321, 0.032266, - 0.031434, 1.950865, 0.975288, 0.040853, - 0.039771, 1.951038, 0.975244, 0.050452, - 0.049067, 1.950336, 0.975173, 0.061077, - 0.059324, 1.949805, 0.975078, 0.072736, - 0.070526, 1.949133, 0.974951, 0.085431, - 0.082528, 1.947947, 0.974777, 0.099182, - 0.095345, 1.946337, 0.974540, 0.113999, - 0.109118, 1.944725, 0.974241, 0.129888, - 0.123741, 1.942857, 0.973852, 0.146842, - 0.139071, 1.940251, 0.973342, 0.164890, - 0.154986, 1.937086, 0.972684, 0.184025, - 0.171661, 1.933404, 0.971856, 0.204245, - 0.188672, 1.928770, 0.970785, 0.225528, - 0.206252, 1.923041, 0.969448, 0.247841, - 0.223972, 1.915788, 0.967742, 0.271157, - 0.241827, 1.907008, 0.965607, 0.295297, - 0.259562, 1.895854, 0.963007, 0.320121, - 0.276909, 1.881289, 0.959722, 0.345566, - 0.293883, 1.864528, 0.955831, 0.371012, - 0.309816, 1.842062, 0.951127, 0.396834, - 0.325157, 1.818068, 0.945725, 0.422277, - 0.339357, 1.788874, 0.939318, 0.447928, - 0.352387, 1.758283, 0.931470, 0.474315, - 0.363680, 1.723668, 0.921900, 0.501560, - 0.372963, 1.686081, 0.909996, 0.528391, - 0.380159, 1.645816, 0.896244, 0.554754, - 0.385545, 1.603709, 0.880326, 0.581888, - 0.389778, 1.565475, 0.862716, 0.607791, - 0.391839, 1.524196, 0.843146, 0.633511, - 0.392331, 1.483921, 0.821554, 0.658621, - 0.391193, 1.445013, 0.798336, 0.683160, - 0.388424, 1.406963, 0.773299, 0.707429, - 0.384104, 1.370996, 0.746668, 0.732212, - 0.377945, 1.335879, 0.717502, 0.756871, - 0.369856, 1.302489, 0.686954, 0.781065, - 0.360707, 1.271815, 0.655372, 0.804167, - 0.350091, 1.242416, 0.622683, 0.827948, - 0.338941, 1.217208, 0.589185, 0.850901, - 0.326427, 1.192354, 0.555005, 0.873589, - 0.312199, 1.169639, 0.519594, 0.897085, - 0.297374, 1.150181, 0.484105, 0.920459, - 0.281932, 1.132858, 0.448661, 0.942637, - 0.265625, 1.115401, 0.413051, 0.965341, - 0.248332, 1.101078, 0.377329, 0.987530, - 0.229983, 1.087377, 0.342349, 1.010739, - 0.211647, 1.076582, 0.307824, 1.033449, - 0.192725, 1.065900, 0.273368, 1.055618, - 0.172726, 1.056958, 0.240238, 1.079345, - 0.152640, 1.049620, 0.208322, 1.100058, - 0.131931, 1.044084, 0.178242, 1.118547, - 0.110351, 1.039387, 0.149493, 1.132748, - 0.088128, 1.033049, 0.119673, 1.143419, - 0.066069, 1.025521, 0.089728, 1.150316, - 0.043513, 1.016378, 0.059253, 1.155208, - 0.021593, 1.007506, 0.029140, 1.160871, 0.000111, 0.999916, - 0.000035, 0.000201, - 0.000206, 2.061000, 1.025243, 0.000504, - 0.000516, 2.049647, 1.025237, 0.002015, - 0.002066, 2.050169, 1.025237, 0.004535, - 0.004650, 2.051254, 1.025255, 0.008063, - 0.008266, 2.051302, 1.025236, 0.012600, - 0.012915, 2.051508, 1.025226, 0.018144, - 0.018594, 2.050981, 1.025215, 0.024700, - 0.025304, 2.050841, 1.025190, 0.032267, - 0.033038, 2.050537, 1.025152, 0.040852, - 0.041795, 2.050660, 1.025090, 0.050460, - 0.051570, 2.049921, 1.025017, 0.061094, - 0.062347, 2.049350, 1.024908, 0.072762, - 0.074111, 2.048517, 1.024760, 0.085475, - 0.086661, 2.047009, 1.024555, 0.099249, - 0.100160, 2.045261, 1.024278, 0.114106, - 0.114628, 2.043508, 1.023941, 0.130032, - 0.130002, 2.041321, 1.023488, 0.147050, - 0.145985, 2.038299, 1.022905, 0.165164, - 0.162762, 2.034658, 1.022151, 0.184380, - 0.180172, 2.030312, 1.021200, 0.204704, - 0.198022, 2.024944, 1.019966, 0.226129, - 0.216359, 2.018546, 1.018424, 0.248582, - 0.234923, 2.010153, 1.016519, 0.272011, - 0.253474, 1.999659, 1.014072, 0.296259, - 0.271820, 1.986076, 1.011071, 0.321423, - 0.289959, 1.970618, 1.007389, 0.346897, - 0.307283, 1.949667, 1.002955, 0.372750, - 0.323817, 1.925287, 0.997633, 0.398603, - 0.339241, 1.896006, 0.991354, 0.424351, - 0.353633, 1.863658, 0.983937, 0.449887, - 0.366660, 1.827430, 0.975254, 0.475715, - 0.378213, 1.789521, 0.964753, 0.502204, - 0.387133, 1.745632, 0.951594, 0.530179, - 0.394976, 1.705347, 0.936344, 0.556732, - 0.400134, 1.658928, 0.918907, 0.583123, - 0.403439, 1.613077, 0.899504, 0.609477, - 0.405285, 1.567884, 0.878172, 0.634927, - 0.405055, 1.523507, 0.854396, 0.660357, - 0.403494, 1.481712, 0.829259, 0.684851, - 0.400104, 1.439000, 0.802359, 0.709654, - 0.395536, 1.400956, 0.773534, 0.733472, - 0.388996, 1.362156, 0.743230, 0.757502, - 0.380263, 1.325113, 0.711090, 0.782249, - 0.370594, 1.292913, 0.677166, 0.806017, - 0.359509, 1.262088, 0.642527, 0.828687, - 0.347126, 1.232059, 0.607589, 0.852372, - 0.334474, 1.207160, 0.571938, 0.874266, - 0.320074, 1.181978, 0.535518, 0.898168, - 0.304719, 1.161156, 0.498375, 0.920456, - 0.288246, 1.140667, 0.461179, 0.942832, - 0.271311, 1.122780, 0.424533, 0.966458, - 0.254154, 1.108743, 0.387784, 0.988907, - 0.235659, 1.093872, 0.351689, 1.011557, - 0.216322, 1.081959, 0.315743, 1.035099, - 0.197007, 1.070885, 0.280402, 1.056354, - 0.176878, 1.059968, 0.246472, 1.079854, - 0.156058, 1.051815, 0.212818, 1.101494, - 0.134772, 1.045757, 0.182143, 1.120587, - 0.113071, 1.041169, 0.152867, 1.135399, - 0.090411, 1.034844, 0.122796, 1.146612, - 0.067477, 1.026974, 0.091888, 1.153168, - 0.044849, 1.017303, 0.060779, 1.157912, - 0.021998, 1.007735, 0.029919, 1.163607, 0.000121, 0.999959, 0.000003, 0.000200, - 0.000216, 2.163956, 1.077737, 0.000504, - 0.000543, 2.161128, 1.077732, 0.002016, - 0.002173, 2.162732, 1.077729, 0.004535, - 0.004887, 2.161402, 1.077749, 0.008066, - 0.008692, 2.163252, 1.077732, 0.012599, - 0.013576, 2.161300, 1.077727, 0.018145, - 0.019546, 2.161151, 1.077702, 0.024702, - 0.026599, 2.161223, 1.077675, 0.032272, - 0.034729, 2.160949, 1.077632, 0.040862, - 0.043936, 2.160967, 1.077575, 0.050470, - 0.054203, 2.160035, 1.077473, 0.061113, - 0.065528, 2.159490, 1.077348, 0.072794, - 0.077882, 2.158517, 1.077178, 0.085528, - 0.091030, 2.156605, 1.076937, 0.099337, - 0.105251, 2.154828, 1.076631, 0.114228, - 0.120456, 2.152812, 1.076229, 0.130202, - 0.136573, 2.150298, 1.075713, 0.147284, - 0.153306, 2.146752, 1.075031, 0.165480, - 0.170931, 2.142744, 1.074173, 0.184793, - 0.189083, 2.137475, 1.073063, 0.205224, - 0.207840, 2.131320, 1.071683, 0.226743, - 0.226939, 2.123154, 1.069914, 0.249401, - 0.246344, 2.114086, 1.067718, 0.272955, - 0.265640, 2.101599, 1.064924, 0.297494, - 0.284846, 2.086612, 1.061512, 0.322731, - 0.303452, 2.067356, 1.057359, 0.348451, - 0.321330, 2.043711, 1.052294, 0.374451, - 0.338201, 2.015033, 1.046153, 0.400454, - 0.353816, 1.981139, 1.039003, 0.426434, - 0.368216, 1.944128, 1.030498, 0.452088, - 0.381251, 1.903094, 1.020454, 0.477901, - 0.392833, 1.860402, 1.008793, 0.504173, - 0.402408, 1.814402, 0.994791, 0.531520, - 0.409545, 1.766273, 0.977733, 0.558049, - 0.414351, 1.714119, 0.958625, 0.584778, - 0.417437, 1.664612, 0.937189, 0.610808, - 0.418519, 1.613793, 0.913543, 0.636915, - 0.418094, 1.565942, 0.888137, 0.662204, - 0.415742, 1.518783, 0.860728, 0.686848, - 0.411746, 1.473306, 0.831793, 0.710992, - 0.406153, 1.430153, 0.800862, 0.735382, - 0.399519, 1.389824, 0.768768, 0.759079, - 0.390927, 1.350744, 0.734825, 0.782912, - 0.380111, 1.313559, 0.699450, 0.806746, - 0.368383, 1.280028, 0.663191, 0.830269, - 0.355606, 1.249814, 0.625927, 0.853305, - 0.341988, 1.221138, 0.588644, 0.876326, - 0.327545, 1.195837, 0.550849, 0.898322, - 0.311779, 1.171844, 0.512694, 0.921811, - 0.294944, 1.150671, 0.474225, 0.944563, - 0.277333, 1.132224, 0.435772, 0.967089, - 0.259340, 1.115422, 0.398001, 0.989754, - 0.240836, 1.100405, 0.360802, 1.012470, - 0.221293, 1.086533, 0.323566, 1.036426, - 0.201191, 1.075496, 0.287387, 1.058709, - 0.180590, 1.064233, 0.252184, 1.081593, - 0.159810, 1.055296, 0.218441, 1.103146, - 0.137772, 1.047978, 0.186223, 1.122814, - 0.115347, 1.042693, 0.156019, 1.137790, - 0.092582, 1.036049, 0.125579, 1.149184, - 0.069152, 1.027944, 0.093986, 1.156062, - 0.045661, 1.018039, 0.062122, 1.160733, - 0.022719, 1.008072, 0.030650, 1.166487, 0.000231, 1.000063, - 0.000120, 0.000201, - 0.000228, 2.308308, 1.133128, 0.000504, - 0.000571, 2.283756, 1.133123, 0.002016, - 0.002284, 2.283756, 1.133123, 0.004535, - 0.005138, 2.283310, 1.133144, 0.008048, - 0.009119, 2.266192, 1.133138, 0.012600, - 0.014274, 2.284377, 1.133110, 0.018147, - 0.020553, 2.284204, 1.133093, 0.024702, - 0.027964, 2.283517, 1.133060, 0.032272, - 0.036510, 2.282997, 1.133007, 0.040866, - 0.046188, 2.282986, 1.132930, 0.050481, - 0.056979, 2.282260, 1.132824, 0.061133, - 0.068881, 2.281533, 1.132678, 0.072830, - 0.081850, 2.280504, 1.132481, 0.085592, - 0.095657, 2.278304, 1.132202, 0.099431, - 0.110594, 2.276269, 1.131845, 0.114360, - 0.126590, 2.273890, 1.131383, 0.130388, - 0.143454, 2.270761, 1.130784, 0.147547, - 0.161029, 2.266794, 1.130003, 0.165836, - 0.179523, 2.262332, 1.129016, 0.185269, - 0.198527, 2.256326, 1.127738, 0.205822, - 0.218138, 2.249031, 1.126156, 0.227527, - 0.238141, 2.239993, 1.124132, 0.250325, - 0.258302, 2.228878, 1.121594, 0.274070, - 0.278329, 2.214204, 1.118449, 0.298793, - 0.298310, 2.196654, 1.114528, 0.324131, - 0.317462, 2.173394, 1.109783, 0.350101, - 0.335853, 2.146395, 1.103901, 0.376293, - 0.353064, 2.112341, 1.096954, 0.402547, - 0.368950, 2.073700, 1.088642, 0.428791, - 0.383462, 2.031152, 1.078946, 0.454976, - 0.396635, 1.986661, 1.067536, 0.480566, - 0.407873, 1.937038, 1.054403, 0.506154, - 0.417303, 1.885155, 1.038894, 0.532862, - 0.424194, 1.830369, 1.020535, 0.560354, - 0.429344, 1.776976, 0.999295, 0.587114, - 0.431949, 1.721214, 0.975990, 0.613345, - 0.432547, 1.665739, 0.950239, 0.639335, - 0.431338, 1.612200, 0.922467, 0.664996, - 0.428473, 1.561035, 0.892593, 0.688947, - 0.423355, 1.508240, 0.861325, 0.713403, - 0.417235, 1.461776, 0.828289, 0.737649, - 0.409848, 1.418888, 0.793863, 0.761275, - 0.400901, 1.376807, 0.758074, 0.784778, - 0.390174, 1.337204, 0.721974, 0.808762, - 0.377683, 1.301527, 0.682718, 0.831993, - 0.364037, 1.267144, 0.644001, 0.854696, - 0.349494, 1.236023, 0.605478, 0.877933, - 0.334499, 1.209284, 0.565588, 0.900180, - 0.318435, 1.183967, 0.526138, 0.923039, - 0.301669, 1.161513, 0.486524, 0.945895, - 0.283298, 1.140838, 0.446747, 0.968069, - 0.264438, 1.122475, 0.408041, 0.991179, - 0.245463, 1.106968, 0.369477, 1.012926, - 0.225680, 1.091435, 0.331626, 1.036995, - 0.205401, 1.079561, 0.294288, 1.060909, - 0.184310, 1.068215, 0.257696, 1.083531, - 0.162846, 1.058133, 0.223343, 1.105644, - 0.141040, 1.050851, 0.190541, 1.125691, - 0.117965, 1.045001, 0.159310, 1.141297, - 0.094377, 1.038028, 0.128238, 1.152672, - 0.070831, 1.029694, 0.096282, 1.159333, - 0.046853, 1.019136, 0.063720, 1.163819, - 0.022991, 1.008518, 0.031234, 1.169564, 0.000125, 1.000069, - 0.000024, 0.000202, - 0.000241, 2.458341, 1.191742, 0.000504, - 0.000600, 2.418738, 1.191740, 0.002015, - 0.002401, 2.418821, 1.191730, 0.004535, - 0.005405, 2.421986, 1.191756, 0.008071, - 0.009618, 2.424988, 1.191753, 0.012600, - 0.015012, 2.420242, 1.191727, 0.018145, - 0.021612, 2.419937, 1.191703, 0.024704, - 0.029410, 2.419746, 1.191662, 0.032278, - 0.038398, 2.419409, 1.191604, 0.040874, - 0.048574, 2.418995, 1.191515, 0.050496, - 0.059920, 2.418190, 1.191389, 0.061160, - 0.072432, 2.417487, 1.191221, 0.072871, - 0.086009, 2.415853, 1.190984, 0.085664, - 0.100559, 2.413669, 1.190664, 0.099543, - 0.116283, 2.411423, 1.190256, 0.114520, - 0.133071, 2.408711, 1.189719, 0.130616, - 0.150670, 2.404900, 1.189019, 0.147856, - 0.169197, 2.400512, 1.188125, 0.166235, - 0.188545, 2.394939, 1.186972, 0.185804, - 0.208480, 2.388232, 1.185515, 0.206488, - 0.228883, 2.379190, 1.183673, 0.228383, - 0.249897, 2.369208, 1.181382, 0.251305, - 0.270851, 2.355459, 1.178478, 0.275349, - 0.291780, 2.339142, 1.174857, 0.300106, - 0.312257, 2.316655, 1.170411, 0.325849, - 0.332225, 2.291540, 1.164883, 0.351782, - 0.350862, 2.257242, 1.158196, 0.378248, - 0.368431, 2.218671, 1.150173, 0.404674, - 0.384428, 2.173680, 1.140703, 0.431385, - 0.399230, 2.127083, 1.129555, 0.457407, - 0.411875, 2.073236, 1.116436, 0.483275, - 0.423013, 2.018223, 1.101373, 0.509278, - 0.432624, 1.962674, 1.084257, 0.534751, - 0.439261, 1.900814, 1.064592, 0.561895, - 0.443801, 1.839558, 1.040881, 0.588677, - 0.445872, 1.777763, 1.015208, 0.614900, - 0.445896, 1.716550, 0.987252, 0.641051, - 0.444148, 1.657984, 0.957271, 0.666409, - 0.440299, 1.600832, 0.924841, 0.691872, - 0.435318, 1.548237, 0.891185, 0.716638, - 0.428631, 1.497572, 0.855929, 0.739864, - 0.419872, 1.447043, 0.819676, 0.763707, - 0.410456, 1.403648, 0.781455, 0.786744, - 0.399390, 1.360844, 0.742965, 0.809585, - 0.386381, 1.320529, 0.703260, 0.834164, - 0.372622, 1.286467, 0.662385, 0.856713, - 0.357177, 1.252306, 0.621379, 0.879820, - 0.341458, 1.223070, 0.580238, 0.902721, - 0.325024, 1.197115, 0.539028, 0.924650, - 0.307543, 1.172314, 0.498592, 0.947613, - 0.289557, 1.151171, 0.457980, 0.969590, - 0.269799, 1.129986, 0.417696, 0.992961, - 0.250111, 1.113321, 0.377529, 1.014582, - 0.229761, 1.097149, 0.339096, 1.038069, - 0.209375, 1.083913, 0.301119, 1.061661, - 0.188038, 1.071241, 0.263506, 1.085069, - 0.165874, 1.060508, 0.227921, 1.107744, - 0.143437, 1.052930, 0.194062, 1.127982, - 0.120574, 1.046396, 0.162506, 1.144541, - 0.096569, 1.039880, 0.130788, 1.155876, - 0.072039, 1.030946, 0.098057, 1.162719, - 0.047888, 1.020124, 0.064956, 1.167089, - 0.023740, 1.008953, 0.031966, 1.172775, 0.000277, 1.000067, - 0.000111, 0.000200, - 0.000251, 2.573709, 1.253951, 0.000504, - 0.000632, 2.572401, 1.253940, 0.002015, - 0.002527, 2.571267, 1.253927, 0.004535, - 0.005687, 2.572481, 1.253948, 0.008062, - 0.010108, 2.571851, 1.253941, 0.012588, - 0.015780, 2.568431, 1.253934, 0.018139, - 0.022731, 2.569765, 1.253893, 0.024709, - 0.030948, 2.572115, 1.253853, 0.032283, - 0.040401, 2.571456, 1.253785, 0.040883, - 0.051105, 2.571041, 1.253683, 0.050514, - 0.063041, 2.570153, 1.253538, 0.061188, - 0.076195, 2.569085, 1.253336, 0.072926, - 0.090402, 2.567184, 1.253065, 0.085746, - 0.105745, 2.564731, 1.252697, 0.099661, - 0.122296, 2.561995, 1.252218, 0.114699, - 0.139912, 2.559019, 1.251590, 0.130882, - 0.158362, 2.555017, 1.250766, 0.148202, - 0.177856, 2.549419, 1.249744, 0.166706, - 0.198049, 2.542908, 1.248423, 0.186404, - 0.219014, 2.535205, 1.246741, 0.207272, - 0.240376, 2.524893, 1.244596, 0.229345, - 0.262230, 2.512804, 1.241917, 0.252494, - 0.284134, 2.496923, 1.238610, 0.276690, - 0.305828, 2.476583, 1.234474, 0.301798, - 0.327107, 2.451548, 1.229292, 0.327423, - 0.347300, 2.418630, 1.222997, 0.353848, - 0.366699, 2.381002, 1.215366, 0.380342, - 0.384421, 2.334413, 1.206199, 0.407390, - 0.400855, 2.285660, 1.195374, 0.433913, - 0.415241, 2.228604, 1.182290, 0.460837, - 0.428275, 2.171532, 1.167385, 0.486381, - 0.438573, 2.105639, 1.150401, 0.511959, - 0.447348, 2.040835, 1.130990, 0.537586, - 0.454152, 1.974797, 1.109302, 0.564035, - 0.458684, 1.907895, 1.084131, 0.590690, - 0.460058, 1.839482, 1.055803, 0.617250, - 0.459662, 1.772332, 1.025103, 0.643406, - 0.457260, 1.707313, 0.992502, 0.668794, - 0.452666, 1.644722, 0.957657, 0.693930, - 0.446641, 1.586832, 0.921340, 0.718708, - 0.439121, 1.531197, 0.883841, 0.743469, - 0.430429, 1.480765, 0.844931, 0.766080, - 0.419622, 1.430338, 0.804786, 0.789801, - 0.408368, 1.386295, 0.764206, 0.812718, - 0.395392, 1.343758, 0.722565, 0.835453, - 0.380699, 1.304655, 0.680585, 0.858801, - 0.364834, 1.269287, 0.637235, 0.881537, - 0.348092, 1.237493, 0.594579, 0.904656, - 0.331087, 1.208862, 0.552313, 0.926357, - 0.312966, 1.182365, 0.510080, 0.949001, - 0.294684, 1.159452, 0.468677, 0.971598, - 0.275361, 1.138706, 0.426723, 0.994905, - 0.254947, 1.120552, 0.385875, 1.017981, - 0.234109, 1.104215, 0.345751, 1.040840, - 0.213040, 1.089276, 0.306762, 1.063893, - 0.191616, 1.075845, 0.269066, 1.086907, - 0.169272, 1.063788, 0.232171, 1.109937, - 0.146076, 1.054977, 0.197826, 1.130808, - 0.122544, 1.048572, 0.165272, 1.146831, - 0.098492, 1.040742, 0.133280, 1.158955, - 0.073710, 1.031818, 0.100262, 1.166161, - 0.048610, 1.020747, 0.066165, 1.170491, - 0.024209, 1.009380, 0.032741, 1.176111, 0.000010, 1.000042, 0.000056, 0.000202, - 0.000267, 2.786357, 1.320169, 0.000504, - 0.000665, 2.741889, 1.320168, 0.002015, - 0.002660, 2.740000, 1.320143, 0.004536, - 0.005987, 2.744276, 1.320161, 0.008063, - 0.010644, 2.743432, 1.320162, 0.012600, - 0.016628, 2.741741, 1.320148, 0.018144, - 0.023937, 2.741314, 1.320127, 0.024708, - 0.032577, 2.741916, 1.320061, 0.032290, - 0.042536, 2.742132, 1.319976, 0.040894, - 0.053799, 2.741199, 1.319861, 0.050533, - 0.066361, 2.740258, 1.319691, 0.061223, - 0.080202, 2.739045, 1.319458, 0.072985, - 0.095109, 2.736519, 1.319138, 0.085841, - 0.111296, 2.733903, 1.318715, 0.099808, - 0.128685, 2.730944, 1.318156, 0.114903, - 0.147202, 2.727293, 1.317424, 0.131164, - 0.166575, 2.722169, 1.316485, 0.148599, - 0.187019, 2.716148, 1.315274, 0.167245, - 0.208240, 2.708701, 1.313733, 0.187078, - 0.230151, 2.698998, 1.311792, 0.208153, - 0.252538, 2.687341, 1.309343, 0.230418, - 0.275295, 2.672621, 1.306247, 0.253802, - 0.298066, 2.653619, 1.302374, 0.278261, - 0.320673, 2.629943, 1.297573, 0.303527, - 0.342528, 2.599228, 1.291625, 0.329571, - 0.363531, 2.562226, 1.284374, 0.355939, - 0.382963, 2.515491, 1.275478, 0.382987, - 0.401306, 2.464858, 1.264866, 0.409917, - 0.417455, 2.404877, 1.252184, 0.437015, - 0.432067, 2.341408, 1.237415, 0.463474, - 0.444204, 2.271837, 1.220687, 0.489835, - 0.454631, 2.200593, 1.200973, 0.516054, - 0.463338, 2.129733, 1.179346, 0.541397, - 0.469425, 2.055635, 1.155039, 0.566798, - 0.473526, 1.980812, 1.127866, 0.593114, - 0.474632, 1.904723, 1.097304, 0.619945, - 0.473597, 1.832456, 1.063603, 0.646325, - 0.470656, 1.761501, 1.027971, 0.672320, - 0.465675, 1.694248, 0.990692, 0.697163, - 0.458527, 1.629227, 0.951582, 0.721472, - 0.449904, 1.568132, 0.911197, 0.745855, - 0.440140, 1.512084, 0.869745, 0.770089, - 0.429338, 1.460694, 0.827648, 0.792546, - 0.416701, 1.410739, 0.784728, 0.815161, - 0.403151, 1.365438, 0.741884, 0.837994, - 0.388714, 1.324811, 0.697800, 0.861220, - 0.372573, 1.287723, 0.653341, 0.883737, - 0.355024, 1.252491, 0.609455, 0.906784, - 0.337092, 1.221844, 0.565275, 0.928493, - 0.318370, 1.192881, 0.521558, 0.951495, - 0.299605, 1.169131, 0.478149, 0.973586, - 0.280067, 1.146316, 0.436325, 0.996400, - 0.259823, 1.127860, 0.394409, 1.019780, - 0.238313, 1.110521, 0.353045, 1.042775, - 0.216506, 1.093915, 0.312803, 1.066822, - 0.194695, 1.080326, 0.274100, 1.089869, - 0.172290, 1.067722, 0.236657, 1.113606, - 0.149264, 1.058471, 0.201603, 1.134229, - 0.124814, 1.050701, 0.168398, 1.150922, - 0.100070, 1.043051, 0.135616, 1.163224, - 0.075155, 1.033742, 0.102144, 1.169965, - 0.049933, 1.021818, 0.067532, 1.174200, - 0.024461, 1.009916, 0.033215, 1.179766, 0.000188, 1.000045, - 0.000014, 0.000202, - 0.000281, 2.964186, 1.390880, 0.000505, - 0.000702, 2.945157, 1.390903, 0.002015, - 0.002802, 2.931184, 1.390863, 0.004535, - 0.006307, 2.935673, 1.390900, 0.008063, - 0.011213, 2.934274, 1.390890, 0.012598, - 0.017516, 2.932216, 1.390876, 0.018147, - 0.025221, 2.933324, 1.390832, 0.024711, - 0.034322, 2.933945, 1.390769, 0.032295, - 0.044810, 2.933496, 1.390674, 0.040904, - 0.056673, 2.932487, 1.390538, 0.050555, - 0.069906, 2.931571, 1.390342, 0.061259, - 0.084468, 2.929914, 1.390064, 0.073053, - 0.100152, 2.927039, 1.389695, 0.085948, - 0.117202, 2.924241, 1.389201, 0.099968, - 0.135531, 2.920760, 1.388548, 0.115135, - 0.154906, 2.915998, 1.387692, 0.131496, - 0.175352, 2.910285, 1.386611, 0.149049, - 0.196783, 2.903174, 1.385190, 0.167848, - 0.219066, 2.894584, 1.383407, 0.187879, - 0.241983, 2.883171, 1.381148, 0.209143, - 0.265398, 2.869102, 1.378261, 0.231689, - 0.289254, 2.852238, 1.374690, 0.255223, - 0.312776, 2.828264, 1.370166, 0.279952, - 0.336260, 2.800175, 1.364591, 0.305572, - 0.358865, 2.764282, 1.357758, 0.331650, - 0.380223, 2.717845, 1.349413, 0.358491, - 0.400252, 2.665326, 1.339084, 0.385445, - 0.418422, 2.602293, 1.326773, 0.412947, - 0.434993, 2.536973, 1.312141, 0.439681, - 0.448757, 2.459463, 1.295205, 0.467272, - 0.461427, 2.386250, 1.275573, 0.493568, - 0.471102, 2.303225, 1.253400, 0.519743, - 0.478930, 2.221945, 1.228890, 0.544882, - 0.484098, 2.136425, 1.201730, 0.570690, - 0.488125, 2.057093, 1.172022, 0.595905, - 0.489185, 1.975334, 1.139312, 0.622747, - 0.487535, 1.895055, 1.103038, 0.648695, - 0.483482, 1.815995, 1.064364, 0.675159, - 0.478096, 1.744272, 1.024098, 0.700714, - 0.470492, 1.675257, 0.982186, 0.725641, - 0.461398, 1.609135, 0.939137, 0.748552, - 0.449825, 1.545091, 0.894791, 0.772808, - 0.438185, 1.489394, 0.850373, 0.795928, - 0.425073, 1.437026, 0.805287, 0.818900, - 0.411028, 1.389654, 0.760003, 0.841633, - 0.396047, 1.345873, 0.714914, 0.863213, - 0.379637, 1.305185, 0.669271, 0.886662, - 0.362227, 1.269147, 0.622935, 0.908504, - 0.343068, 1.234714, 0.577757, 0.931425, - 0.323982, 1.204997, 0.532922, 0.953835, - 0.304347, 1.178871, 0.488154, 0.975813, - 0.284219, 1.155019, 0.444885, 0.997662, - 0.263544, 1.133941, 0.402224, 1.021167, - 0.242611, 1.116100, 0.360530, 1.044038, - 0.220065, 1.098348, 0.318968, 1.068837, - 0.197580, 1.084605, 0.279107, 1.092548, - 0.174779, 1.071217, 0.241111, 1.116157, - 0.151596, 1.060486, 0.204913, 1.137486, - 0.127478, 1.052751, 0.171410, 1.154694, - 0.101915, 1.044807, 0.137999, 1.166867, - 0.076246, 1.034824, 0.103807, 1.173715, - 0.050661, 1.022501, 0.068802, 1.178236, - 0.025355, 1.010324, 0.034155, 1.183545, 0.000205, 1.000059, - 0.000110, 0.000201, - 0.000294, 3.161080, 1.466721, 0.000505, - 0.000740, 3.155526, 1.466737, 0.002016, - 0.002957, 3.152852, 1.466688, 0.004537, - 0.006655, 3.150654, 1.466667, 0.008066, - 0.011828, 3.153109, 1.466694, 0.012604, - 0.018479, 3.152143, 1.466721, 0.018150, - 0.026598, 3.151025, 1.466636, 0.024714, - 0.036191, 3.150300, 1.466562, 0.032301, - 0.047249, 3.149861, 1.466450, 0.040924, - 0.059766, 3.149548, 1.466289, 0.050579, - 0.073703, 3.147516, 1.466055, 0.061306, - 0.089022, 3.145680, 1.465738, 0.073135, - 0.105563, 3.142428, 1.465301, 0.086075, - 0.123544, 3.139113, 1.464715, 0.100153, - 0.142853, 3.135064, 1.463956, 0.115411, - 0.163183, 3.129509, 1.462962, 0.131876, - 0.184760, 3.122959, 1.461670, 0.149570, - 0.207172, 3.114153, 1.460045, 0.168523, - 0.230578, 3.103626, 1.457945, 0.188784, - 0.254658, 3.090818, 1.455279, 0.210264, - 0.279114, 3.073352, 1.451998, 0.233030, - 0.303930, 3.052592, 1.447780, 0.256959, - 0.328517, 3.025187, 1.442568, 0.281901, - 0.352755, 2.990341, 1.436026, 0.307728, - 0.375894, 2.946820, 1.427979, 0.334197, - 0.397924, 2.892845, 1.418249, 0.360966, - 0.417914, 2.827937, 1.406370, 0.388478, - 0.436526, 2.758006, 1.392134, 0.415567, - 0.452366, 2.674696, 1.375244, 0.443518, - 0.466917, 2.595136, 1.355660, 0.470631, - 0.478417, 2.504173, 1.333123, 0.497419, - 0.487825, 2.413227, 1.308181, 0.523961, - 0.495064, 2.321239, 1.280227, 0.549708, - 0.499844, 2.228911, 1.249894, 0.575296, - 0.502844, 2.138834, 1.217130, 0.600168, - 0.503368, 2.049030, 1.181412, 0.625874, - 0.501622, 1.962267, 1.142648, 0.652164, - 0.496936, 1.876900, 1.101268, 0.678029, - 0.490319, 1.796344, 1.057782, 0.703248, - 0.481575, 1.718925, 1.012884, 0.728520, - 0.471822, 1.648358, 0.966487, 0.752577, - 0.460134, 1.581989, 0.919880, 0.776163, - 0.447164, 1.520109, 0.873087, 0.800016, - 0.433601, 1.465081, 0.825803, 0.822176, - 0.418388, 1.412564, 0.778249, 0.844873, - 0.402704, 1.366184, 0.730849, 0.865955, - 0.385633, 1.321865, 0.684037, 0.888173, - 0.368255, 1.283464, 0.637192, 0.910994, - 0.349332, 1.249215, 0.590131, 0.934270, - 0.329612, 1.218366, 0.543213, 0.956653, - 0.309228, 1.189808, 0.497752, 0.978476, - 0.288310, 1.163674, 0.452837, 1.000755, - 0.267243, 1.141389, 0.409481, 1.023827, - 0.246015, 1.122012, 0.367354, 1.045572, - 0.223777, 1.103303, 0.325171, 1.070445, - 0.200837, 1.088010, 0.284442, 1.094268, - 0.177211, 1.073650, 0.245138, 1.118639, - 0.153531, 1.063051, 0.208289, 1.139786, - 0.129074, 1.053921, 0.173607, 1.157848, - 0.104051, 1.045968, 0.140467, 1.170697, - 0.077694, 1.035782, 0.105594, 1.177874, - 0.051393, 1.023483, 0.069898, 1.182242, - 0.025392, 1.010620, 0.034532, 1.187612, - 0.000032, 1.000062, - 0.000035, 0.000202, - 0.000313, 3.450327, 1.548291, 0.000504, - 0.000780, 3.396162, 1.548289, 0.002015, - 0.003120, 3.395621, 1.548260, 0.004533, - 0.007019, 3.394299, 1.548217, 0.008066, - 0.012486, 3.398803, 1.548274, 0.012600, - 0.019500, 3.396363, 1.548245, 0.018151, - 0.028076, 3.396805, 1.548192, 0.024722, - 0.038209, 3.396384, 1.548109, 0.032306, - 0.049868, 3.395158, 1.547979, 0.040936, - 0.063077, 3.394303, 1.547785, 0.050610, - 0.077791, 3.392979, 1.547513, 0.061360, - 0.093869, 3.389910, 1.547134, 0.073227, - 0.111380, 3.386669, 1.546619, 0.086217, - 0.130371, 3.382974, 1.545938, 0.100364, - 0.150684, 3.378046, 1.545039, 0.115733, - 0.172116, 3.371719, 1.543880, 0.132309, - 0.194809, 3.363764, 1.542380, 0.150174, - 0.218431, 3.353699, 1.540462, 0.169340, - 0.242954, 3.341397, 1.538002, 0.189788, - 0.268175, 3.324957, 1.534894, 0.211581, - 0.293776, 3.304776, 1.530954, 0.234561, - 0.319619, 3.278192, 1.526033, 0.258776, - 0.345089, 3.244910, 1.519926, 0.284059, - 0.370176, 3.203338, 1.512296, 0.310312, - 0.394171, 3.152477, 1.502956, 0.336748, - 0.416137, 3.083616, 1.491463, 0.364029, - 0.436752, 3.010481, 1.477493, 0.391575, - 0.455102, 2.925454, 1.460933, 0.419409, - 0.471378, 2.834380, 1.441554, 0.446811, - 0.484714, 2.733329, 1.418861, 0.474489, - 0.496021, 2.633630, 1.393405, 0.501751, - 0.504991, 2.530935, 1.364633, 0.528488, - 0.511392, 2.426653, 1.333234, 0.554428, - 0.515395, 2.323633, 1.299138, 0.580434, - 0.517761, 2.224964, 1.262462, 0.605474, - 0.517598, 2.127228, 1.223784, 0.629888, - 0.514946, 2.030545, 1.182321, 0.655579, - 0.510177, 1.939070, 1.138515, 0.681940, - 0.503097, 1.852355, 1.091502, 0.707228, - 0.493537, 1.768084, 1.043464, 0.731894, - 0.482372, 1.690840, 0.994242, 0.756741, - 0.470312, 1.619277, 0.944749, 0.780160, - 0.456412, 1.553430, 0.894816, 0.803384, - 0.441492, 1.493357, 0.845202, 0.826347, - 0.425944, 1.437830, 0.795954, 0.849145, - 0.409532, 1.388578, 0.746915, 0.870617, - 0.391988, 1.341527, 0.698025, 0.892943, - 0.374229, 1.302188, 0.649579, 0.913828, - 0.355148, 1.262877, 0.601833, 0.936830, - 0.335238, 1.230136, 0.554521, 0.958687, - 0.313939, 1.199596, 0.507208, 0.982008, - 0.292741, 1.173619, 0.461357, 1.003691, - 0.270940, 1.149015, 0.416031, 1.027223, - 0.249102, 1.128689, 0.372457, 1.050048, - 0.226899, 1.109444, 0.330281, 1.074105, - 0.204329, 1.092943, 0.288987, 1.098971, - 0.180560, 1.078591, 0.249075, 1.123324, - 0.155987, 1.066885, 0.211519, 1.145445, - 0.130929, 1.057617, 0.176506, 1.162856, - 0.105269, 1.048453, 0.142345, 1.175360, - 0.079267, 1.037439, 0.107452, 1.182514, - 0.052547, 1.024393, 0.071252, 1.186575, - 0.025744, 1.011093, 0.035019, 1.192050, 0.000318, 1.000013, - 0.000152, 0.000204, - 0.000334, 3.909175, 1.636412, 0.000504, - 0.000825, 3.678647, 1.636410, 0.002015, - 0.003298, 3.678315, 1.636387, 0.004533, - 0.007417, 3.674126, 1.636310, 0.008062, - 0.013190, 3.676771, 1.636376, 0.012603, - 0.020613, 3.678135, 1.636369, 0.018153, - 0.029675, 3.677315, 1.636299, 0.024723, - 0.040378, 3.676872, 1.636196, 0.032318, - 0.052708, 3.675750, 1.636038, 0.040955, - 0.066660, 3.674803, 1.635810, 0.050645, - 0.082203, 3.672735, 1.635494, 0.061429, - 0.099150, 3.669047, 1.635048, 0.073333, - 0.117679, 3.665401, 1.634437, 0.086388, - 0.137725, 3.661315, 1.633634, 0.100620, - 0.159081, 3.654992, 1.632571, 0.116087, - 0.181721, 3.647341, 1.631202, 0.132820, - 0.205611, 3.637877, 1.629432, 0.150867, - 0.230542, 3.626333, 1.627161, 0.170234, - 0.256239, 3.610671, 1.624266, 0.190981, - 0.282751, 3.591685, 1.620589, 0.213013, - 0.309430, 3.565864, 1.615999, 0.236387, - 0.336427, 3.534826, 1.610216, 0.260943, - 0.362931, 3.493984, 1.603047, 0.286497, - 0.388644, 3.442075, 1.593920, 0.312769, - 0.412912, 3.375973, 1.582961, 0.339832, - 0.435635, 3.299355, 1.569343, 0.367214, - 0.456181, 3.208994, 1.553137, 0.394935, - 0.474325, 3.108910, 1.533791, 0.422935, - 0.490318, 3.001767, 1.511093, 0.451166, - 0.503827, 2.891735, 1.485145, 0.478695, - 0.514185, 2.773430, 1.455617, 0.506313, - 0.522502, 2.657639, 1.422946, 0.533427, - 0.528119, 2.541132, 1.387843, 0.559942, - 0.531430, 2.426950, 1.349542, 0.585150, - 0.531978, 2.312437, 1.309303, 0.610500, - 0.531054, 2.205966, 1.266280, 0.635380, - 0.528058, 2.101993, 1.221709, 0.659852, - 0.522751, 2.002950, 1.175062, 0.685151, - 0.515026, 1.908647, 1.125078, 0.710920, - 0.505020, 1.819389, 1.074296, 0.736066, - 0.493268, 1.735806, 1.022420, 0.760503, - 0.480032, 1.658607, 0.970230, 0.785091, - 0.465986, 1.589424, 0.917077, 0.807523, - 0.449721, 1.522533, 0.864888, 0.830974, - 0.433461, 1.465416, 0.813006, 0.852659, - 0.415808, 1.409076, 0.761689, 0.874841, - 0.397855, 1.360758, 0.711258, 0.896322, - 0.379041, 1.316829, 0.661721, 0.918134, - 0.360048, 1.278574, 0.612263, 0.939356, - 0.340108, 1.242200, 0.564369, 0.961025, - 0.318877, 1.210305, 0.516506, 0.984371, - 0.297130, 1.183689, 0.469342, 1.006905, - 0.274661, 1.157466, 0.423080, 1.029941, - 0.252234, 1.135066, 0.378315, 1.052751, - 0.229268, 1.114518, 0.335169, 1.077981, - 0.206662, 1.097760, 0.293336, 1.102542, - 0.183331, 1.082051, 0.252984, 1.126539, - 0.158797, 1.068935, 0.214990, 1.149023, - 0.133014, 1.058996, 0.178903, 1.167550, - 0.106641, 1.050245, 0.144559, 1.179994, - 0.079952, 1.038648, 0.108667, 1.187104, - 0.053316, 1.025284, 0.072209, 1.191406, - 0.026826, 1.011453, 0.035833, 1.196748, 0.000226, 1.000034, - 0.000061, 0.000200, - 0.000346, 3.996419, 1.732034, 0.000504, - 0.000873, 4.000138, 1.732038, 0.002016, - 0.003492, 4.002078, 1.732012, 0.004538, - 0.007859, 4.005626, 1.731962, 0.008064, - 0.013963, 3.998500, 1.731999, 0.012590, - 0.021794, 3.995024, 1.732004, 0.018154, - 0.031406, 3.999233, 1.731901, 0.024727, - 0.042733, 3.998497, 1.731774, 0.032327, - 0.055781, 3.997064, 1.731599, 0.040974, - 0.070543, 3.995856, 1.731325, 0.050685, - 0.086984, 3.993839, 1.730945, 0.061506, - 0.104897, 3.989519, 1.730417, 0.073458, - 0.124506, 3.985313, 1.729697, 0.086573, - 0.145706, 3.979984, 1.728747, 0.100909, - 0.168211, 3.972562, 1.727491, 0.116509, - 0.192198, 3.963836, 1.725854, 0.133404, - 0.217280, 3.951919, 1.723749, 0.151659, - 0.243556, 3.937734, 1.721093, 0.171288, - 0.270611, 3.919021, 1.717640, 0.192301, - 0.298389, 3.895171, 1.713272, 0.214683, - 0.326338, 3.864171, 1.707825, 0.238392, - 0.354394, 3.824682, 1.700956, 0.263151, - 0.381636, 3.771168, 1.692392, 0.289155, - 0.408266, 3.709961, 1.681769, 0.315832, - 0.433070, 3.630302, 1.668539, 0.342942, - 0.455741, 3.534719, 1.652513, 0.370892, - 0.476655, 3.431531, 1.633428, 0.398985, - 0.494692, 3.314933, 1.610694, 0.427206, - 0.510313, 3.189741, 1.584240, 0.455266, - 0.522760, 3.058325, 1.554195, 0.483472, - 0.532872, 2.927213, 1.520805, 0.511192, - 0.540229, 2.794112, 1.484026, 0.538706, - 0.545105, 2.663786, 1.443796, 0.565422, - 0.547251, 2.534841, 1.401429, 0.591270, - 0.547115, 2.408437, 1.356231, 0.616787, - 0.545113, 2.291284, 1.308887, 0.641380, - 0.540853, 2.177478, 1.260447, 0.665344, - 0.534561, 2.069265, 1.210634, 0.690147, - 0.527115, 1.969776, 1.158569, 0.714578, - 0.516171, 1.870847, 1.104593, 0.740349, - 0.504048, 1.782674, 1.049578, 0.764563, - 0.489683, 1.698614, 0.994458, 0.788710, - 0.474541, 1.624447, 0.938612, 0.812154, - 0.458099, 1.554453, 0.883694, 0.834566, - 0.440345, 1.490045, 0.830220, 0.857486, - 0.422491, 1.432889, 0.776499, 0.879224, - 0.403588, 1.380669, 0.724257, 0.899971, - 0.383819, 1.333124, 0.673311, 0.922111, - 0.364250, 1.292648, 0.622999, 0.942842, - 0.343873, 1.253933, 0.573304, 0.964398, - 0.323206, 1.221027, 0.525090, 0.986860, - 0.301711, 1.191806, 0.477580, 1.009760, - 0.278695, 1.165162, 0.430624, 1.033347, - 0.255591, 1.141715, 0.384482, 1.055937, - 0.232039, 1.119739, 0.340532, 1.081178, - 0.208664, 1.102117, 0.297311, 1.105696, - 0.184935, 1.085062, 0.256227, 1.129575, - 0.160673, 1.070918, 0.217709, 1.152135, - 0.135414, 1.060642, 0.181471, 1.171221, - 0.108462, 1.051041, 0.146380, 1.184412, - 0.081008, 1.039694, 0.110120, 1.191820, - 0.053710, 1.025903, 0.073052, 1.196195, - 0.026625, 1.011816, 0.036129, 1.201677, - 0.000175, 0.999945, 0.000098, 0.000196, - 0.000360, 4.100786, 1.836290, 0.000504, - 0.000925, 4.370184, 1.836295, 0.002018, - 0.003706, 4.385247, 1.836243, 0.004534, - 0.008324, 4.370146, 1.836210, 0.008064, - 0.014805, 4.372335, 1.836256, 0.012597, - 0.023116, 4.359918, 1.836259, 0.018158, - 0.033299, 4.371503, 1.836123, 0.024732, - 0.045301, 4.370533, 1.835988, 0.032344, - 0.059143, 4.369649, 1.835768, 0.040999, - 0.074779, 4.367861, 1.835454, 0.050739, - 0.092178, 4.364322, 1.834974, 0.061594, - 0.111161, 4.359221, 1.834355, 0.073604, - 0.131958, 4.354620, 1.833499, 0.086796, - 0.154393, 4.347915, 1.832355, 0.101246, - 0.178201, 4.339152, 1.830880, 0.116990, - 0.203531, 4.328327, 1.828936, 0.134086, - 0.230043, 4.314240, 1.826442, 0.152589, - 0.257718, 4.296795, 1.823230, 0.172514, - 0.286176, 4.273985, 1.819124, 0.193853, - 0.315295, 4.244136, 1.813909, 0.216582, - 0.344507, 4.205152, 1.807410, 0.240668, - 0.373646, 4.154781, 1.799084, 0.265904, - 0.401897, 4.091563, 1.788905, 0.292226, - 0.429136, 4.013199, 1.776206, 0.319045, - 0.454057, 3.912886, 1.760500, 0.346721, - 0.477219, 3.800927, 1.741586, 0.374849, - 0.497883, 3.675652, 1.718818, 0.403078, - 0.515504, 3.536892, 1.692138, 0.431597, - 0.530621, 3.391351, 1.661434, 0.460246, - 0.542852, 3.242817, 1.626989, 0.488899, - 0.552238, 3.093685, 1.588582, 0.517215, - 0.559045, 2.944163, 1.546300, 0.544480, - 0.562351, 2.794189, 1.501299, 0.571542, - 0.563394, 2.650239, 1.453758, 0.598167, - 0.562590, 2.513757, 1.403321, 0.624104, - 0.559636, 2.384203, 1.352431, 0.648789, - 0.554148, 2.259149, 1.298758, 0.672715, - 0.546779, 2.140250, 1.244943, 0.696258, - 0.537896, 2.030401, 1.189971, 0.720048, - 0.527401, 1.928311, 1.134526, 0.744078, - 0.514142, 1.830175, 1.076504, 0.768895, - 0.499352, 1.740731, 1.018032, 0.792551, - 0.482982, 1.658911, 0.960250, 0.817007, - 0.466406, 1.586579, 0.903029, 0.839035, - 0.447616, 1.516969, 0.846484, 0.862742, - 0.429261, 1.458675, 0.791420, 0.884307, - 0.409479, 1.402989, 0.737125, 0.905641, - 0.389303, 1.352817, 0.683912, 0.926185, - 0.368344, 1.306684, 0.632690, 0.947229, - 0.347366, 1.267395, 0.581739, 0.969502, - 0.326720, 1.233192, 0.532305, 0.990758, - 0.304973, 1.201017, 0.484166, 1.012749, - 0.282816, 1.173018, 0.437385, 1.035533, - 0.259084, 1.147184, 0.390755, 1.059915, - 0.235239, 1.125388, 0.345399, 1.084348, - 0.211044, 1.105859, 0.301356, 1.109544, - 0.186698, 1.088888, 0.259708, 1.133770, - 0.161900, 1.073848, 0.220324, 1.157553, - 0.136604, 1.063190, 0.183857, 1.176461, - 0.110428, 1.053110, 0.148521, 1.190137, - 0.082898, 1.041484, 0.112124, 1.197215, - 0.054554, 1.026844, 0.074160, 1.201654, - 0.026744, 1.012264, 0.036527, 1.207085, 0.000399, 1.000034, - 0.000201, 0.000191, - 0.000373, 4.194318, 1.950551, 0.000504, - 0.000983, 4.804350, 1.950552, 0.002015, - 0.003931, 4.802820, 1.950518, 0.004536, - 0.008847, 4.805254, 1.950472, 0.008064, - 0.015725, 4.804152, 1.950517, 0.012693, - 0.024740, 4.826828, 1.949914, 0.018159, - 0.035365, 4.803103, 1.950349, 0.024740, - 0.048122, 4.803220, 1.950183, 0.032361, - 0.062822, 4.801522, 1.949917, 0.041034, - 0.079430, 4.799593, 1.949538, 0.050815, - 0.097841, 4.797179, 1.948972, 0.061702, - 0.118026, 4.789557, 1.948246, 0.073766, - 0.140112, 4.783293, 1.947204, 0.087066, - 0.163819, 4.775698, 1.945855, 0.101637, - 0.189122, 4.764612, 1.944052, 0.117558, - 0.215884, 4.751486, 1.941710, 0.134884, - 0.243968, 4.734791, 1.938727, 0.153637, - 0.273170, 4.712078, 1.934891, 0.173890, - 0.303146, 4.683575, 1.929976, 0.195643, - 0.333704, 4.646766, 1.923740, 0.218767, - 0.364170, 4.596814, 1.915888, 0.243337, - 0.394530, 4.535509, 1.905970, 0.268860, - 0.423512, 4.452006, 1.893623, 0.295173, - 0.450609, 4.345682, 1.878286, 0.322784, - 0.476488, 4.231632, 1.859391, 0.350616, - 0.499420, 4.093553, 1.836912, 0.379127, - 0.519862, 3.944127, 1.809625, 0.407860, - 0.537373, 3.782223, 1.778529, 0.436717, - 0.551802, 3.615563, 1.742684, 0.465345, - 0.562951, 3.440672, 1.702289, 0.494158, - 0.571334, 3.268070, 1.658666, 0.522896, - 0.577227, 3.100668, 1.611027, 0.551379, - 0.580514, 2.937615, 1.559742, 0.578992, - 0.580610, 2.778703, 1.507257, 0.605095, - 0.577729, 2.621626, 1.451941, 0.630653, - 0.573000, 2.476506, 1.395218, 0.656175, - 0.566944, 2.341592, 1.337862, 0.681036, - 0.558988, 2.216478, 1.279275, 0.704713, - 0.549211, 2.096972, 1.220526, 0.726894, - 0.537190, 1.983311, 1.161709, 0.749865, - 0.524167, 1.881100, 1.102095, 0.773553, - 0.508991, 1.785637, 1.042039, 0.797102, - 0.491658, 1.697234, 0.981588, 0.821187, - 0.474093, 1.620250, 0.921265, 0.843848, - 0.454980, 1.547071, 0.862757, 0.866662, - 0.435421, 1.482008, 0.804700, 0.888696, - 0.414990, 1.424116, 0.749432, 0.910945, - 0.394472, 1.372658, 0.694767, 0.932300, - 0.373239, 1.325157, 0.641106, 0.952850, - 0.351347, 1.282217, 0.589689, 0.974718, - 0.329809, 1.244897, 0.539322, 0.996445, - 0.307902, 1.212306, 0.490083, 1.017580, - 0.285392, 1.181402, 0.442702, 1.040342, - 0.262782, 1.155996, 0.395911, 1.064399, - 0.238995, 1.131708, 0.350206, 1.089464, - 0.214297, 1.111215, 0.305175, 1.115565, - 0.189293, 1.093094, 0.262686, 1.140640, - 0.163843, 1.077994, 0.223078, 1.163824, - 0.137789, 1.066014, 0.185651, 1.182577, - 0.111087, 1.055615, 0.150045, 1.195775, - 0.083945, 1.042940, 0.113457, 1.203175, - 0.056145, 1.028015, 0.075453, 1.207282, - 0.027685, 1.012552, 0.037217, 1.213019, 0.000362, 0.999938, - 0.000293, 0.000187, - 0.000388, 4.316009, 2.076500, 0.000504, - 0.001048, 5.317799, 2.076499, 0.002014, - 0.004182, 5.306557, 2.076523, 0.004539, - 0.009425, 5.317505, 2.076453, 0.008063, - 0.016737, 5.312143, 2.076410, 0.012614, - 0.026171, 5.316434, 2.076389, 0.018158, - 0.037641, 5.307836, 2.076265, 0.024767, - 0.051266, 5.315297, 2.076044, 0.032372, - 0.066859, 5.307433, 2.075743, 0.041066, - 0.084538, 5.304809, 2.075270, 0.050871, - 0.104062, 5.299277, 2.074622, 0.061821, - 0.125613, 5.293419, 2.073708, 0.073970, - 0.149085, 5.286629, 2.072457, 0.087375, - 0.174214, 5.275937, 2.070804, 0.102105, - 0.201136, 5.263267, 2.068647, 0.118223, - 0.229505, 5.246309, 2.065846, 0.135814, - 0.259217, 5.225496, 2.062189, 0.154887, - 0.289990, 5.196580, 2.057566, 0.175510, - 0.321618, 5.160716, 2.051593, 0.197636, - 0.353632, 5.112202, 2.043949, 0.221168, - 0.385303, 5.046981, 2.034445, 0.246099, - 0.416511, 4.965386, 2.022368, 0.272070, - 0.446377, 4.860735, 2.007160, 0.299090, - 0.474279, 4.735140, 1.988598, 0.326702, - 0.499809, 4.584962, 1.965865, 0.355017, - 0.522790, 4.420447, 1.938705, 0.383856, - 0.542755, 4.241942, 1.906370, 0.413059, - 0.559903, 4.053302, 1.869455, 0.441882, - 0.573174, 3.852753, 1.827946, 0.471516, - 0.584151, 3.660377, 1.781652, 0.500872, - 0.591843, 3.466027, 1.730885, 0.529677, - 0.596253, 3.272812, 1.676821, 0.557683, - 0.597604, 3.084286, 1.620064, 0.585652, - 0.596591, 2.906111, 1.560909, 0.612819, - 0.593138, 2.738258, 1.500318, 0.639848, - 0.588245, 2.584172, 1.438127, 0.664758, - 0.580140, 2.430697, 1.375746, 0.688754, - 0.570189, 2.290701, 1.312727, 0.712848, - 0.559420, 2.162679, 1.250063, 0.735111, - 0.546570, 2.042186, 1.187840, 0.757521, - 0.532944, 1.933435, 1.125513, 0.780056, - 0.517981, 1.833524, 1.063827, 0.802513, - 0.500724, 1.739053, 1.002154, 0.825462, - 0.481625, 1.652381, 0.939811, 0.848973, - 0.462327, 1.577560, 0.878279, 0.871521, - 0.441928, 1.509291, 0.819200, 0.892325, - 0.420297, 1.443799, 0.761607, 0.914935, - 0.399072, 1.389647, 0.705351, 0.936429, - 0.377232, 1.339903, 0.650213, 0.957614, - 0.355091, 1.295467, 0.597773, 0.979578, - 0.332767, 1.256692, 0.545914, 1.000860, - 0.310147, 1.221666, 0.495661, 1.022550, - 0.287395, 1.190775, 0.448026, 1.045005, - 0.264582, 1.162641, 0.400490, 1.068703, - 0.241464, 1.138358, 0.354088, 1.093098, - 0.217504, 1.115973, 0.309812, 1.119230, - 0.192140, 1.096284, 0.266297, 1.144608, - 0.165975, 1.080042, 0.225831, 1.168599, - 0.139174, 1.067749, 0.187761, 1.187970, - 0.111910, 1.056635, 0.151322, 1.201240, - 0.083978, 1.043566, 0.114337, 1.208895, - 0.056089, 1.028366, 0.076083, 1.213344, - 0.028369, 1.013074, 0.037735, 1.219220, - 0.000534, 0.999968, 0.000076, 0.000182, - 0.000404, 4.433519, 2.216201, 0.000504, - 0.001117, 5.911693, 2.216198, 0.002017, - 0.004469, 5.919142, 2.216190, 0.004536, - 0.010051, 5.913172, 2.216130, 0.008065, - 0.017867, 5.911791, 2.216145, 0.012467, - 0.027603, 5.785357, 2.216447, 0.018156, - 0.040159, 5.901121, 2.215958, 0.024758, - 0.054670, 5.908781, 2.215654, 0.032395, - 0.071352, 5.906098, 2.215283, 0.041108, - 0.090201, 5.902558, 2.214715, 0.050955, - 0.111004, 5.895707, 2.213905, 0.061968, - 0.134002, 5.888736, 2.212807, 0.074206, - 0.159038, 5.880633, 2.211303, 0.087742, - 0.185801, 5.867001, 2.209297, 0.102652, - 0.214368, 5.851446, 2.206657, 0.119006, - 0.244573, 5.830722, 2.203232, 0.136883, - 0.276067, 5.802688, 2.198778, 0.156335, - 0.308660, 5.767185, 2.193091, 0.177396, - 0.341940, 5.719726, 2.185858, 0.200070, - 0.375591, 5.658792, 2.176584, 0.224067, - 0.408564, 5.573508, 2.164759, 0.249420, - 0.440668, 5.465696, 2.149777, 0.275879, - 0.471138, 5.332207, 2.131225, 0.303307, - 0.499204, 5.173339, 2.108794, 0.331189, - 0.524547, 4.985102, 2.080585, 0.359932, - 0.547256, 4.785788, 2.047792, 0.389063, - 0.566479, 4.569344, 2.009518, 0.418725, - 0.583031, 4.349557, 1.965601, 0.448181, - 0.595809, 4.121278, 1.916911, 0.477703, - 0.605102, 3.892291, 1.863530, 0.507999, - 0.612462, 3.676557, 1.806286, 0.536889, - 0.615451, 3.456241, 1.745841, 0.565778, - 0.616029, 3.249464, 1.681137, 0.593863, - 0.613644, 3.050273, 1.615238, 0.620770, - 0.608268, 2.859599, 1.548003, 0.647171, - 0.601116, 2.683287, 1.480447, 0.673458, - 0.592840, 2.524036, 1.412084, 0.698064, - 0.581973, 2.371046, 1.345130, 0.721011, - 0.568963, 2.229104, 1.278440, 0.744293, - 0.555642, 2.103213, 1.212448, 0.766314, - 0.540934, 1.985370, 1.146287, 0.788164, - 0.525271, 1.878842, 1.082600, 0.809019, - 0.507986, 1.779821, 1.019978, 0.830947, - 0.489717, 1.691630, 0.956931, 0.853732, - 0.469345, 1.607513, 0.894207, 0.874904, - 0.447618, 1.531176, 0.833436, 0.897289, - 0.426124, 1.467302, 0.773611, 0.919226, - 0.404025, 1.408321, 0.716016, 0.940860, - 0.381454, 1.356209, 0.659515, 0.962764, - 0.358901, 1.310082, 0.604629, 0.984322, - 0.335983, 1.268485, 0.552335, 1.005343, - 0.312533, 1.230662, 0.501591, 1.028153, - 0.289452, 1.199168, 0.452032, 1.049283, - 0.265754, 1.168575, 0.404347, 1.073687, - 0.242571, 1.143533, 0.357445, 1.097546, - 0.218681, 1.119859, 0.312534, 1.123340, - 0.194465, 1.099634, 0.269437, 1.148166, - 0.168797, 1.081968, 0.228586, 1.172518, - 0.141552, 1.068789, 0.189866, 1.192930, - 0.113325, 1.057548, 0.152772, 1.206816, - 0.084800, 1.044145, 0.115390, 1.215045, - 0.056019, 1.028938, 0.076493, 1.220048, - 0.027733, 1.013338, 0.037767, 1.225852, 0.000050, 0.999927, - 0.000160, 0.000178, - 0.000422, 4.587902, 2.372253, 0.000504, - 0.001195, 6.624675, 2.372248, 0.002016, - 0.004782, 6.626884, 2.372187, 0.004531, - 0.010746, 6.607379, 2.372318, 0.008081, - 0.019161, 6.640102, 2.372084, 0.012637, - 0.029945, 6.653708, 2.372128, 0.018167, - 0.042999, 6.623837, 2.371902, 0.024769, - 0.058516, 6.624484, 2.371595, 0.032421, - 0.076370, 6.620877, 2.371120, 0.041164, - 0.096474, 6.615235, 2.370428, 0.051057, - 0.118786, 6.607844, 2.369440, 0.062136, - 0.143390, 6.599216, 2.368075, 0.074490, - 0.170034, 6.588018, 2.366218, 0.088179, - 0.198717, 6.572526, 2.363747, 0.103307, - 0.229147, 6.551868, 2.360517, 0.119964, - 0.261253, 6.526089, 2.356304, 0.138173, - 0.294703, 6.489593, 2.350797, 0.158072, - 0.329261, 6.443573, 2.343783, 0.179592, - 0.364298, 6.379764, 2.334673, 0.202709, - 0.399375, 6.295845, 2.323125, 0.227335, - 0.433616, 6.184929, 2.308547, 0.253230, - 0.466794, 6.045905, 2.289980, 0.280100, - 0.497509, 5.871803, 2.266964, 0.308146, - 0.525956, 5.672422, 2.239074, 0.336544, - 0.551101, 5.443256, 2.204809, 0.365223, - 0.572471, 5.188034, 2.164827, 0.395484, - 0.592088, 4.943783, 2.119489, 0.424416, - 0.606026, 4.666400, 2.067262, 0.455641, - 0.619671, 4.418961, 2.009937, 0.485298, - 0.627583, 4.152737, 1.948900, 0.514774, - 0.632072, 3.893344, 1.882692, 0.544172, - 0.634033, 3.645332, 1.814073, 0.573283, - 0.633239, 3.414651, 1.742717, 0.602155, - 0.630008, 3.195712, 1.669703, 0.630520, - 0.624550, 2.994536, 1.596021, 0.657121, - 0.615749, 2.799373, 1.522572, 0.682071, - 0.604738, 2.616102, 1.448978, 0.707605, - 0.593301, 2.456112, 1.376250, 0.731492, - 0.579628, 2.303517, 1.305297, 0.754139, - 0.564473, 2.165340, 1.235548, 0.776505, - 0.548787, 2.041646, 1.167051, 0.796833, - 0.531415, 1.923334, 1.100534, 0.817565, - 0.513778, 1.818176, 1.035144, 0.837981, - 0.495167, 1.723830, 0.971583, 0.858513, - 0.475690, 1.638448, 0.908841, 0.879892, - 0.454099, 1.559420, 0.846701, 0.902258, - 0.432038, 1.491471, 0.785332, 0.924114, - 0.409316, 1.428878, 0.726409, 0.944230, - 0.385618, 1.370785, 0.668588, 0.967001, - 0.362604, 1.323529, 0.612943, 0.988579, - 0.339117, 1.279679, 0.559038, 1.010210, - 0.315355, 1.240104, 0.506867, 1.032084, - 0.291408, 1.205261, 0.456934, 1.054671, - 0.267387, 1.175197, 0.407792, 1.078314, - 0.243346, 1.148153, 0.360992, 1.102443, - 0.219205, 1.123799, 0.315577, 1.128524, - 0.194996, 1.102624, 0.271742, 1.153989, - 0.169897, 1.085134, 0.230702, 1.179420, - 0.143960, 1.071699, 0.192146, 1.200098, - 0.116173, 1.060179, 0.155164, 1.214837, - 0.086655, 1.046290, 0.117071, 1.222749, - 0.056956, 1.030040, 0.077450, 1.227273, - 0.027883, 1.013650, 0.038092, 1.233293, 0.000831, 1.000043, - 0.000462, 0.000173, - 0.000442, 4.741539, 2.547922, 0.000504, - 0.001284, 7.491127, 2.547919, 0.002014, - 0.005132, 7.484889, 2.547844, 0.004523, - 0.011521, 7.439875, 2.547587, 0.008059, - 0.020524, 7.483694, 2.547725, 0.012586, - 0.032029, 7.470912, 2.547685, 0.018081, - 0.045948, 7.422534, 2.547686, 0.024783, - 0.062844, 7.487581, 2.547107, 0.032451, - 0.082011, 7.483603, 2.546522, 0.041233, - 0.103540, 7.475124, 2.545684, 0.051181, - 0.127537, 7.467521, 2.544438, 0.062347, - 0.153921, 7.456266, 2.542744, 0.074829, - 0.182427, 7.440422, 2.540459, 0.088703, - 0.213134, 7.420694, 2.537380, 0.104080, - 0.245750, 7.394875, 2.533347, 0.121050, - 0.279941, 7.358515, 2.528069, 0.139697, - 0.315591, 7.313001, 2.521237, 0.160036, - 0.351980, 7.246342, 2.512378, 0.182147, - 0.388993, 7.163688, 2.500993, 0.205799, - 0.425570, 7.048339, 2.486450, 0.231091, - 0.461093, 6.902586, 2.468174, 0.257405, - 0.494668, 6.712721, 2.444774, 0.284956, - 0.525889, 6.491261, 2.415538, 0.313180, - 0.553693, 6.232833, 2.380610, 0.342327, - 0.578724, 5.953834, 2.338525, 0.371689, - 0.599706, 5.649698, 2.290256, 0.401919, - 0.617615, 5.347900, 2.235157, 0.432204, - 0.631632, 5.036417, 2.173932, 0.463151, - 0.643082, 4.735976, 2.107298, 0.493388, - 0.649970, 4.432044, 2.036121, 0.524128, - 0.654188, 4.145472, 1.961595, 0.553930, - 0.654671, 3.866877, 1.883602, 0.583856, - 0.653051, 3.607848, 1.804521, 0.611762, - 0.646994, 3.356237, 1.724047, 0.639117, - 0.638860, 3.122531, 1.643016, 0.666279, - 0.629093, 2.913178, 1.563932, 0.692936, - 0.617862, 2.722675, 1.484614, 0.716498, - 0.603279, 2.536926, 1.406734, 0.742273, - 0.589878, 2.381054, 1.331469, 0.764031, - 0.572744, 2.228312, 1.256796, 0.786601, - 0.555933, 2.095451, 1.185290, 0.807776, - 0.537992, 1.972866, 1.115940, 0.828400, - 0.519596, 1.863394, 1.048371, 0.847412, - 0.499847, 1.760630, 0.982934, 0.866850, - 0.479920, 1.670998, 0.919972, 0.886340, - 0.459434, 1.587962, 0.858100, 0.906933, - 0.437767, 1.515505, 0.796714, 0.927490, - 0.414068, 1.448243, 0.736162, 0.950217, - 0.390910, 1.390505, 0.677613, 0.971545, - 0.366964, 1.337865, 0.620477, 0.992901, - 0.342603, 1.291104, 0.565807, 1.015460, - 0.318596, 1.251138, 0.513086, 1.037859, - 0.294242, 1.214291, 0.461573, 1.060535, - 0.269601, 1.182517, 0.411838, 1.086885, - 0.245608, 1.155300, 0.363221, 1.111237, - 0.220589, 1.129715, 0.317174, 1.138718, - 0.196008, 1.108103, 0.273213, 1.164223, - 0.170408, 1.089640, 0.231968, 1.187256, - 0.144205, 1.074145, 0.192987, 1.207851, - 0.116945, 1.061615, 0.156118, 1.222217, - 0.088852, 1.047599, 0.118674, 1.230315, - 0.059381, 1.030869, 0.078993, 1.235052, - 0.029145, 1.014126, 0.038924, 1.241359, 0.000479, 1.000114, - 0.000211, 0.000169, - 0.000465, 4.953966, 2.747437, 0.000504, - 0.001384, 8.544530, 2.747430, 0.002015, - 0.005537, 8.545147, 2.747339, 0.004542, - 0.012477, 8.557734, 2.747125, 0.008064, - 0.022143, 8.530193, 2.747341, 0.012543, - 0.034411, 8.465151, 2.747411, 0.018178, - 0.049792, 8.543328, 2.746874, 0.024810, - 0.067784, 8.547247, 2.746396, 0.032489, - 0.088416, 8.537436, 2.745730, 0.041313, - 0.111580, 8.526655, 2.744596, 0.051332, - 0.137462, 8.517438, 2.743082, 0.062603, - 0.165860, 8.502803, 2.740950, 0.075240, - 0.196548, 8.481507, 2.738057, 0.089341, - 0.229440, 8.454287, 2.734174, 0.105021, - 0.264395, 8.420289, 2.729086, 0.122399, - 0.301020, 8.373503, 2.722420, 0.141526, - 0.338997, 8.309059, 2.713686, 0.162451, - 0.377589, 8.221539, 2.702492, 0.185098, - 0.416349, 8.100116, 2.687893, 0.209406, - 0.454284, 7.941704, 2.669386, 0.235098, - 0.490450, 7.733318, 2.645590, 0.262100, - 0.524592, 7.486120, 2.615709, 0.290103, - 0.555558, 7.193498, 2.579231, 0.319135, - 0.583516, 6.874796, 2.534957, 0.348286, - 0.606714, 6.516118, 2.483017, 0.378840, - 0.627850, 6.163912, 2.424214, 0.409608, - 0.644715, 5.801404, 2.357563, 0.440553, - 0.657657, 5.435955, 2.285835, 0.470599, - 0.665621, 5.063481, 2.207940, 0.503172, - 0.673767, 4.743532, 2.126440, 0.533884, - 0.676009, 4.413409, 2.040694, 0.563808, - 0.674536, 4.092169, 1.953979, 0.591849, - 0.668913, 3.787057, 1.865897, 0.621474, - 0.663159, 3.520578, 1.777762, 0.650500, - 0.655018, 3.275065, 1.689902, 0.678011, - 0.643949, 3.043141, 1.603528, 0.703490, - 0.630030, 2.827104, 1.519484, 0.728250, - 0.614910, 2.632620, 1.436677, 0.752165, - 0.598649, 2.455570, 1.355753, 0.775894, - 0.581771, 2.295932, 1.278884, 0.797650, - 0.563193, 2.152291, 1.202767, 0.818505, - 0.543750, 2.022099, 1.130338, 0.838596, - 0.524017, 1.903562, 1.060263, 0.858396, - 0.504064, 1.797204, 0.993077, 0.877088, - 0.483418, 1.701208, 0.928606, 0.896606, - 0.462786, 1.617736, 0.866039, 0.914342, - 0.440943, 1.539227, 0.804293, 0.933550, - 0.419129, 1.470383, 0.745206, 0.955237, - 0.396100, 1.409100, 0.685832, 0.976700, - 0.371743, 1.354930, 0.627953, 0.997681, - 0.346882, 1.305249, 0.572127, 1.020784, - 0.322391, 1.262603, 0.517941, 1.043840, - 0.297564, 1.225115, 0.466188, 1.067224, - 0.272639, 1.190817, 0.415499, 1.092358, - 0.247664, 1.161265, 0.366782, 1.117573, - 0.222260, 1.133935, 0.319377, 1.145730, - 0.196933, 1.111750, 0.275293, 1.170822, - 0.170577, 1.091981, 0.233306, 1.194559, - 0.143878, 1.075810, 0.193950, 1.214819, - 0.116347, 1.062438, 0.156724, 1.229830, - 0.088233, 1.048092, 0.118984, 1.238185, - 0.059408, 1.031325, 0.079385, 1.243527, - 0.030703, 1.014698, 0.039893, 1.249724, - 0.001520, 0.999819, 0.000760, 0.000164, - 0.000489, 5.157359, 2.976300, 0.000505, - 0.001502, 9.891415, 2.976286, 0.002016, - 0.006000, 9.857730, 2.976197, 0.004543, - 0.013519, 9.870651, 2.975832, 0.008064, - 0.023985, 9.855780, 2.976170, 0.012611, - 0.037471, 9.850209, 2.975941, 0.018162, - 0.053866, 9.827134, 2.974968, 0.024820, - 0.073390, 9.849955, 2.975010, 0.032545, - 0.095758, 9.842021, 2.974073, 0.041418, - 0.120834, 9.829989, 2.972700, 0.051511, - 0.148861, 9.817421, 2.970736, 0.062920, - 0.179456, 9.797347, 2.968033, 0.075744, - 0.212674, 9.771533, 2.964371, 0.090131, - 0.248193, 9.735924, 2.959437, 0.106187, - 0.285748, 9.687707, 2.952881, 0.124035, - 0.325017, 9.622684, 2.944273, 0.143733, - 0.365463, 9.531452, 2.933093, 0.165262, - 0.406157, 9.401732, 2.918484, 0.188622, - 0.446833, 9.232451, 2.899529, 0.213693, - 0.486209, 9.013432, 2.875137, 0.239987, - 0.522925, 8.725671, 2.844166, 0.267796, - 0.557452, 8.400028, 2.805649, 0.296547, - 0.588266, 8.023041, 2.758720, 0.325838, - 0.614837, 7.606773, 2.702676, 0.355479, - 0.636760, 7.160680, 2.638483, 0.386984, - 0.657230, 6.736765, 2.566849, 0.418853, - 0.673592, 6.313742, 2.488091, 0.450302, - 0.684966, 5.884479, 2.402458, 0.481149, - 0.691591, 5.455771, 2.311816, 0.512177, - 0.695337, 5.055698, 2.217330, 0.543437, - 0.696370, 4.681506, 2.121285, 0.574309, - 0.694186, 4.334716, 2.024160, 0.604787, - 0.689158, 4.008524, 1.927738, 0.633483, - 0.680580, 3.703505, 1.830456, 0.660766, - 0.669088, 3.418386, 1.734934, 0.688471, - 0.656673, 3.168101, 1.642316, 0.715729, - 0.642820, 2.941735, 1.550744, 0.740435, - 0.626155, 2.730570, 1.463345, 0.764114, - 0.608299, 2.537561, 1.378151, 0.787028, - 0.589519, 2.364323, 1.297630, 0.807985, - 0.569257, 2.207970, 1.217830, 0.830663, - 0.550055, 2.076646, 1.142746, 0.850416, - 0.528812, 1.948085, 1.070757, 0.869609, - 0.507478, 1.834684, 1.001282, 0.888324, - 0.486131, 1.734879, 0.934987, 0.907482, - 0.464910, 1.645974, 0.871203, 0.924829, - 0.442742, 1.563550, 0.809260, 0.942958, - 0.420777, 1.491264, 0.750037, 0.961999, - 0.398842, 1.428069, 0.691715, 0.981043, - 0.375967, 1.369668, 0.635669, 1.002371, - 0.351469, 1.318588, 0.578689, 1.025343, - 0.326601, 1.273628, 0.524424, 1.048511, - 0.301395, 1.234572, 0.471403, 1.072242, - 0.275835, 1.198354, 0.419950, 1.096758, - 0.250200, 1.166392, 0.370733, 1.122781, - 0.224474, 1.138991, 0.322864, 1.150871, - 0.198592, 1.114313, 0.277723, 1.177319, - 0.171805, 1.093534, 0.234950, 1.201765, - 0.144291, 1.077462, 0.195376, 1.222629, - 0.115949, 1.063288, 0.157315, 1.237334, - 0.087140, 1.048366, 0.118843, 1.246153, - 0.058094, 1.031224, 0.079207, 1.252570, - 0.029194, 1.014695, 0.039376, 1.259060, - 0.000418, 0.999881, 0.000307, 0.000159, - 0.000515, 5.393984, 3.241865, 0.000505, - 0.001636, 11.548038, 3.241848, 0.002016, - 0.006534, 11.506640, 3.241718, 0.004537, - 0.014706, 11.513460, 3.241196, 0.008068, - 0.026134, 11.510533, 3.241693, 0.012573, - 0.040676, 11.428978, 3.241030, 0.018212, - 0.058794, 11.510745, 3.240924, 0.024847, - 0.079926, 11.497339, 3.240201, 0.032603, - 0.104160, 11.484607, 3.238994, 0.041543, - 0.131552, 11.470801, 3.237182, 0.051738, - 0.162012, 11.453219, 3.234635, 0.063313, - 0.195260, 11.427244, 3.231153, 0.076381, - 0.231205, 11.388534, 3.226361, 0.091096, - 0.269678, 11.340406, 3.219943, 0.107600, - 0.310170, 11.270127, 3.211448, 0.126017, - 0.352435, 11.178583, 3.200168, 0.146411, - 0.395551, 11.046559, 3.185328, 0.168663, - 0.438627, 10.858624, 3.165972, 0.192730, - 0.480660, 10.606379, 3.140735, 0.218497, - 0.520987, 10.288093, 3.108388, 0.245752, - 0.558483, 9.907480, 3.067586, 0.273993, - 0.592090, 9.453246, 3.016931, 0.303495, - 0.622416, 8.966138, 2.956444, 0.333717, - 0.648303, 8.443776, 2.885116, 0.363928, - 0.668640, 7.894122, 2.805963, 0.396240, - 0.687748, 7.385728, 2.718338, 0.427161, - 0.699903, 6.838511, 2.622346, 0.460175, - 0.711210, 6.356371, 2.522476, 0.492593, - 0.717734, 5.878312, 2.417984, 0.524449, - 0.719956, 5.423285, 2.310941, 0.556010, - 0.719127, 4.997909, 2.201885, 0.587032, - 0.715077, 4.600426, 2.093330, 0.617030, - 0.707574, 4.235885, 1.986585, 0.644684, - 0.695781, 3.881712, 1.881279, 0.674483, - 0.685313, 3.590960, 1.777918, 0.700290, - 0.669619, 3.303138, 1.678004, 0.727892, - 0.654728, 3.057771, 1.581162, 0.751694, - 0.635727, 2.826642, 1.487769, 0.776271, - 0.617343, 2.622178, 1.399628, 0.799502, - 0.597683, 2.441265, 1.313195, 0.821768, - 0.577090, 2.276954, 1.232316, 0.841960, - 0.555165, 2.125744, 1.153914, 0.861582, - 0.532983, 1.991236, 1.079598, 0.881460, - 0.510933, 1.874027, 1.008883, 0.899952, - 0.488321, 1.766812, 0.940802, 0.918954, - 0.466405, 1.673436, 0.875653, 0.936130, - 0.443623, 1.586986, 0.813130, 0.954799, - 0.421532, 1.513558, 0.752241, 0.972435, - 0.398897, 1.445787, 0.694711, 0.990147, - 0.376302, 1.384382, 0.638770, 1.009189, - 0.353623, 1.331934, 0.583826, 1.029687, - 0.330635, 1.284478, 0.530476, 1.052604, - 0.305698, 1.243632, 0.477187, 1.076524, - 0.279917, 1.204997, 0.425349, 1.101701, - 0.253951, 1.171750, 0.375165, 1.127264, - 0.227541, 1.142519, 0.326869, 1.156397, - 0.201265, 1.116817, 0.280912, 1.183020, - 0.173943, 1.095289, 0.237447, 1.208448, - 0.145860, 1.078296, 0.196694, 1.230417, - 0.116901, 1.064416, 0.158409, 1.248617, - 0.087507, 1.050504, 0.119483, 1.257310, - 0.057353, 1.032796, 0.079092, 1.263076, - 0.027785, 1.015128, 0.038883, 1.269870, 0.001331, 0.999935, - 0.000557, 0.000154, - 0.000549, 5.705205, 3.554136, 0.000506, - 0.001797, 13.703335, 3.554133, 0.002014, - 0.007156, 13.614074, 3.553937, 0.004544, - 0.016145, 13.657344, 3.553096, 0.008070, - 0.028652, 13.627997, 3.553894, 0.012584, - 0.044617, 13.606235, 3.554000, 0.018180, - 0.064288, 13.581339, 3.549637, 0.024887, - 0.087627, 13.608851, 3.552006, 0.032690, - 0.114134, 13.599099, 3.550341, 0.041705, - 0.144154, 13.579829, 3.547982, 0.052035, - 0.177400, 13.552845, 3.544641, 0.063810, - 0.213813, 13.515619, 3.539941, 0.077171, - 0.252978, 13.460460, 3.533696, 0.092329, - 0.294852, 13.393559, 3.524977, 0.109390, - 0.338688, 13.292376, 3.513655, 0.128455, - 0.384018, 13.147332, 3.498484, 0.149661, - 0.429960, 12.945774, 3.478323, 0.172694, - 0.475024, 12.658979, 3.451862, 0.197650, - 0.518614, 12.289564, 3.417602, 0.224156, - 0.559298, 11.828307, 3.372913, 0.252008, - 0.596110, 11.285162, 3.317454, 0.281165, - 0.629292, 10.684922, 3.251171, 0.311434, - 0.658379, 10.052939, 3.172222, 0.342741, - 0.683455, 9.405296, 3.082825, 0.373543, - 0.701674, 8.716078, 2.983976, 0.407008, - 0.719664, 8.108425, 2.876244, 0.438623, - 0.729882, 7.461252, 2.763279, 0.471872, - 0.738696, 6.880182, 2.645590, 0.504700, - 0.743136, 6.324308, 2.524680, 0.537118, - 0.743676, 5.808302, 2.402723, 0.569412, - 0.741181, 5.332306, 2.281437, 0.598202, - 0.732348, 4.857402, 2.161401, 0.629640, - 0.724832, 4.465554, 2.043872, 0.659239, - 0.713435, 4.093661, 1.930129, 0.686547, - 0.698539, 3.752593, 1.817654, 0.715529, - 0.684471, 3.457593, 1.712567, 0.739456, - 0.664983, 3.171220, 1.610687, 0.764892, - 0.646322, 2.929674, 1.512031, 0.789301, - 0.626393, 2.710719, 1.419033, 0.809881, - 0.603498, 2.506139, 1.330115, 0.833385, - 0.582934, 2.336089, 1.245859, 0.854254, - 0.560419, 2.178470, 1.165042, 0.873964, - 0.537294, 2.040087, 1.086633, 0.893433, - 0.514264, 1.911969, 1.015028, 0.911756, - 0.490657, 1.799840, 0.944938, 0.930894, - 0.467601, 1.703188, 0.878743, 0.948078, - 0.444043, 1.612092, 0.815356, 0.966162, - 0.421155, 1.534444, 0.753883, 0.984166, - 0.398238, 1.462397, 0.695534, 1.002184, - 0.375278, 1.400793, 0.638806, 1.019669, - 0.352159, 1.344172, 0.584549, 1.039571, - 0.329651, 1.295227, 0.531660, 1.059989, - 0.306804, 1.251281, 0.480529, 1.081116, - 0.283345, 1.211504, 0.430071, 1.105742, - 0.258568, 1.176400, 0.380277, 1.133080, - 0.232146, 1.144519, 0.331076, 1.161888, - 0.205244, 1.118059, 0.284040, 1.192408, - 0.177932, 1.097561, 0.239958, 1.221043, - 0.149532, 1.082021, 0.198751, 1.244141, - 0.120046, 1.067634, 0.160114, 1.259465, - 0.089542, 1.051626, 0.121101, 1.268124, - 0.058593, 1.033296, 0.079898, 1.274330, - 0.028011, 1.015382, 0.039038, 1.281590, 0.002330, 1.000087, - 0.001259, 0.000149, - 0.000587, 6.059834, 3.927143, 0.000507, - 0.001992, 16.560400, 3.927149, 0.002014, - 0.007910, 16.406326, 3.926821, 0.004549, - 0.017856, 16.545532, 3.927027, 0.008064, - 0.031632, 16.375853, 3.925487, 0.012450, - 0.048749, 15.928564, 3.928272, 0.018030, - 0.070371, 16.072989, 3.917862, 0.024964, - 0.096897, 16.458925, 3.924489, 0.032807, - 0.126073, 16.377851, 3.921896, 0.041917, - 0.159205, 16.351561, 3.918860, 0.052416, - 0.195762, 16.307037, 3.914339, 0.064464, - 0.235784, 16.255514, 3.907954, 0.078225, - 0.278812, 16.176226, 3.899254, 0.093900, - 0.324457, 16.066530, 3.887455, 0.111657, - 0.372174, 15.913818, 3.871777, 0.131478, - 0.420530, 15.669197, 3.850776, 0.153574, - 0.469330, 15.355453, 3.822348, 0.177505, - 0.516029, 14.908978, 3.785168, 0.203383, - 0.560585, 14.352687, 3.736602, 0.230569, - 0.600607, 13.666022, 3.675046, 0.259188, - 0.636296, 12.900244, 3.599811, 0.289272, - 0.668312, 12.111226, 3.510550, 0.320490, - 0.695986, 11.292102, 3.408535, 0.353031, - 0.719848, 10.493485, 3.295667, 0.385228, - 0.737073, 9.661955, 3.171998, 0.419219, - 0.752419, 8.909942, 3.042428, 0.452096, - 0.761179, 8.155107, 2.907108, 0.484909, - 0.766166, 7.450609, 2.769858, 0.518306, - 0.768596, 6.811866, 2.631935, 0.550067, - 0.765683, 6.205275, 2.492870, 0.582562, - 0.761197, 5.663215, 2.358645, 0.614450, - 0.753834, 5.165358, 2.227377, 0.644563, - 0.742860, 4.712554, 2.097547, 0.673658, - 0.729294, 4.306101, 1.974920, 0.702857, - 0.714839, 3.943352, 1.857613, 0.729350, - 0.696774, 3.609432, 1.743601, 0.754958, - 0.677394, 3.308389, 1.636607, 0.779575, - 0.657018, 3.043803, 1.533841, 0.800491, - 0.633342, 2.793592, 1.437092, 0.825030, - 0.612471, 2.590307, 1.344272, 0.847535, - 0.589882, 2.406477, 1.256436, 0.865979, - 0.564850, 2.231999, 1.173938, 0.886254, - 0.541357, 2.083556, 1.094722, 0.905566, - 0.517353, 1.950928, 1.021107, 0.924607, - 0.493320, 1.835979, 0.948941, 0.943365, - 0.469366, 1.731417, 0.881060, 0.960405, - 0.444745, 1.635838, 0.816479, 0.977893, - 0.420493, 1.552981, 0.754604, 0.996573, - 0.397150, 1.481595, 0.694917, 1.014000, - 0.373483, 1.414070, 0.638445, 1.031807, - 0.349985, 1.356031, 0.584035, 1.051877, - 0.327062, 1.305041, 0.530010, 1.071701, - 0.304134, 1.258836, 0.479439, 1.093109, - 0.280962, 1.217297, 0.429763, 1.116681, - 0.258121, 1.182063, 0.381050, 1.143886, - 0.235365, 1.150039, 0.333395, 1.175163, - 0.211621, 1.125074, 0.287477, 1.203675, - 0.184061, 1.102339, 0.243301, 1.230477, - 0.154815, 1.083927, 0.201826, 1.253134, - 0.124513, 1.067989, 0.162271, 1.270092, - 0.093383, 1.052032, 0.122855, 1.279576, - 0.061770, 1.033685, 0.081639, 1.286472, - 0.030317, 1.015583, 0.040411, 1.294476, 0.000964, 1.000206, - 0.000454, 0.000144, - 0.000630, 6.467978, 4.381146, 0.000504, - 0.002208, 20.193617, 4.381151, 0.002017, - 0.008834, 20.206446, 4.380687, 0.004536, - 0.019864, 20.183254, 4.380550, 0.008174, - 0.035759, 20.564249, 4.381247, 0.012608, - 0.055034, 20.111612, 4.382390, 0.018198, - 0.079119, 20.106096, 4.379815, 0.025057, - 0.108067, 20.215635, 4.376874, 0.032962, - 0.140630, 20.153549, 4.374143, 0.042199, - 0.177350, 20.084061, 4.369558, 0.052928, - 0.218094, 20.026609, 4.363287, 0.065327, - 0.262407, 19.940054, 4.354386, 0.079568, - 0.309833, 19.806814, 4.342127, 0.095961, - 0.360074, 19.641878, 4.325533, 0.114516, - 0.411747, 19.370914, 4.302950, 0.135349, - 0.463726, 18.983900, 4.271991, 0.158293, - 0.514211, 18.433926, 4.230856, 0.183348, - 0.562511, 17.733471, 4.176250, 0.209959, - 0.606310, 16.864214, 4.105895, 0.238736, - 0.646958, 15.935207, 4.020104, 0.268543, - 0.681574, 14.890014, 3.916094, 0.299996, - 0.712458, 13.846786, 3.798239, 0.331930, - 0.737130, 12.758296, 3.664191, 0.365222, - 0.758156, 11.732940, 3.521867, 0.399061, - 0.774364, 10.741743, 3.369831, 0.433480, - 0.786412, 9.812527, 3.212079, 0.467002, - 0.792373, 8.915130, 3.053715, 0.500754, - 0.795410, 8.094276, 2.894526, 0.534023, - 0.794617, 7.342067, 2.735959, 0.566988, - 0.790689, 6.664186, 2.581160, 0.599960, - 0.784433, 6.052983, 2.432318, 0.630599, - 0.773378, 5.486277, 2.287630, 0.660807, - 0.760334, 4.982516, 2.150183, 0.690103, - 0.745430, 4.531104, 2.017266, 0.717315, - 0.727511, 4.120734, 1.891699, 0.743819, - 0.708376, 3.759599, 1.772680, 0.770147, - 0.688632, 3.441912, 1.660620, 0.793510, - 0.665931, 3.152600, 1.553166, 0.816535, - 0.643045, 2.898883, 1.452080, 0.839163, - 0.619917, 2.674488, 1.355544, 0.859066, - 0.594923, 2.469262, 1.267232, 0.879489, - 0.570343, 2.292209, 1.181702, 0.898525, - 0.544975, 2.131086, 1.102089, 0.918359, - 0.520585, 1.994526, 1.024744, 0.937502, - 0.496044, 1.873079, 0.951712, 0.955573, - 0.471010, 1.761232, 0.883374, 0.972957, - 0.445712, 1.661604, 0.818008, 0.991248, - 0.421201, 1.577169, 0.754446, 1.008997, - 0.396444, 1.499653, 0.694518, 1.028127, - 0.372362, 1.432030, 0.637259, 1.045710, - 0.347895, 1.369870, 0.581515, 1.065977, - 0.324409, 1.317341, 0.527713, 1.087469, - 0.301181, 1.270447, 0.476281, 1.109943, - 0.277866, 1.228398, 0.426403, 1.134440, - 0.254849, 1.190986, 0.377822, 1.160986, - 0.231754, 1.157681, 0.330740, 1.188458, - 0.207973, 1.128665, 0.286014, 1.214405, - 0.183424, 1.103711, 0.243600, 1.239504, - 0.157972, 1.084253, 0.203686, 1.262961, - 0.130607, 1.068258, 0.165214, 1.280340, - 0.099652, 1.051919, 0.126067, 1.292129, - 0.067363, 1.034016, 0.084791, 1.299876, - 0.035026, 1.015775, 0.042786, 1.308328, - 0.002944, 0.999963, 0.001385, 0.000138, - 0.000681, 6.943771, 4.946556, 0.000503, - 0.002486, 25.346689, 4.946532, 0.002016, - 0.009973, 25.494320, 4.946311, 0.004539, - 0.022440, 25.484949, 4.945823, 0.008069, - 0.039836, 25.420902, 4.945311, 0.012628, - 0.062172, 25.394403, 4.945041, 0.018294, - 0.089609, 25.440279, 4.943295, 0.025079, - 0.121584, 25.399988, 4.939368, 0.033142, - 0.158595, 25.356537, 4.936200, 0.042596, - 0.199971, 25.295067, 4.929842, 0.053628, - 0.245624, 25.196465, 4.920586, 0.066496, - 0.295240, 25.055311, 4.907700, 0.081434, - 0.348006, 24.846170, 4.889647, 0.098640, - 0.403167, 24.527803, 4.864680, 0.118231, - 0.459106, 24.051735, 4.830574, 0.140139, - 0.513907, 23.352467, 4.783530, 0.164198, - 0.565953, 22.418245, 4.720530, 0.190502, - 0.614858, 21.324049, 4.638075, 0.218530, - 0.658304, 20.038671, 4.535464, 0.248094, - 0.696133, 18.639786, 4.411646, 0.279435, - 0.729388, 17.234526, 4.268872, 0.312002, - 0.757534, 15.830426, 4.109603, 0.346173, - 0.781866, 14.495901, 3.938782, 0.379435, - 0.797579, 13.136444, 3.756138, 0.414945, - 0.812334, 11.946491, 3.571258, 0.449991, - 0.821119, 10.811908, 3.384217, 0.484636, - 0.825066, 9.763482, 3.198076, 0.518675, - 0.824728, 8.796811, 3.015808, 0.552559, - 0.821710, 7.932528, 2.836886, 0.587272, - 0.817478, 7.185156, 2.664995, 0.616960, - 0.804441, 6.445302, 2.502223, 0.648054, - 0.792063, 5.818812, 2.345851, 0.678575, - 0.777793, 5.264731, 2.197150, 0.707287, - 0.760476, 4.766033, 2.056042, 0.735851, - 0.742541, 4.335871, 1.922805, 0.760594, - 0.720503, 3.928021, 1.798585, 0.784534, - 0.697719, 3.579153, 1.680605, 0.811029, - 0.677036, 3.285307, 1.568942, 0.831809, - 0.651479, 3.001423, 1.465496, 0.854364, - 0.627376, 2.760672, 1.367849, 0.872639, - 0.600496, 2.540697, 1.275644, 0.894296, - 0.576297, 2.355273, 1.188638, 0.913123, - 0.550377, 2.188563, 1.105652, 0.932025, - 0.524640, 2.040739, 1.028614, 0.949876, - 0.498402, 1.910315, 0.954421, 0.968933, - 0.473220, 1.795750, 0.884061, 0.985366, - 0.447086, 1.690336, 0.817765, 1.004940, - 0.422394, 1.599626, 0.753295, 1.022217, - 0.396726, 1.519055, 0.693380, 1.041490, - 0.371854, 1.448745, 0.635747, 1.059920, - 0.346769, 1.384292, 0.579508, 1.080408, - 0.322343, 1.328798, 0.525045, 1.101632, - 0.297979, 1.279898, 0.473773, 1.124812, - 0.274059, 1.234005, 0.422949, 1.148503, - 0.249954, 1.195373, 0.374609, 1.174554, - 0.225988, 1.160362, 0.327350, 1.202931, - 0.201932, 1.131307, 0.283494, 1.229335, - 0.176886, 1.105885, 0.241092, 1.254254, - 0.151225, 1.085802, 0.201514, 1.275743, - 0.124282, 1.068524, 0.162866, 1.292929, - 0.097122, 1.051493, 0.124991, 1.305805, - 0.068939, 1.033890, 0.085521, 1.314991, - 0.040082, 1.015927, 0.045247, 1.324033, - 0.009923, 0.999893, 0.004738, 0.000131, - 0.000745, 7.562414, 5.671075, 0.000473, - 0.002681, 27.216688, 5.670949, 0.002021, - 0.011462, 32.962402, 5.670177, 0.004540, - 0.025728, 33.183949, 5.670197, 0.008087, - 0.045746, 33.185688, 5.667313, 0.012673, - 0.071427, 33.170441, 5.668396, 0.018358, - 0.102673, 33.145138, 5.665252, 0.025299, - 0.139780, 33.303326, 5.653404, 0.033469, - 0.181718, 33.107243, 5.652829, 0.043139, - 0.228698, 32.859524, 5.645676, 0.054622, - 0.280648, 32.694893, 5.631547, 0.068115, - 0.336524, 32.422569, 5.611561, 0.083957, - 0.395671, 32.035511, 5.583449, 0.102259, - 0.456164, 31.415047, 5.543651, 0.123021, - 0.515765, 30.470440, 5.488278, 0.146127, - 0.572309, 29.186451, 5.413118, 0.171749, - 0.625710, 27.653852, 5.312369, 0.199549, - 0.673853, 25.902435, 5.185774, 0.229188, - 0.715905, 23.978609, 5.030582, 0.260421, - 0.751533, 21.999035, 4.853484, 0.293421, - 0.782309, 20.087366, 4.656137, 0.327077, - 0.806332, 18.186535, 4.443975, 0.361892, - 0.825818, 16.418409, 4.223844, 0.397146, - 0.840019, 14.774344, 3.998959, 0.434169, - 0.852434, 13.321097, 3.775443, 0.469288, - 0.856632, 11.929448, 3.552818, 0.504319, - 0.857130, 10.675201, 3.338825, 0.540067, - 0.855903, 9.591900, 3.130547, 0.575404, - 0.851565, 8.607655, 2.932930, 0.606782, - 0.839818, 7.690560, 2.743876, 0.638660, - 0.827508, 6.900781, 2.565115, 0.670577, - 0.814154, 6.216821, 2.395215, 0.696718, - 0.793162, 5.551886, 2.238233, 0.725990, - 0.775291, 5.015406, 2.090264, 0.754140, - 0.755758, 4.546843, 1.950834, 0.775992, - 0.729824, 4.094254, 1.820582, 0.802990, - 0.708909, 3.732984, 1.699191, 0.828291, - 0.686483, 3.413194, 1.583805, 0.847406, - 0.659162, 3.103861, 1.478093, 0.864951, - 0.631051, 2.832976, 1.378496, 0.887154, - 0.606590, 2.616645, 1.282127, 0.906337, - 0.580124, 2.413988, 1.194643, 0.927184, - 0.554835, 2.244380, 1.110354, 0.943810, - 0.527583, 2.081964, 1.031996, 0.963630, - 0.502243, 1.948979, 0.956718, 0.979691, - 0.475006, 1.822701, 0.886957, 0.997690, - 0.448815, 1.715714, 0.819006, 1.016460, - 0.423044, 1.621868, 0.754892, 1.035485, - 0.397637, 1.539537, 0.693707, 1.053165, - 0.371775, 1.462285, 0.634867, 1.072394, - 0.346372, 1.396193, 0.578574, 1.093397, - 0.321291, 1.338344, 0.524341, 1.115194, - 0.296102, 1.287594, 0.472059, 1.137943, - 0.271023, 1.240495, 0.421674, 1.164163, - 0.246367, 1.201224, 0.371963, 1.191457, - 0.221414, 1.164472, 0.325040, 1.220253, - 0.196228, 1.134325, 0.280343, 1.245456, - 0.169991, 1.108214, 0.238098, 1.270647, - 0.143314, 1.087277, 0.197886, 1.292124, - 0.115881, 1.069397, 0.159560, 1.309091, - 0.087816, 1.051426, 0.120547, 1.321130, - 0.059301, 1.032904, 0.080834, 1.332484, - 0.030912, 1.015767, 0.040933, 1.342834, - 0.002172, 0.999591, 0.001185, 0.000125, - 0.000830, 8.392562, 6.634228, 0.000443, - 0.002936, 29.687805, 6.634032, 0.002016, - 0.013374, 45.025234, 6.633008, 0.004540, - 0.030089, 45.020294, 6.633056, 0.008092, - 0.053499, 45.066029, 6.626466, 0.012710, - 0.083610, 44.810101, 6.630330, 0.018485, - 0.120260, 45.216747, 6.614516, 0.025134, - 0.161031, 44.674168, 6.600349, 0.033897, - 0.212161, 44.819195, 6.610186, 0.043978, - 0.266661, 44.450245, 6.593605, 0.056094, - 0.326582, 44.134544, 6.570142, 0.070528, - 0.390342, 43.591648, 6.536712, 0.087498, - 0.456162, 42.708160, 6.488329, 0.107138, - 0.521609, 41.365093, 6.420198, 0.129461, - 0.584225, 39.525822, 6.323702, 0.154245, - 0.641931, 37.186111, 6.193606, 0.181228, - 0.692829, 34.478470, 6.026897, 0.210711, - 0.738440, 31.680904, 5.825769, 0.242181, - 0.777397, 28.828054, 5.595428, 0.275337, - 0.809980, 26.042755, 5.342321, 0.309698, - 0.835990, 23.376804, 5.073076, 0.345702, - 0.858077, 20.965754, 4.794572, 0.382135, - 0.874122, 18.710079, 4.516676, 0.419871, - 0.887133, 16.713011, 4.241767, 0.455609, - 0.891199, 14.819674, 3.972124, 0.492617, - 0.894082, 13.187921, 3.717271, 0.528186, - 0.891270, 11.708584, 3.471719, 0.563462, - 0.885719, 10.422834, 3.237760, 0.596013, - 0.874241, 9.237741, 3.019060, 0.629455, - 0.862814, 8.248549, 2.813572, 0.661110, - 0.848126, 7.358398, 2.621046, 0.690314, - 0.829798, 6.569392, 2.441627, 0.720589, - 0.812314, 5.905934, 2.274629, 0.745631, - 0.788704, 5.276800, 2.119423, 0.771488, - 0.766133, 4.752773, 1.974380, 0.798704, - 0.744726, 4.306095, 1.839482, 0.820172, - 0.718062, 3.889792, 1.713244, 0.844368, - 0.693972, 3.545456, 1.594809, 0.863128, - 0.665748, 3.212762, 1.487512, 0.880094, - 0.637003, 2.926572, 1.386724, 0.904252, - 0.613728, 2.704260, 1.288131, 0.920506, - 0.585217, 2.483164, 1.199845, 0.940919, - 0.559603, 2.300348, 1.114958, 0.957044, - 0.531597, 2.130516, 1.034754, 0.972648, - 0.503583, 1.979313, 0.960912, 0.994318, - 0.478813, 1.859664, 0.889786, 1.008754, - 0.450943, 1.742705, 0.820833, 1.028667, - 0.425516, 1.645220, 0.756332, 1.046145, - 0.398977, 1.557184, 0.693921, 1.067212, - 0.373657, 1.480814, 0.635955, 1.084111, - 0.346657, 1.408762, 0.578832, 1.106749, - 0.321392, 1.350468, 0.523561, 1.128440, - 0.295773, 1.294865, 0.471146, 1.151073, - 0.270028, 1.246118, 0.420298, 1.178601, - 0.244816, 1.204226, 0.370575, 1.206845, - 0.219027, 1.166896, 0.323716, 1.235963, - 0.192622, 1.135756, 0.278058, 1.263030, - 0.165331, 1.109240, 0.235743, 1.288937, - 0.137489, 1.088379, 0.195390, 1.310681, - 0.108685, 1.068987, 0.156439, 1.334352, - 0.079710, 1.054273, 0.117096, 1.344847, - 0.049947, 1.034598, 0.076554, 1.354943, - 0.020272, 1.016079, 0.035585, 1.365515, 0.009170, 0.999969, - 0.004771, 0.000117, - 0.000935, 9.424866, 7.979243, 0.000410, - 0.003275, 33.013195, 7.979422, 0.002009, - 0.016024, 64.370331, 7.977156, 0.004541, - 0.036176, 64.655952, 7.976128, 0.008109, - 0.064384, 64.864494, 7.964988, 0.012694, - 0.099984, 64.487198, 7.971348, 0.018554, - 0.143991, 64.637970, 7.923116, 0.025303, - 0.192040, 61.930538, 7.953975, 0.035297, - 0.259442, 66.274422, 7.921861, 0.045226, - 0.318370, 63.334690, 7.909609, 0.058370, - 0.388821, 62.686401, 7.864696, 0.074083, - 0.461667, 61.332054, 7.801843, 0.092537, - 0.533744, 59.125607, 7.708949, 0.113781, - 0.601905, 55.997845, 7.575799, 0.137786, - 0.664409, 52.177567, 7.393524, 0.164770, - 0.721193, 48.019485, 7.161756, 0.193894, - 0.768842, 43.460278, 6.882018, 0.225586, - 0.810332, 39.086590, 6.564607, 0.259311, - 0.845096, 34.896049, 6.221983, 0.294517, - 0.872849, 30.952213, 5.865831, 0.331163, - 0.895159, 27.375792, 5.507064, 0.368964, - 0.912860, 24.213310, 5.149763, 0.407255, - 0.925338, 21.364958, 4.806172, 0.444704, - 0.930956, 18.791691, 4.472272, 0.482041, - 0.932576, 16.521160, 4.160864, 0.519572, - 0.931547, 14.589918, 3.865206, 0.556236, - 0.926554, 12.887797, 3.590445, 0.590431, - 0.915839, 11.352402, 3.332747, 0.622723, - 0.901266, 10.002660, 3.093264, 0.657029, - 0.888747, 8.905210, 2.873842, 0.686164, - 0.868666, 7.876704, 2.666740, 0.719168, - 0.853152, 7.051816, 2.479017, 0.742294, - 0.826169, 6.226034, 2.306498, 0.770320, - 0.804936, 5.590831, 2.141328, 0.792337, - 0.777772, 4.984083, 1.994663, 0.819050, - 0.755478, 4.507655, 1.853950, 0.837684, - 0.726072, 4.049884, 1.725590, 0.861324, - 0.701424, 3.678201, 1.606303, 0.880741, - 0.673615, 3.337163, 1.495452, 0.903335, - 0.648506, 3.055720, 1.391162, 0.920311, - 0.619640, 2.792068, 1.294734, 0.935769, - 0.590245, 2.554566, 1.204518, 0.956592, - 0.564944, 2.366468, 1.118630, 0.972424, - 0.536842, 2.187863, 1.038323, 0.986269, - 0.508020, 2.023480, 0.963803, 1.006122, - 0.482411, 1.895137, 0.890986, 1.022504, - 0.455110, 1.775886, 0.820936, 1.037905, - 0.427450, 1.665951, 0.758556, 1.059281, - 0.402198, 1.577363, 0.696126, 1.076613, - 0.375156, 1.493391, 0.636676, 1.097828, - 0.349577, 1.421129, 0.579947, 1.116671, - 0.322955, 1.355205, 0.525140, 1.140514, - 0.297406, 1.299979, 0.471460, 1.166473, - 0.271786, 1.249847, 0.420473, 1.192591, - 0.245461, 1.204625, 0.371118, 1.223349, - 0.219412, 1.166686, 0.322600, 1.254833, - 0.192660, 1.134121, 0.277572, 1.285808, - 0.165167, 1.108617, 0.234417, 1.322015, - 0.137236, 1.093841, 0.194640, 1.342172, - 0.106871, 1.074616, 0.155001, 1.357238, - 0.075759, 1.053550, 0.114648, 1.367725, - 0.044279, 1.033851, 0.073254, 1.379461, - 0.013001, 1.015713, 0.031895, 1.391625, 0.018075, 1.000203, - 0.009397, 0.000109, - 0.001093, 10.986820, 9.992467, 0.000378, - 0.003779, 37.989063, 9.992861, 0.002028, - 0.020252, 101.850441, 9.988345, 0.004557, - 0.045429, 101.106750, 9.983879, 0.008115, - 0.080453, 100.646606, 9.953411, 0.012864, - 0.125836, 101.366592, 9.943727, 0.018734, - 0.179350, 100.786118, 9.908408, 0.026314, - 0.243680, 99.779343, 9.821631, 0.035500, - 0.313552, 98.608231, 9.782450, 0.047562, - 0.394644, 97.689568, 9.845875, 0.062065, - 0.476697, 95.177795, 9.755218, 0.079552, - 0.557933, 91.095581, 9.615121, 0.099905, - 0.632818, 85.110382, 9.408299, 0.123231, - 0.699926, 77.948921, 9.120996, 0.149980, - 0.760671, 70.491119, 8.764173, 0.179550, - 0.812251, 62.821407, 8.341752, 0.211839, - 0.855909, 55.512890, 7.876337, 0.246434, - 0.892023, 48.744549, 7.386268, 0.282317, - 0.919200, 42.462059, 6.886009, 0.319580, - 0.940333, 36.901031, 6.400318, 0.360135, - 0.962176, 32.353752, 5.937503, 0.397805, - 0.969755, 27.996445, 5.489783, 0.437077, - 0.976494, 24.359192, 5.072855, 0.474388, - 0.975265, 21.124300, 4.684682, 0.513695, - 0.975335, 18.476677, 4.326597, 0.551542, - 0.970264, 16.167391, 3.999049, 0.587525, - 0.960365, 14.143442, 3.696317, 0.621251, - 0.945944, 12.374341, 3.414176, 0.654738, - 0.930709, 10.877112, 3.160455, 0.685794, - 0.911702, 9.580887, 2.921461, 0.717135, - 0.892948, 8.481939, 2.707478, 0.740798, - 0.865086, 7.435941, 2.510382, 0.770920, - 0.845137, 6.650625, 2.329648, 0.792303, - 0.815956, 5.879976, 2.163206, 0.818363, - 0.792225, 5.274404, 2.008042, 0.837362, - 0.762396, 4.700960, 1.867576, 0.862266, - 0.738465, 4.254798, 1.735819, 0.880069, - 0.708890, 3.828697, 1.614690, 0.896021, - 0.678588, 3.451655, 1.503477, 0.920156, - 0.654832, 3.168722, 1.395800, 0.934948, - 0.624740, 2.879533, 1.299955, 0.949686, - 0.595203, 2.628258, 1.208597, 0.970989, - 0.570041, 2.433689, 1.122310, 0.985606, - 0.541116, 2.241461, 1.042168, 1.000819, - 0.512835, 2.075567, 0.966543, 1.012209, - 0.483024, 1.919932, 0.895758, 1.035320, - 0.459125, 1.807884, 0.825668, 1.052077, - 0.432333, 1.695689, 0.760812, 1.070459, - 0.406131, 1.595491, 0.699897, 1.088704, - 0.379721, 1.508512, 0.640575, 1.103817, - 0.352104, 1.428159, 0.583765, 1.131711, - 0.328122, 1.366565, 0.528240, 1.156448, - 0.302568, 1.306843, 0.473988, 1.181821, - 0.276487, 1.252861, 0.422189, 1.211347, - 0.250540, 1.205265, 0.372005, 1.243636, - 0.224264, 1.165943, 0.324184, 1.283038, - 0.198289, 1.137772, 0.278419, 1.316722, - 0.170179, 1.115057, 0.235425, 1.342715, - 0.140095, 1.092994, 0.195084, 1.363288, - 0.108794, 1.071875, 0.155439, 1.380656, - 0.076774, 1.052475, 0.114636, 1.394826, - 0.044509, 1.032525, 0.072890, 1.408830, - 0.011968, 1.015459, 0.031101, 1.422370, 0.020555, 0.999808, - 0.011002, 0.000100, - 0.001334, 13.377127, 13.342275, 0.000342, - 0.004563, 45.758434, 13.342710, 0.002026, - 0.027004, 179.672058, 13.331846, 0.004559, - 0.060563, 179.294235, 13.314877, 0.008232, - 0.108154, 181.242035, 13.222856, 0.013031, - 0.167590, 179.684509, 13.153860, 0.019526, - 0.242041, 181.004608, 12.986094, 0.026364, - 0.309289, 159.606293, 13.247752, 0.037670, - 0.409755, 179.468521, 12.368877, 0.051804, - 0.512051, 167.955582, 12.981333, 0.068214, - 0.601994, 156.278793, 12.704532, 0.088295, - 0.686849, 143.096878, 12.316531, 0.111478, - 0.758670, 127.423111, 11.793048, 0.138336, - 0.821348, 111.763031, 11.157992, 0.168447, - 0.873616, 96.887924, 10.447472, 0.201411, - 0.916322, 83.225327, 9.696606, 0.237443, - 0.953090, 71.403137, 8.949244, 0.274234, - 0.977751, 60.739277, 8.225874, 0.314566, - 1.003135, 52.115578, 7.547433, 0.353932, - 1.016312, 44.341869, 6.910326, 0.393858, - 1.024848, 37.827263, 6.324401, 0.433805, - 1.028950, 32.380932, 5.790555, 0.475812, - 1.034084, 27.955982, 5.312826, 0.513254, - 1.026743, 23.977417, 4.866118, 0.549965, - 1.016740, 20.628025, 4.468437, 0.590300, - 1.012030, 18.036856, 4.105483, 0.626420, - 0.998919, 15.669224, 3.780593, 0.658897, - 0.979874, 13.603898, 3.482054, 0.687252, - 0.955238, 11.788331, 3.211213, 0.718941, - 0.935663, 10.355552, 2.962083, 0.749877, - 0.915206, 9.131123, 2.741382, 0.772094, - 0.884837, 7.973935, 2.536501, 0.799495, - 0.861214, 7.086230, 2.347282, 0.820136, - 0.830976, 6.240769, 2.179332, 0.846715, - 0.807408, 5.604792, 2.018005, 0.865176, - 0.776657, 4.975034, 1.877021, 0.881100, - 0.744657, 4.442767, 1.743528, 0.907637, - 0.722088, 4.035177, 1.621563, 0.922239, - 0.690432, 3.633160, 1.506158, 0.936558, - 0.659650, 3.281798, 1.403606, 0.950047, - 0.629105, 2.974179, 1.304276, 0.961959, - 0.598277, 2.704483, 1.213888, 0.987410, - 0.576085, 2.510453, 1.125569, 0.999996, - 0.546494, 2.304016, 1.045567, 1.014127, - 0.518186, 2.127867, 0.970718, 1.036275, - 0.494009, 1.985804, 0.897557, 1.049695, - 0.465659, 1.845074, 0.830584, 1.064617, - 0.438159, 1.725130, 0.766083, 1.077131, - 0.409813, 1.613818, 0.705101, 1.101054, - 0.385632, 1.528694, 0.644828, 1.122361, - 0.360045, 1.447086, 0.587878, 1.147359, - 0.335186, 1.377588, 0.532130, 1.169881, - 0.309040, 1.313673, 0.478843, 1.200554, - 0.284590, 1.257256, 0.426855, 1.232047, - 0.259332, 1.208431, 0.376125, 1.275402, - 0.235215, 1.174692, 0.326614, 1.306595, - 0.207508, 1.141042, 0.281524, 1.334304, - 0.178290, 1.111778, 0.238694, 1.364678, - 0.148530, 1.090976, 0.198549, 1.387168, - 0.117114, 1.069308, 0.158529, 1.408657, - 0.084977, 1.050625, 0.118042, 1.426214, - 0.052052, 1.031444, 0.076541, 1.444257, - 0.018653, 1.014298, 0.034061, 1.460618, 0.015206, 0.999413, - 0.008132, 0.000100, - 0.002003, 20.052612, 20.032721, 0.000297, - 0.005947, 59.540512, 20.033842, 0.002022, - 0.040439, 404.848511, 20.032743, 0.004588, - 0.090999, 403.741241, 19.910591, 0.008769, - 0.169802, 441.471558, 19.572552, 0.013708, - 0.253629, 411.667816, 19.145721, 0.020331, - 0.349396, 371.322571, 18.591049, 0.030259, - 0.468121, 385.816498, 18.331083, 0.045190, - 0.611444, 391.924133, 15.807686, 0.058476, - 0.676875, 319.638641, 16.947781, 0.079894, - 0.781421, 278.804260, 17.512903, 0.103871, - 0.855116, 235.999786, 16.290295, 0.131756, - 0.915747, 197.168076, 14.956566, 0.163487, - 0.966333, 163.452347, 13.608010, 0.198693, - 1.008386, 135.632706, 12.299661, 0.236157, - 1.039862, 111.919281, 11.088790, 0.274579, - 1.059988, 92.136581, 9.983883, 0.317164, - 1.084069, 77.063034, 9.008505, 0.357624, - 1.092124, 63.963051, 8.127298, 0.399009, - 1.097560, 53.483341, 7.347628, 0.441182, - 1.100981, 45.052429, 6.658191, 0.481606, - 1.097318, 37.932640, 6.047333, 0.524253, - 1.096570, 32.395638, 5.505878, 0.564351, - 1.088739, 27.679380, 5.018494, 0.600843, - 1.073396, 23.611519, 4.580770, 0.635527, - 1.055024, 20.207081, 4.194785, 0.672045, - 1.039775, 17.469036, 3.847436, 0.698372, - 1.009545, 14.928226, 3.532546, 0.729336, - 0.987168, 12.953170, 3.248834, 0.761147, - 0.966299, 11.346271, 2.994166, 0.782270, - 0.932841, 9.813129, 2.762244, 0.811832, - 0.910431, 8.672224, 2.549933, 0.832053, - 0.878369, 7.578633, 2.363132, 0.849383, - 0.844673, 6.648379, 2.189266, 0.866020, - 0.811703, 5.850784, 2.031716, 0.893083, - 0.789181, 5.273372, 1.884480, 0.909212, - 0.757541, 4.700618, 1.750298, 0.923169, - 0.725157, 4.196640, 1.627590, 0.937112, - 0.693769, 3.764841, 1.514906, 0.961901, - 0.670828, 3.444598, 1.406839, 0.975245, - 0.640240, 3.120745, 1.307873, 0.989696, - 0.611032, 2.840732, 1.216417, 1.002057, - 0.581144, 2.591596, 1.132553, 1.014022, - 0.551620, 2.373820, 1.051695, 1.025307, - 0.522268, 2.177992, 0.977500, 1.052190, - 0.500826, 2.042511, 0.904301, 1.064408, - 0.472355, 1.891934, 0.837557, 1.077876, - 0.444815, 1.761054, 0.773004, 1.088939, - 0.416531, 1.638939, 0.713958, 1.118551, - 0.395057, 1.555724, 0.652485, 1.134469, - 0.368289, 1.465490, 0.596330, 1.162778, - 0.345095, 1.390977, 0.539703, 1.185298, - 0.319527, 1.321225, 0.486250, 1.208419, - 0.293590, 1.259318, 0.434178, 1.261013, - 0.273471, 1.219767, 0.382032, 1.297811, - 0.248226, 1.176422, 0.334190, 1.326591, - 0.220354, 1.139881, 0.289075, 1.357918, - 0.191937, 1.111418, 0.246259, 1.387590, - 0.162282, 1.086511, 0.205129, 1.415797, - 0.131515, 1.067072, 0.165601, 1.440194, - 0.099555, 1.047799, 0.125462, 1.465600, - 0.066957, 1.030406, 0.084082, 1.487714, - 0.033496, 1.013889, 0.041981, 1.509947, 0.000663, 0.998773, - 0.000485, 0.000100, - 0.004009, 40.102047, 40.087105, 0.000228, - 0.009141, 91.431366, 40.074432, 0.001522, - 0.060544, 605.651733, 39.918827, 0.004919, - 0.188871, 1712.982300, 38.873421, 0.009053, - 0.320325, 1583.453125, 39.715633, 0.015375, - 0.471415, 1486.033691, 39.162876, 0.029306, - 0.735111, 1751.701050, 28.083200, 0.043450, - 0.859759, 1392.475220, 24.599945, 0.079075, - 1.220033, 1629.972656, 18.507019, 0.090130, - 1.091255, 940.347351, 17.961655, 0.098008, - 0.945965, 425.901093, 24.478010, 0.138246, - 1.084105, 416.823944, 20.003433, 0.174489, - 1.133148, 302.730042, 18.550846, 0.207969, - 1.138483, 242.853577, 15.923334, 0.249132, - 1.168197, 191.649445, 13.940813, 0.291391, - 1.187038, 152.910309, 12.263267, 0.332856, - 1.192793, 121.905075, 10.822873, 0.377473, - 1.202846, 99.145561, 9.618412, 0.422601, - 1.208871, 81.343315, 8.591735, 0.465276, - 1.204545, 66.742569, 7.692911, 0.504710, - 1.190839, 54.787876, 6.915612, 0.544909, - 1.178827, 45.507313, 6.242786, 0.582125, - 1.160590, 37.819912, 5.651690, 0.620694, - 1.145481, 31.926588, 5.123660, 0.659127, - 1.130178, 27.147310, 4.669475, 0.684358, - 1.093728, 22.650702, 4.258717, 0.719453, - 1.074591, 19.454103, 3.901225, 0.751695, - 1.051678, 16.735672, 3.576870, 0.775082, - 1.017716, 14.281039, 3.287471, 0.796233, - 0.982759, 12.261332, 3.023708, 0.827404, - 0.961227, 10.767912, 2.787740, 0.848149, - 0.928433, 9.371350, 2.570737, 0.864891, - 0.892838, 8.142364, 2.379204, 0.880979, - 0.858193, 7.118954, 2.204470, 0.910434, - 0.837281, 6.389041, 2.041554, 0.925396, - 0.803638, 5.643217, 1.893353, 0.942463, - 0.772925, 5.031223, 1.757331, 0.955217, - 0.739720, 4.486978, 1.633572, 0.968570, - 0.708048, 4.014621, 1.520414, 0.981672, - 0.677109, 3.617768, 1.412506, 0.992829, - 0.645712, 3.258773, 1.317079, 1.021270, - 0.625746, 3.006640, 1.222611, 1.031247, - 0.594628, 2.733073, 1.137911, 1.043581, - 0.565540, 2.498495, 1.058439, 1.055930, - 0.536962, 2.289843, 0.984428, 1.066727, - 0.508076, 2.108603, 0.912794, 1.081225, - 0.481297, 1.951454, 0.845536, 1.088198, - 0.451563, 1.801891, 0.782718, 1.123316, - 0.433247, 1.704316, 0.721664, 1.133206, - 0.404812, 1.586153, 0.662761, 1.152889, - 0.379901, 1.490958, 0.606866, 1.188158, - 0.359421, 1.415730, 0.550666, 1.217064, - 0.336049, 1.344172, 0.496748, 1.257727, - 0.314816, 1.283196, 0.443538, 1.286647, - 0.289530, 1.225903, 0.394018, 1.308729, - 0.262053, 1.173928, 0.346255, 1.351453, - 0.237704, 1.139992, 0.300393, 1.380284, - 0.209733, 1.105997, 0.256661, 1.414621, - 0.181613, 1.082109, 0.215429, 1.453045, - 0.152797, 1.063853, 0.177098, 1.481066, - 0.121803, 1.043185, 0.137203, 1.514113, - 0.090250, 1.027072, 0.096998, 1.547317, - 0.057603, 1.012551, 0.055328, 1.577983, - 0.023799, 0.999267, 0.013094, 0.000108, - 0.124970, 1249.704346, 1249.703491, 0.000140, - 0.119585, 1195.855469, 1195.854370, 0.003995, - 0.927433, 9274.246094, 232.443573, 0.012013, - 1.131580, 11315.999023, 98.211105, 0.023892, - 1.216018, 12162.739258, 67.214500, 0.047506, - 1.517865, 15186.294922, 42.410069, 0.082523, - 1.812564, 18145.718750, 24.421545, 0.112452, - 1.805072, 11112.966797, 18.450365, 0.164460, - 2.016784, 8086.032715, 14.043465, 0.195870, - 1.898199, 4245.658203, 13.178202, 0.197797, - 1.556158, 1315.561768, 30.760096, 0.219540, - 1.433455, 802.380371, 25.037956, 0.268696, - 1.483235, 579.715515, 20.975695, 0.265968, - 1.261051, 386.583649, 12.017023, 0.325369, - 1.343349, 316.795959, 12.612406, 0.387968, - 1.411606, 232.491623, 13.296940, 0.435543, - 1.411236, 181.515228, 11.646996, 0.482729, - 1.405722, 143.425354, 10.265131, 0.531742, - 1.402782, 114.920082, 9.114828, 0.559383, - 1.346165, 88.589005, 8.089214, 0.607851, - 1.342407, 73.056610, 7.249064, 0.656928, - 1.338238, 60.826897, 6.531094, 0.681212, - 1.285692, 48.727219, 5.868711, 0.729238, - 1.279951, 41.256016, 5.324553, 0.751172, - 1.230045, 33.728260, 4.816513, 0.773107, - 1.184288, 27.913816, 4.377203, 0.815726, - 1.171653, 24.065962, 3.999965, 0.837886, - 1.130636, 20.254860, 3.658493, 0.857674, - 1.089071, 17.138168, 3.347930, 0.876120, - 1.048303, 14.572968, 3.072666, 0.893935, - 1.009040, 12.496377, 2.825165, 0.927998, - 0.989064, 11.040731, 2.605520, 0.928445, - 0.935017, 9.365102, 2.401481, 0.945279, - 0.899993, 8.177711, 2.222282, 0.959378, - 0.863854, 7.155303, 2.059342, 0.971761, - 0.827684, 6.284632, 1.909314, 0.987812, - 0.795878, 5.583837, 1.771094, 1.001958, - 0.763540, 4.962345, 1.645968, 1.014357, - 0.730897, 4.435898, 1.527438, 1.025946, - 0.698675, 3.973241, 1.421337, 1.036435, - 0.666662, 3.568025, 1.323677, 1.046807, - 0.635466, 3.218647, 1.232678, 1.052974, - 0.602660, 2.902273, 1.147675, 1.086089, - 0.585364, 2.694939, 1.068352, 1.094660, - 0.554784, 2.454491, 0.993445, 1.117131, - 0.531500, 2.270746, 0.923758, 1.114009, - 0.496581, 2.063934, 0.858381, 1.137328, - 0.473914, 1.917990, 0.794980, 1.158671, - 0.450127, 1.786523, 0.735697, 1.177878, - 0.425306, 1.662454, 0.677498, 1.207510, - 0.403797, 1.559058, 0.621762, 1.244496, - 0.383812, 1.466801, 0.566190, 1.240412, - 0.351080, 1.366853, 0.514288, 1.321257, - 0.341200, 1.309808, 0.464621, 1.336512, - 0.312710, 1.241822, 0.413228, 1.365047, - 0.286935, 1.186612, 0.366092, 1.418984, - 0.265184, 1.152120, 0.321528, 1.388864, - 0.227750, 1.089937, 0.271827, 1.464383, - 0.207168, 1.077271, 0.232838, 1.473125, - 0.175770, 1.041835, 0.193289, 1.542908, - 0.150424, 1.036794, 0.156153, 1.563005, - 0.118748, 1.013029, 0.114866, 1.637048, - 0.089604, 1.013493, 0.076804, 1.670777, - 0.056398, 0.999208, 0.032691]; + public static var ltc_mag = [1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999995, 0.999990, 0.999971, 0.999937, 0.999853, 0.999670, 0.999138, 0.996746, 0.979578, 0.979309, 0.978836, 0.977972, 0.976223, 0.972205, 0.962466, 0.953919, 0.949829, 0.942492, 0.929870, 0.921319, 0.911112, 0.896015, 0.885105, 0.869971, 0.855017, 0.838328, 0.821241, 0.802352, 0.783873, 0.763309, 0.743058, 0.721929, 0.699755, 0.677721, 0.655456, 0.632681, 0.609629, 0.586831, 0.564287, 0.541772, 0.519428, 0.497353, 0.475624, 0.454606, 0.434099, 0.414085, 0.394605, 0.375698, 0.357386, 0.339871, 0.323085, 0.306905, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999999, 0.999998, 0.999995, 0.999990, 0.999980, 0.999959, 0.999923, 0.999842, 0.999660, 0.999119, 0.996613, 0.981824, 0.979298, 0.978826, 0.977957, 0.976184, 0.972091, 0.962188, 0.953875, 0.949746, 0.942335, 0.930166, 0.921211, 0.910927, 0.896979, 0.884940, 0.869864, 0.854835, 0.838200, 0.821049, 0.802552, 0.783659, 0.763512, 0.742927, 0.721715, 0.699938, 0.677775, 0.655246, 0.632555, 0.609805, 0.586996, 0.564225, 0.541606, 0.519346, 0.497419, 0.475863, 0.454738, 0.434099, 0.414003, 0.394547, 0.375747, 0.357564, 0.340012, 0.323099, 0.306861, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999995, 0.999991, 0.999979, 0.999959, 0.999917, 0.999839, 0.999648, 0.999074, 0.996168, 0.983770, 0.979279, 0.978800, 0.977905, 0.976058, 0.971727, 0.962120, 0.953901, 0.949485, 0.941859, 0.930911, 0.920853, 0.910394, 0.897600, 0.884427, 0.870101, 0.854522, 0.838325, 0.820754, 0.802707, 0.783223, 0.763605, 0.742872, 0.721565, 0.699935, 0.677726, 0.655242, 0.632580, 0.609766, 0.586946, 0.564275, 0.541759, 0.519467, 0.497478, 0.475886, 0.454794, 0.434233, 0.414207, 0.394751, 0.375892, 0.357683, 0.340146, 0.323287, 0.307095, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999999, 0.999998, 0.999996, 0.999992, 0.999987, 0.999975, 0.999953, 0.999913, 0.999830, 0.999630, 0.998993, 0.995279, 0.985142, 0.979252, 0.978754, 0.977821, 0.975838, 0.971088, 0.962563, 0.954785, 0.949048, 0.941052, 0.931420, 0.920812, 0.909750, 0.897867, 0.883856, 0.870091, 0.854353, 0.838166, 0.820661, 0.802465, 0.783308, 0.763346, 0.742734, 0.721608, 0.699747, 0.677626, 0.655245, 0.632547, 0.609793, 0.587044, 0.564340, 0.541779, 0.519529, 0.497633, 0.476114, 0.455030, 0.434430, 0.414406, 0.394974, 0.376154, 0.357979, 0.340443, 0.323572, 0.307379, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999998, 0.999998, 0.999996, 0.999991, 0.999984, 0.999970, 0.999946, 0.999905, 0.999815, 0.999599, 0.998856, 0.993704, 0.986135, 0.979212, 0.978690, 0.977691, 0.975504, 0.970133, 0.962951, 0.955649, 0.948405, 0.940418, 0.931660, 0.920881, 0.909376, 0.897785, 0.883844, 0.869756, 0.854326, 0.837732, 0.820617, 0.802053, 0.783195, 0.763119, 0.742610, 0.721344, 0.699709, 0.677624, 0.655114, 0.632523, 0.609812, 0.587052, 0.564417, 0.541966, 0.519751, 0.497824, 0.476309, 0.455271, 0.434735, 0.414736, 0.395317, 0.376524, 0.358364, 0.340852, 0.323988, 0.307786, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999999, 0.999997, 0.999996, 0.999994, 0.999989, 0.999980, 0.999965, 0.999940, 0.999895, 0.999796, 0.999559, 0.998638, 0.992774, 0.986878, 0.980297, 0.978602, 0.977514, 0.975026, 0.969169, 0.963214, 0.956267, 0.947689, 0.940054, 0.931637, 0.920678, 0.908990, 0.897349, 0.883905, 0.869139, 0.854177, 0.837476, 0.820295, 0.801977, 0.782798, 0.762978, 0.742418, 0.721193, 0.699560, 0.677402, 0.655108, 0.632543, 0.609804, 0.587158, 0.564557, 0.542096, 0.519908, 0.498088, 0.476632, 0.455623, 0.435104, 0.415161, 0.395783, 0.377005, 0.358843, 0.341345, 0.324529, 0.308355, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999999, 0.999998, 0.999997, 0.999992, 0.999991, 0.999985, 0.999977, 0.999959, 0.999935, 0.999878, 0.999773, 0.999505, 0.998284, 0.992353, 0.987457, 0.981665, 0.978492, 0.977277, 0.974360, 0.968716, 0.963373, 0.956629, 0.947397, 0.939657, 0.931339, 0.920588, 0.908975, 0.896712, 0.883763, 0.868890, 0.853731, 0.837333, 0.819702, 0.801738, 0.782454, 0.762712, 0.742024, 0.721037, 0.699325, 0.677359, 0.655030, 0.632439, 0.609869, 0.587221, 0.564663, 0.542328, 0.520220, 0.498400, 0.476997, 0.456053, 0.435593, 0.415658, 0.396300, 0.377577, 0.359473, 0.342004, 0.325170, 0.308997, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999998, 0.999996, 0.999993, 0.999988, 0.999981, 0.999971, 0.999951, 0.999921, 0.999863, 0.999748, 0.999433, 0.997681, 0.992120, 0.987920, 0.982864, 0.978353, 0.976961, 0.973451, 0.968396, 0.963400, 0.956680, 0.947529, 0.939151, 0.930747, 0.920511, 0.908867, 0.896142, 0.883335, 0.868764, 0.853025, 0.837015, 0.819452, 0.801249, 0.782176, 0.762345, 0.741843, 0.720721, 0.699135, 0.677194, 0.654889, 0.632487, 0.609902, 0.587328, 0.564891, 0.542567, 0.520501, 0.498793, 0.477442, 0.456528, 0.436131, 0.416273, 0.396980, 0.378276, 0.360176, 0.342738, 0.325950, 0.309803, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999999, 0.999997, 0.999995, 0.999991, 0.999985, 0.999978, 0.999963, 0.999942, 0.999907, 0.999844, 0.999715, 0.999332, 0.996612, 0.991974, 0.988297, 0.983843, 0.978349, 0.976540, 0.972351, 0.968109, 0.963280, 0.956464, 0.947779, 0.938754, 0.929952, 0.920253, 0.908530, 0.895785, 0.882679, 0.868456, 0.852669, 0.836406, 0.819138, 0.800708, 0.781803, 0.761855, 0.741534, 0.720405, 0.698959, 0.676964, 0.654827, 0.632411, 0.609922, 0.587477, 0.565051, 0.542829, 0.520889, 0.499225, 0.477951, 0.457148, 0.436792, 0.416963, 0.397723, 0.379068, 0.361025, 0.343608, 0.326842, 0.310718, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999995, 0.999994, 0.999990, 0.999983, 0.999971, 0.999954, 0.999932, 0.999892, 0.999820, 0.999675, 0.999190, 0.995492, 0.991911, 0.988610, 0.984662, 0.979221, 0.975975, 0.971671, 0.967788, 0.963002, 0.955938, 0.947965, 0.938692, 0.929309, 0.919781, 0.908268, 0.895518, 0.882022, 0.867884, 0.852346, 0.835746, 0.818607, 0.800261, 0.781335, 0.761539, 0.741063, 0.720116, 0.698617, 0.676815, 0.654700, 0.632389, 0.610037, 0.587591, 0.565328, 0.543205, 0.521293, 0.499745, 0.478562, 0.457776, 0.437515, 0.417776, 0.398586, 0.379963, 0.361984, 0.344616, 0.327857, 0.311751, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999997, 0.999996, 0.999992, 0.999986, 0.999977, 0.999965, 0.999947, 0.999916, 0.999873, 0.999794, 0.999628, 0.998966, 0.994914, 0.991849, 0.988873, 0.985288, 0.980170, 0.975207, 0.971156, 0.967476, 0.962538, 0.955601, 0.947978, 0.938542, 0.928618, 0.919056, 0.907890, 0.895098, 0.881352, 0.867263, 0.851806, 0.835168, 0.818003, 0.799785, 0.780633, 0.761080, 0.740618, 0.719795, 0.698332, 0.676629, 0.654544, 0.632411, 0.610042, 0.587805, 0.565593, 0.543549, 0.521793, 0.500309, 0.479195, 0.458546, 0.438353, 0.418669, 0.399557, 0.381012, 0.363049, 0.345710, 0.329006, 0.312948, 1.000000, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999997, 0.999993, 0.999990, 0.999984, 0.999972, 0.999960, 0.999939, 0.999906, 0.999853, 0.999765, 0.999567, 0.998603, 0.994519, 0.991794, 0.989089, 0.985781, 0.980956, 0.974161, 0.970688, 0.967064, 0.961890, 0.955292, 0.947848, 0.938359, 0.928226, 0.918214, 0.907361, 0.894702, 0.880834, 0.866500, 0.851209, 0.834627, 0.817211, 0.799250, 0.780131, 0.760512, 0.740218, 0.719264, 0.698063, 0.676325, 0.654450, 0.632316, 0.610170, 0.587988, 0.565891, 0.544013, 0.522305, 0.500958, 0.479971, 0.459376, 0.439271, 0.419699, 0.400620, 0.382126, 0.364246, 0.346967, 0.330273, 0.314236, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999996, 0.999994, 0.999988, 0.999979, 0.999967, 0.999952, 0.999924, 0.999888, 0.999833, 0.999733, 0.999490, 0.997946, 0.994192, 0.991812, 0.989274, 0.986224, 0.981547, 0.974000, 0.970269, 0.966545, 0.961031, 0.954921, 0.947416, 0.938226, 0.928003, 0.917390, 0.906553, 0.894191, 0.880329, 0.865540, 0.850476, 0.834058, 0.816467, 0.798509, 0.779561, 0.759828, 0.739738, 0.718878, 0.697718, 0.676138, 0.654342, 0.632317, 0.610292, 0.588207, 0.566289, 0.544443, 0.522927, 0.501674, 0.480765, 0.460314, 0.440304, 0.420782, 0.401824, 0.383410, 0.365538, 0.348312, 0.331692, 0.315688, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999996, 0.999993, 0.999985, 0.999976, 0.999961, 0.999943, 0.999913, 0.999872, 0.999807, 0.999691, 0.999390, 0.996859, 0.994003, 0.991808, 0.989423, 0.986523, 0.981783, 0.974511, 0.969791, 0.965933, 0.960377, 0.954434, 0.946803, 0.938026, 0.927620, 0.916545, 0.905639, 0.893489, 0.879820, 0.864852, 0.849513, 0.833311, 0.815878, 0.797621, 0.778938, 0.759253, 0.739142, 0.718479, 0.697274, 0.675902, 0.654135, 0.632357, 0.610364, 0.588497, 0.566631, 0.545012, 0.523579, 0.502429, 0.481680, 0.461304, 0.441425, 0.422039, 0.403135, 0.384779, 0.366976, 0.349796, 0.333231, 0.317277, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999996, 0.999991, 0.999983, 0.999974, 0.999956, 0.999932, 0.999901, 0.999852, 0.999780, 0.999646, 0.999248, 0.996193, 0.993784, 0.991782, 0.989539, 0.986694, 0.981765, 0.975135, 0.969309, 0.965128, 0.959788, 0.953831, 0.946255, 0.937664, 0.927351, 0.916044, 0.904715, 0.892528, 0.879111, 0.864256, 0.848452, 0.832434, 0.815129, 0.796806, 0.778118, 0.758668, 0.738466, 0.718024, 0.696958, 0.675642, 0.654067, 0.632325, 0.610546, 0.588786, 0.567123, 0.545617, 0.524312, 0.503348, 0.482637, 0.462418, 0.442657, 0.423338, 0.404564, 0.386277, 0.368545, 0.351448, 0.334906, 0.318961, 1.000000, 1.000000, 1.000000, 0.999999, 0.999999, 0.999998, 0.999994, 0.999989, 0.999979, 0.999968, 0.999949, 0.999921, 0.999886, 0.999833, 0.999747, 0.999596, 0.999029, 0.995749, 0.993677, 0.991724, 0.989620, 0.986723, 0.981515, 0.975767, 0.969056, 0.964124, 0.959142, 0.953036, 0.945650, 0.937022, 0.926971, 0.915515, 0.903584, 0.891603, 0.878212, 0.863472, 0.847652, 0.831398, 0.814299, 0.796105, 0.777231, 0.757977, 0.737895, 0.717415, 0.696595, 0.675317, 0.653980, 0.632343, 0.610735, 0.589076, 0.567620, 0.546251, 0.525165, 0.504255, 0.483759, 0.463666, 0.443987, 0.424783, 0.406042, 0.387891, 0.370293, 0.353221, 0.336715, 0.320806, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999998, 0.999993, 0.999987, 0.999977, 0.999964, 0.999943, 0.999911, 0.999867, 0.999807, 0.999714, 0.999531, 0.998645, 0.995399, 0.993512, 0.991717, 0.989661, 0.986652, 0.981559, 0.976183, 0.969411, 0.963317, 0.958457, 0.952091, 0.944951, 0.936307, 0.926454, 0.915043, 0.902668, 0.890462, 0.877245, 0.862672, 0.846823, 0.830201, 0.813293, 0.795306, 0.776393, 0.757199, 0.737324, 0.716808, 0.696187, 0.675094, 0.653814, 0.632453, 0.610885, 0.589483, 0.568099, 0.546975, 0.525953, 0.505268, 0.484936, 0.464988, 0.445458, 0.426314, 0.407750, 0.389670, 0.372098, 0.355105, 0.338682, 0.322825, 1.000000, 1.000000, 1.000000, 1.000000, 0.999999, 0.999996, 0.999992, 0.999983, 0.999976, 0.999959, 0.999933, 0.999898, 0.999849, 0.999780, 0.999676, 0.999454, 0.997884, 0.995166, 0.993394, 0.991723, 0.989654, 0.986389, 0.981632, 0.976607, 0.969701, 0.962555, 0.957605, 0.951232, 0.944099, 0.935556, 0.925699, 0.914492, 0.902027, 0.889116, 0.876093, 0.861649, 0.845956, 0.829238, 0.812220, 0.794420, 0.775657, 0.756265, 0.736673, 0.716372, 0.695669, 0.674886, 0.653728, 0.632568, 0.611217, 0.589929, 0.568783, 0.547752, 0.526931, 0.506425, 0.486238, 0.466425, 0.446945, 0.428026, 0.409536, 0.391551, 0.374087, 0.357155, 0.340787, 0.324974, 1.000000, 1.000000, 1.000000, 1.000000, 0.999998, 0.999996, 0.999990, 0.999984, 0.999970, 0.999952, 0.999925, 0.999886, 0.999831, 0.999757, 0.999633, 0.999356, 0.997017, 0.994868, 0.993337, 0.991710, 0.989580, 0.985848, 0.981640, 0.976711, 0.969755, 0.962166, 0.956609, 0.950365, 0.943026, 0.934693, 0.924880, 0.913729, 0.901350, 0.887966, 0.874726, 0.860474, 0.844905, 0.828269, 0.810905, 0.793364, 0.774812, 0.755478, 0.735886, 0.715847, 0.695231, 0.674537, 0.653667, 0.632527, 0.611475, 0.590363, 0.569462, 0.548571, 0.527976, 0.507634, 0.487632, 0.467901, 0.448680, 0.429833, 0.411467, 0.393568, 0.376197, 0.359374, 0.343034, 0.327273, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999993, 0.999989, 0.999980, 0.999965, 0.999945, 0.999913, 0.999869, 0.999810, 0.999723, 0.999583, 0.999213, 0.996540, 0.994740, 0.993244, 0.991671, 0.989411, 0.985533, 0.981616, 0.976847, 0.969968, 0.962315, 0.955468, 0.949420, 0.942016, 0.933617, 0.923949, 0.912899, 0.900495, 0.887022, 0.873283, 0.859153, 0.843830, 0.827325, 0.809888, 0.792172, 0.773832, 0.754686, 0.735035, 0.715297, 0.694955, 0.674242, 0.653660, 0.632752, 0.611804, 0.590993, 0.570154, 0.549539, 0.529087, 0.508974, 0.489030, 0.469599, 0.450466, 0.431761, 0.413508, 0.395761, 0.378480, 0.361679, 0.345465, 0.329752, 1.000000, 1.000000, 1.000000, 1.000000, 0.999997, 0.999994, 0.999987, 0.999978, 0.999961, 0.999936, 0.999903, 0.999855, 0.999786, 0.999689, 0.999527, 0.998988, 0.996137, 0.994527, 0.993108, 0.991599, 0.989084, 0.985308, 0.981527, 0.976677, 0.970079, 0.962535, 0.954490, 0.948271, 0.940942, 0.932422, 0.922836, 0.911896, 0.899632, 0.886118, 0.871864, 0.857719, 0.842536, 0.826163, 0.808849, 0.790860, 0.772802, 0.753860, 0.734335, 0.714582, 0.694543, 0.674071, 0.653544, 0.632922, 0.612153, 0.591573, 0.570951, 0.550520, 0.530352, 0.510311, 0.490707, 0.471359, 0.452396, 0.433837, 0.415736, 0.398052, 0.380874, 0.364232, 0.348023, 0.332368, 1.000000, 1.000000, 1.000000, 0.999999, 0.999998, 0.999994, 0.999988, 0.999976, 0.999957, 0.999928, 0.999891, 0.999837, 0.999759, 0.999650, 0.999463, 0.998551, 0.995879, 0.994366, 0.992964, 0.991479, 0.988521, 0.985101, 0.981482, 0.976168, 0.970242, 0.962585, 0.953950, 0.946973, 0.939686, 0.931248, 0.921614, 0.910765, 0.898617, 0.885183, 0.870772, 0.856138, 0.841120, 0.824962, 0.807732, 0.789813, 0.771638, 0.753008, 0.733686, 0.713927, 0.694082, 0.673967, 0.653549, 0.633135, 0.612702, 0.592200, 0.571904, 0.551679, 0.531678, 0.511898, 0.492437, 0.473239, 0.454451, 0.436067, 0.418054, 0.400542, 0.383486, 0.366848, 0.350781, 0.335182, 1.000000, 1.000000, 1.000000, 0.999999, 0.999997, 0.999993, 0.999985, 0.999972, 0.999951, 0.999919, 0.999877, 0.999817, 0.999733, 0.999608, 0.999380, 0.997685, 0.995603, 0.994264, 0.992911, 0.991287, 0.987923, 0.984871, 0.981239, 0.975933, 0.970149, 0.962511, 0.953824, 0.945699, 0.938285, 0.929907, 0.920343, 0.909537, 0.897435, 0.884056, 0.869626, 0.854490, 0.839459, 0.823511, 0.806511, 0.788752, 0.770440, 0.751995, 0.732962, 0.713424, 0.693525, 0.673798, 0.653622, 0.633301, 0.613224, 0.592938, 0.572833, 0.552904, 0.533030, 0.513556, 0.494215, 0.475279, 0.456673, 0.438411, 0.420583, 0.403178, 0.386178, 0.369728, 0.353688, 0.338147, 1.000000, 1.000000, 1.000000, 0.999999, 0.999997, 0.999991, 0.999984, 0.999967, 0.999944, 0.999912, 0.999863, 0.999796, 0.999703, 0.999563, 0.999279, 0.997104, 0.995394, 0.994111, 0.992825, 0.990979, 0.987529, 0.984661, 0.980774, 0.975758, 0.969866, 0.962465, 0.953678, 0.944489, 0.936886, 0.928356, 0.918820, 0.908073, 0.896092, 0.882833, 0.868463, 0.853212, 0.837744, 0.822048, 0.805333, 0.787643, 0.769414, 0.750830, 0.732178, 0.712972, 0.693227, 0.673569, 0.653744, 0.633739, 0.613735, 0.593822, 0.573916, 0.554158, 0.534652, 0.515248, 0.496233, 0.477436, 0.459009, 0.440929, 0.423259, 0.405951, 0.389136, 0.372690, 0.356789, 0.341329, 1.000000, 1.000000, 1.000000, 0.999999, 0.999996, 0.999991, 0.999981, 0.999966, 0.999939, 0.999903, 0.999847, 0.999771, 0.999666, 0.999510, 0.999131, 0.996690, 0.995147, 0.993882, 0.992696, 0.990474, 0.987227, 0.984334, 0.980153, 0.975438, 0.969406, 0.962238, 0.953598, 0.943868, 0.935356, 0.926721, 0.917122, 0.906430, 0.894550, 0.881354, 0.867131, 0.851954, 0.835972, 0.820331, 0.803911, 0.786452, 0.768420, 0.749821, 0.731298, 0.712393, 0.692979, 0.673418, 0.653859, 0.634232, 0.614327, 0.594732, 0.575131, 0.555584, 0.536346, 0.517175, 0.498323, 0.479744, 0.461485, 0.443645, 0.426061, 0.408969, 0.392155, 0.375921, 0.360060, 0.344677, 1.000000, 1.000000, 1.000000, 0.999999, 0.999997, 0.999991, 0.999979, 0.999960, 0.999931, 0.999891, 0.999832, 0.999748, 0.999629, 0.999449, 0.998880, 0.996305, 0.995024, 0.993812, 0.992508, 0.989721, 0.986936, 0.983936, 0.979629, 0.974979, 0.968928, 0.961970, 0.953291, 0.943458, 0.933644, 0.925007, 0.915388, 0.904755, 0.892932, 0.879831, 0.865794, 0.850672, 0.834591, 0.818398, 0.802304, 0.785151, 0.767450, 0.748987, 0.730325, 0.711758, 0.692761, 0.673417, 0.653908, 0.634686, 0.615168, 0.595707, 0.576393, 0.557198, 0.538018, 0.519253, 0.500555, 0.482220, 0.464197, 0.446414, 0.429106, 0.412035, 0.395508, 0.379284, 0.363538, 0.348220, 1.000000, 1.000000, 1.000000, 0.999999, 0.999995, 0.999989, 0.999977, 0.999955, 0.999924, 0.999879, 0.999813, 0.999722, 0.999590, 0.999381, 0.998335, 0.996088, 0.994814, 0.993709, 0.992220, 0.989209, 0.986575, 0.983383, 0.979084, 0.974272, 0.968359, 0.961275, 0.953025, 0.943098, 0.932434, 0.923101, 0.913477, 0.902861, 0.891059, 0.878072, 0.864118, 0.849188, 0.833281, 0.816808, 0.800596, 0.783745, 0.766331, 0.748123, 0.729686, 0.711078, 0.692527, 0.673491, 0.654296, 0.635113, 0.616048, 0.596847, 0.577720, 0.558879, 0.540028, 0.521371, 0.502996, 0.484858, 0.466997, 0.449477, 0.432217, 0.415426, 0.398924, 0.382890, 0.367206, 0.351955, 1.000000, 1.000000, 1.000000, 0.999998, 0.999996, 0.999988, 0.999974, 0.999953, 0.999918, 0.999865, 0.999791, 0.999690, 0.999542, 0.999293, 0.997535, 0.995790, 0.994609, 0.993557, 0.991766, 0.988767, 0.986255, 0.982544, 0.978541, 0.973528, 0.967700, 0.960596, 0.952299, 0.942684, 0.931653, 0.921211, 0.911489, 0.900818, 0.889018, 0.876245, 0.862406, 0.847517, 0.831852, 0.815367, 0.798719, 0.782224, 0.765167, 0.747304, 0.729133, 0.710485, 0.692196, 0.673589, 0.654770, 0.635717, 0.616986, 0.598119, 0.579298, 0.560560, 0.542163, 0.523669, 0.505564, 0.487642, 0.469991, 0.452658, 0.435620, 0.418937, 0.402612, 0.386633, 0.371091, 0.355949, 1.000000, 1.000000, 0.999999, 0.999998, 0.999995, 0.999986, 0.999973, 0.999948, 0.999909, 0.999852, 0.999769, 0.999656, 0.999490, 0.999186, 0.997059, 0.995624, 0.994510, 0.993327, 0.991020, 0.988379, 0.985771, 0.981971, 0.978051, 0.972892, 0.967020, 0.959965, 0.951625, 0.941902, 0.930951, 0.919370, 0.909285, 0.898562, 0.886809, 0.874251, 0.860597, 0.845808, 0.830365, 0.813972, 0.797260, 0.780597, 0.763854, 0.746401, 0.728519, 0.710203, 0.691882, 0.673687, 0.655275, 0.636621, 0.617909, 0.599473, 0.581032, 0.562560, 0.544295, 0.526228, 0.508293, 0.490652, 0.473242, 0.456004, 0.439212, 0.422663, 0.406476, 0.390647, 0.375204, 0.360129, 1.000000, 1.000000, 1.000000, 0.999999, 0.999994, 0.999984, 0.999969, 0.999940, 0.999898, 0.999837, 0.999746, 0.999617, 0.999438, 0.999016, 0.996703, 0.995302, 0.994356, 0.992993, 0.990390, 0.988072, 0.985152, 0.981447, 0.977273, 0.972234, 0.966113, 0.959033, 0.950869, 0.941217, 0.930175, 0.918279, 0.906941, 0.896201, 0.884509, 0.871920, 0.858420, 0.843906, 0.828730, 0.812524, 0.795978, 0.778979, 0.762450, 0.745459, 0.727966, 0.710046, 0.691808, 0.673739, 0.655756, 0.637574, 0.619153, 0.600887, 0.582796, 0.564748, 0.546636, 0.528904, 0.511252, 0.493791, 0.476563, 0.459695, 0.442942, 0.426632, 0.410558, 0.394895, 0.379517, 0.364560, 1.000000, 1.000000, 1.000000, 0.999998, 0.999994, 0.999984, 0.999966, 0.999934, 0.999887, 0.999819, 0.999720, 0.999578, 0.999367, 0.998696, 0.996353, 0.995201, 0.994115, 0.992665, 0.989948, 0.987633, 0.984331, 0.980827, 0.976390, 0.971327, 0.965201, 0.957977, 0.949712, 0.940128, 0.929187, 0.917237, 0.904645, 0.893711, 0.882112, 0.869516, 0.856236, 0.841929, 0.826924, 0.810991, 0.794686, 0.777761, 0.760980, 0.744384, 0.727314, 0.709877, 0.691988, 0.674098, 0.656243, 0.638603, 0.620606, 0.602574, 0.584694, 0.567018, 0.549311, 0.531673, 0.514403, 0.497148, 0.480177, 0.463439, 0.446998, 0.430743, 0.414943, 0.399304, 0.384121, 0.369251, 1.000000, 1.000000, 1.000000, 0.999997, 0.999992, 0.999981, 0.999962, 0.999927, 0.999874, 0.999798, 0.999691, 0.999533, 0.999291, 0.997909, 0.996117, 0.995029, 0.993880, 0.992142, 0.989576, 0.987185, 0.983587, 0.980055, 0.975487, 0.970172, 0.963998, 0.956738, 0.948637, 0.939083, 0.928169, 0.916144, 0.903147, 0.890916, 0.879389, 0.866895, 0.853826, 0.839729, 0.824957, 0.809472, 0.793341, 0.776743, 0.759808, 0.743277, 0.726643, 0.709685, 0.692249, 0.674639, 0.657008, 0.639576, 0.622114, 0.604471, 0.586851, 0.569340, 0.552135, 0.534806, 0.517599, 0.500765, 0.484035, 0.467440, 0.451212, 0.435240, 0.419399, 0.404083, 0.388944, 0.374182, 1.000000, 1.000000, 1.000000, 0.999998, 0.999993, 0.999979, 0.999958, 0.999919, 0.999861, 0.999774, 0.999656, 0.999482, 0.999195, 0.997307, 0.995837, 0.994722, 0.993707, 0.991391, 0.989169, 0.986461, 0.982904, 0.979062, 0.974536, 0.969035, 0.962653, 0.955486, 0.947243, 0.937747, 0.926861, 0.914936, 0.901835, 0.888472, 0.876571, 0.864223, 0.851252, 0.837374, 0.822985, 0.807788, 0.791927, 0.775702, 0.758928, 0.742347, 0.725914, 0.709495, 0.692569, 0.675363, 0.658085, 0.640639, 0.623698, 0.606505, 0.589267, 0.572008, 0.554939, 0.538132, 0.521211, 0.504487, 0.488048, 0.471807, 0.455651, 0.439858, 0.424332, 0.408983, 0.394071, 0.379402, 1.000000, 1.000000, 1.000000, 0.999997, 0.999992, 0.999978, 0.999954, 0.999913, 0.999844, 0.999753, 0.999618, 0.999424, 0.999067, 0.996875, 0.995659, 0.994603, 0.993420, 0.990874, 0.988713, 0.985585, 0.982193, 0.978145, 0.973416, 0.967801, 0.961483, 0.954069, 0.945704, 0.936138, 0.925374, 0.913395, 0.900339, 0.886675, 0.873512, 0.861326, 0.848513, 0.834956, 0.820820, 0.805943, 0.790574, 0.774677, 0.758279, 0.741807, 0.725271, 0.709231, 0.692874, 0.676189, 0.659352, 0.642296, 0.625250, 0.608700, 0.591823, 0.575012, 0.558143, 0.541491, 0.525075, 0.508558, 0.492277, 0.476270, 0.460459, 0.444740, 0.429400, 0.414309, 0.399421, 0.384907, 1.000000, 1.000000, 1.000000, 0.999997, 0.999990, 0.999977, 0.999947, 0.999902, 0.999832, 0.999730, 0.999577, 0.999359, 0.998845, 0.996554, 0.995328, 0.994442, 0.992919, 0.990393, 0.988170, 0.984855, 0.981312, 0.977149, 0.972137, 0.966207, 0.959967, 0.952454, 0.943873, 0.934434, 0.923813, 0.911942, 0.898928, 0.885120, 0.871043, 0.858248, 0.845666, 0.832346, 0.818482, 0.804029, 0.788982, 0.773571, 0.757700, 0.741484, 0.725186, 0.708915, 0.693244, 0.677028, 0.660656, 0.644079, 0.627377, 0.610804, 0.594542, 0.578112, 0.561650, 0.545163, 0.528962, 0.512926, 0.496893, 0.481007, 0.465397, 0.450042, 0.434740, 0.419831, 0.405156, 0.390692, 1.000000, 1.000000, 0.999999, 0.999997, 0.999989, 0.999973, 0.999942, 0.999891, 0.999813, 0.999698, 0.999532, 0.999285, 0.998286, 0.996295, 0.995215, 0.994182, 0.992032, 0.989855, 0.987415, 0.984047, 0.980050, 0.976017, 0.970845, 0.964767, 0.958269, 0.950600, 0.942033, 0.932501, 0.921807, 0.910017, 0.897149, 0.883414, 0.869182, 0.855055, 0.842687, 0.829548, 0.816162, 0.802072, 0.787436, 0.772533, 0.757043, 0.741263, 0.725330, 0.709262, 0.693497, 0.678038, 0.662128, 0.646068, 0.629824, 0.613437, 0.597334, 0.581401, 0.565372, 0.549288, 0.533182, 0.517405, 0.501765, 0.486143, 0.470675, 0.455465, 0.440532, 0.425630, 0.411113, 0.396887, 1.000000, 1.000000, 0.999999, 0.999996, 0.999989, 0.999970, 0.999934, 0.999879, 0.999793, 0.999665, 0.999481, 0.999192, 0.997506, 0.995926, 0.995009, 0.993736, 0.991298, 0.989326, 0.986371, 0.983199, 0.979032, 0.974596, 0.969364, 0.963198, 0.956385, 0.948509, 0.939993, 0.930421, 0.919590, 0.908140, 0.895349, 0.881699, 0.867456, 0.852784, 0.839500, 0.826629, 0.813602, 0.799983, 0.785873, 0.771340, 0.756480, 0.741190, 0.725687, 0.709997, 0.694192, 0.678975, 0.663673, 0.648135, 0.632442, 0.616477, 0.600565, 0.584772, 0.569202, 0.553595, 0.537881, 0.522193, 0.506784, 0.491554, 0.476349, 0.461278, 0.446419, 0.431913, 0.417443, 0.403271, 1.000000, 1.000000, 0.999999, 0.999995, 0.999986, 0.999966, 0.999927, 0.999867, 0.999772, 0.999629, 0.999423, 0.999075, 0.997024, 0.995773, 0.994651, 0.993353, 0.990822, 0.988569, 0.985596, 0.982182, 0.977871, 0.973140, 0.967584, 0.961408, 0.954294, 0.946398, 0.937603, 0.927937, 0.917305, 0.905833, 0.893138, 0.879770, 0.865720, 0.851023, 0.836801, 0.823784, 0.810909, 0.797886, 0.784177, 0.770243, 0.755925, 0.741144, 0.726214, 0.710971, 0.695563, 0.680212, 0.665304, 0.650297, 0.635168, 0.619796, 0.604217, 0.588692, 0.573254, 0.557998, 0.542839, 0.527470, 0.512162, 0.497115, 0.482296, 0.467477, 0.452812, 0.438310, 0.424184, 0.410163, 1.000000, 1.000000, 0.999999, 0.999996, 0.999984, 0.999962, 0.999920, 0.999852, 0.999745, 0.999586, 0.999354, 0.998894, 0.996686, 0.995485, 0.994493, 0.992573, 0.990323, 0.987772, 0.984692, 0.980887, 0.976446, 0.971625, 0.965717, 0.959421, 0.951975, 0.944086, 0.935066, 0.925403, 0.914814, 0.903208, 0.890958, 0.877817, 0.863828, 0.849289, 0.834872, 0.820889, 0.808183, 0.795660, 0.782556, 0.769066, 0.755386, 0.741229, 0.726726, 0.712170, 0.697209, 0.682170, 0.667203, 0.652689, 0.637938, 0.623262, 0.608190, 0.593002, 0.577817, 0.562737, 0.547836, 0.533036, 0.518052, 0.503135, 0.488422, 0.473986, 0.459552, 0.445282, 0.431149, 0.417407, 1.000000, 1.000000, 0.999999, 0.999994, 0.999983, 0.999957, 0.999914, 0.999835, 0.999718, 0.999538, 0.999275, 0.998454, 0.996341, 0.995246, 0.994222, 0.991844, 0.989829, 0.986688, 0.983562, 0.979638, 0.974932, 0.969827, 0.963621, 0.957146, 0.949365, 0.941398, 0.932245, 0.922556, 0.911949, 0.900627, 0.888440, 0.875544, 0.862005, 0.847810, 0.833372, 0.819134, 0.805508, 0.793339, 0.780916, 0.767837, 0.754858, 0.741307, 0.727496, 0.713386, 0.699131, 0.684542, 0.669878, 0.655261, 0.641035, 0.626685, 0.612377, 0.597625, 0.582805, 0.568030, 0.553204, 0.538684, 0.524269, 0.509662, 0.495119, 0.480735, 0.466634, 0.452593, 0.438748, 0.424915, 1.000000, 1.000000, 0.999998, 0.999994, 0.999982, 0.999956, 0.999901, 0.999818, 0.999683, 0.999487, 0.999185, 0.997584, 0.996004, 0.995050, 0.993715, 0.991212, 0.989057, 0.985879, 0.982243, 0.978206, 0.973119, 0.967919, 0.961343, 0.954603, 0.946712, 0.938378, 0.929266, 0.919443, 0.908911, 0.897725, 0.885589, 0.873254, 0.859889, 0.846123, 0.832094, 0.817898, 0.803866, 0.791061, 0.779235, 0.766885, 0.754292, 0.741565, 0.728331, 0.714861, 0.701179, 0.687166, 0.673012, 0.658716, 0.644442, 0.630472, 0.616519, 0.602514, 0.588172, 0.573689, 0.559281, 0.544768, 0.530543, 0.516485, 0.502303, 0.488100, 0.474095, 0.460245, 0.446598, 0.433169, 1.000000, 1.000000, 0.999997, 0.999993, 0.999980, 0.999947, 0.999891, 0.999794, 0.999647, 0.999425, 0.999062, 0.997049, 0.995778, 0.994652, 0.992778, 0.990482, 0.988004, 0.984893, 0.980881, 0.976605, 0.971199, 0.965610, 0.958925, 0.951746, 0.943791, 0.935200, 0.926018, 0.916028, 0.905724, 0.894528, 0.882914, 0.870740, 0.857802, 0.844552, 0.830857, 0.816921, 0.803102, 0.789625, 0.777480, 0.765891, 0.753908, 0.741795, 0.729390, 0.716440, 0.703411, 0.690068, 0.676438, 0.662586, 0.648697, 0.634732, 0.620997, 0.607451, 0.593765, 0.579748, 0.565661, 0.551594, 0.537396, 0.523433, 0.509708, 0.495972, 0.482082, 0.468427, 0.454890, 0.441623, 1.000000, 1.000000, 0.999999, 0.999991, 0.999977, 0.999940, 0.999875, 0.999769, 0.999605, 0.999352, 0.998882, 0.996665, 0.995459, 0.994380, 0.992014, 0.989912, 0.986796, 0.983537, 0.979326, 0.974792, 0.969140, 0.963160, 0.956222, 0.948807, 0.940518, 0.931755, 0.922452, 0.912319, 0.902227, 0.891142, 0.879838, 0.868047, 0.855745, 0.842718, 0.829827, 0.816398, 0.802786, 0.789396, 0.776581, 0.764901, 0.753710, 0.742102, 0.730448, 0.718337, 0.705768, 0.693172, 0.680153, 0.666882, 0.653401, 0.639837, 0.626152, 0.612676, 0.599435, 0.586109, 0.572473, 0.558715, 0.544964, 0.531112, 0.517416, 0.503992, 0.490653, 0.477162, 0.463832, 0.450645, 1.000000, 1.000000, 0.999999, 0.999992, 0.999973, 0.999933, 0.999861, 0.999741, 0.999554, 0.999267, 0.998411, 0.996303, 0.995191, 0.993945, 0.991406, 0.989019, 0.985720, 0.982057, 0.977501, 0.972605, 0.966697, 0.960340, 0.953031, 0.945347, 0.936866, 0.927917, 0.918562, 0.908598, 0.898486, 0.887794, 0.876545, 0.865379, 0.853428, 0.841167, 0.828649, 0.815967, 0.802957, 0.789865, 0.777077, 0.764695, 0.753544, 0.742694, 0.731571, 0.720304, 0.708490, 0.696351, 0.684134, 0.671470, 0.658541, 0.645376, 0.632209, 0.618776, 0.605511, 0.592527, 0.579546, 0.566310, 0.552860, 0.539492, 0.526005, 0.512564, 0.499340, 0.486360, 0.473357, 0.460306, 1.000000, 1.000000, 0.999998, 0.999991, 0.999970, 0.999926, 0.999842, 0.999710, 0.999498, 0.999164, 0.997464, 0.995870, 0.994917, 0.992911, 0.990682, 0.987816, 0.984410, 0.980551, 0.975693, 0.970263, 0.963946, 0.957248, 0.949765, 0.941571, 0.932941, 0.923873, 0.914332, 0.904560, 0.894394, 0.884127, 0.873294, 0.862503, 0.851335, 0.839566, 0.827776, 0.815708, 0.803370, 0.790821, 0.778386, 0.766121, 0.754193, 0.743420, 0.732975, 0.722326, 0.711376, 0.699992, 0.688180, 0.676354, 0.664004, 0.651449, 0.638600, 0.625776, 0.612660, 0.599603, 0.586719, 0.574078, 0.561273, 0.548129, 0.535155, 0.522015, 0.508851, 0.495837, 0.483190, 0.470624, 1.000000, 1.000000, 0.999998, 0.999988, 0.999965, 0.999916, 0.999823, 0.999669, 0.999425, 0.999025, 0.996874, 0.995670, 0.994415, 0.991991, 0.989766, 0.986646, 0.982812, 0.978356, 0.973317, 0.967612, 0.960820, 0.953603, 0.945969, 0.937323, 0.928661, 0.919507, 0.909833, 0.900245, 0.890390, 0.880252, 0.870000, 0.859518, 0.849163, 0.838101, 0.826960, 0.815688, 0.804126, 0.792234, 0.780356, 0.768474, 0.756678, 0.745159, 0.734601, 0.724624, 0.714339, 0.703751, 0.692766, 0.681267, 0.669799, 0.657871, 0.645577, 0.633102, 0.620560, 0.607737, 0.594890, 0.582143, 0.569779, 0.557360, 0.544651, 0.531942, 0.519228, 0.506467, 0.493710, 0.481143, 1.000000, 1.000000, 0.999998, 0.999988, 0.999961, 0.999902, 0.999798, 0.999622, 0.999341, 0.998801, 0.996397, 0.995225, 0.993927, 0.991338, 0.988500, 0.985327, 0.981195, 0.976383, 0.970726, 0.964471, 0.957386, 0.949813, 0.941694, 0.932681, 0.923974, 0.914755, 0.905026, 0.895649, 0.886178, 0.876277, 0.866629, 0.856890, 0.846934, 0.836887, 0.826373, 0.815885, 0.805169, 0.794133, 0.782812, 0.771547, 0.760175, 0.748896, 0.737687, 0.727152, 0.717601, 0.707670, 0.697425, 0.686788, 0.675664, 0.664513, 0.652962, 0.640965, 0.628851, 0.616551, 0.604168, 0.591559, 0.579009, 0.566648, 0.554597, 0.542382, 0.529999, 0.517655, 0.505254, 0.492894, 1.000000, 1.000000, 0.999997, 0.999986, 0.999956, 0.999889, 0.999766, 0.999562, 0.999240, 0.997952, 0.996094, 0.994979, 0.992773, 0.990536, 0.987214, 0.983322, 0.978938, 0.973714, 0.967681, 0.960981, 0.953144, 0.945475, 0.936909, 0.927734, 0.918826, 0.909590, 0.900085, 0.890867, 0.881801, 0.872565, 0.863236, 0.854239, 0.845060, 0.835686, 0.826251, 0.816284, 0.806586, 0.796419, 0.785914, 0.775210, 0.764461, 0.753599, 0.742805, 0.731872, 0.721370, 0.711898, 0.702337, 0.692383, 0.682137, 0.671365, 0.660479, 0.649314, 0.637685, 0.625899, 0.613898, 0.601865, 0.589582, 0.577285, 0.565013, 0.553106, 0.541280, 0.529367, 0.517320, 0.505411, 1.000000, 1.000000, 0.999997, 0.999983, 0.999948, 0.999869, 0.999732, 0.999499, 0.999111, 0.997167, 0.995720, 0.994349, 0.991727, 0.989197, 0.985883, 0.981483, 0.976618, 0.970597, 0.964122, 0.956994, 0.948639, 0.940500, 0.931606, 0.922385, 0.913291, 0.904205, 0.894938, 0.885890, 0.877334, 0.868754, 0.860053, 0.851683, 0.843447, 0.834889, 0.826304, 0.817441, 0.808285, 0.799141, 0.789570, 0.779600, 0.769510, 0.759155, 0.748882, 0.738346, 0.727629, 0.717273, 0.707467, 0.698283, 0.688609, 0.678748, 0.668371, 0.657739, 0.646951, 0.635765, 0.624254, 0.612647, 0.600900, 0.589061, 0.576998, 0.564991, 0.553102, 0.541517, 0.530027, 0.518495, 1.000000, 1.000000, 0.999997, 0.999983, 0.999939, 0.999851, 0.999684, 0.999412, 0.998925, 0.996597, 0.995207, 0.993603, 0.990903, 0.987594, 0.983814, 0.979016, 0.973647, 0.967048, 0.960109, 0.952123, 0.943560, 0.934900, 0.925747, 0.916566, 0.907305, 0.898441, 0.889629, 0.881042, 0.872874, 0.865064, 0.857225, 0.849446, 0.842063, 0.834561, 0.826814, 0.818875, 0.810748, 0.802316, 0.793699, 0.784704, 0.775198, 0.765643, 0.755735, 0.745873, 0.735526, 0.725229, 0.714892, 0.704807, 0.695502, 0.686241, 0.676633, 0.666688, 0.656384, 0.645871, 0.635174, 0.624113, 0.612788, 0.601426, 0.589925, 0.578399, 0.566612, 0.554931, 0.543383, 0.532065, 1.000000, 1.000000, 0.999996, 0.999977, 0.999928, 0.999824, 0.999633, 0.999306, 0.998429, 0.996133, 0.994890, 0.992316, 0.989752, 0.986095, 0.981564, 0.976234, 0.970081, 0.962779, 0.955232, 0.946702, 0.937716, 0.928604, 0.919281, 0.910167, 0.901046, 0.892446, 0.884183, 0.876253, 0.868619, 0.861545, 0.854673, 0.847885, 0.841074, 0.834610, 0.827984, 0.820945, 0.813648, 0.806232, 0.798444, 0.790232, 0.781853, 0.772897, 0.763648, 0.754227, 0.744542, 0.734689, 0.724526, 0.714204, 0.704152, 0.694222, 0.685143, 0.675860, 0.666319, 0.656415, 0.646273, 0.635902, 0.625399, 0.614563, 0.603490, 0.592413, 0.581217, 0.570000, 0.558608, 0.547242, 1.000000, 0.999999, 0.999995, 0.999972, 0.999915, 0.999790, 0.999562, 0.999168, 0.997237, 0.995672, 0.994074, 0.991220, 0.987792, 0.983822, 0.978599, 0.972804, 0.965718, 0.958053, 0.949460, 0.940503, 0.931011, 0.921608, 0.912409, 0.903378, 0.894606, 0.886369, 0.878756, 0.871573, 0.864862, 0.858421, 0.852541, 0.846802, 0.841027, 0.835206, 0.829628, 0.823730, 0.817415, 0.810655, 0.803873, 0.796659, 0.788887, 0.780940, 0.772537, 0.763507, 0.754487, 0.745163, 0.735572, 0.725687, 0.715611, 0.705398, 0.695418, 0.685592, 0.676518, 0.667304, 0.657875, 0.648182, 0.638235, 0.628062, 0.617813, 0.607283, 0.596552, 0.585770, 0.575033, 0.564153, 1.000000, 1.000000, 0.999995, 0.999970, 0.999898, 0.999748, 0.999472, 0.998969, 0.996528, 0.995102, 0.992701, 0.989963, 0.985981, 0.981194, 0.975183, 0.968501, 0.960502, 0.952012, 0.942861, 0.933376, 0.923506, 0.914042, 0.904921, 0.896282, 0.887987, 0.880341, 0.873536, 0.867293, 0.861556, 0.856148, 0.850987, 0.846352, 0.841684, 0.836880, 0.832036, 0.827091, 0.821900, 0.816206, 0.810042, 0.803629, 0.796918, 0.789653, 0.781915, 0.774014, 0.765530, 0.756526, 0.747669, 0.738342, 0.728770, 0.718942, 0.708942, 0.698855, 0.688933, 0.679131, 0.669855, 0.660811, 0.651549, 0.642127, 0.632454, 0.622651, 0.612709, 0.602606, 0.592344, 0.581877, 1.000000, 0.999999, 0.999993, 0.999963, 0.999874, 0.999691, 0.999350, 0.998431, 0.995873, 0.994456, 0.991327, 0.987798, 0.983232, 0.977500, 0.970828, 0.962815, 0.954228, 0.944752, 0.935126, 0.925179, 0.915102, 0.905763, 0.897087, 0.888933, 0.881452, 0.874687, 0.868716, 0.863585, 0.858931, 0.854662, 0.850569, 0.846719, 0.843151, 0.839426, 0.835588, 0.831443, 0.827004, 0.822395, 0.817254, 0.811630, 0.805464, 0.799124, 0.792382, 0.785091, 0.777315, 0.769360, 0.760908, 0.751957, 0.743128, 0.733917, 0.724340, 0.714713, 0.704721, 0.694835, 0.684862, 0.675099, 0.665570, 0.656644, 0.647651, 0.638581, 0.629337, 0.619926, 0.610358, 0.600707, 1.000000, 1.000000, 0.999990, 0.999953, 0.999843, 0.999613, 0.999186, 0.997025, 0.995317, 0.992850, 0.989760, 0.985270, 0.979807, 0.973049, 0.965228, 0.956248, 0.946394, 0.936324, 0.926124, 0.915808, 0.905942, 0.897060, 0.889001, 0.881755, 0.875351, 0.869688, 0.864736, 0.860745, 0.857305, 0.854190, 0.851261, 0.848484, 0.845642, 0.842948, 0.840060, 0.836901, 0.833379, 0.829393, 0.825103, 0.820431, 0.815288, 0.809575, 0.803326, 0.796949, 0.790174, 0.782873, 0.775048, 0.767139, 0.758772, 0.750019, 0.741120, 0.732127, 0.722743, 0.713225, 0.703637, 0.693768, 0.684016, 0.674277, 0.664703, 0.655328, 0.646550, 0.637812, 0.629036, 0.620129, 1.000000, 1.000000, 0.999988, 0.999933, 0.999800, 0.999508, 0.998917, 0.996236, 0.994617, 0.991176, 0.987089, 0.981880, 0.974966, 0.967156, 0.957914, 0.947585, 0.936937, 0.926318, 0.915662, 0.905567, 0.896223, 0.888166, 0.881117, 0.875079, 0.869981, 0.865675, 0.862091, 0.859183, 0.856981, 0.855065, 0.853273, 0.851572, 0.849782, 0.847768, 0.845668, 0.843345, 0.840703, 0.837646, 0.834094, 0.830030, 0.825631, 0.820873, 0.815619, 0.809856, 0.803578, 0.797096, 0.790359, 0.783152, 0.775507, 0.767504, 0.759411, 0.750982, 0.742208, 0.733383, 0.724445, 0.715190, 0.705827, 0.696440, 0.686773, 0.677242, 0.667735, 0.658471, 0.649236, 0.640305, 1.000000, 0.999999, 0.999984, 0.999918, 0.999737, 0.999350, 0.997576, 0.995476, 0.992614, 0.988817, 0.983601, 0.976880, 0.968694, 0.959092, 0.948297, 0.936831, 0.925592, 0.914494, 0.904159, 0.894643, 0.886417, 0.879620, 0.874023, 0.869533, 0.865967, 0.863238, 0.861113, 0.859527, 0.858367, 0.857594, 0.856882, 0.856172, 0.855316, 0.854197, 0.852818, 0.851062, 0.849046, 0.846747, 0.844043, 0.840842, 0.837164, 0.832985, 0.828344, 0.823544, 0.818276, 0.812543, 0.806374, 0.799838, 0.793170, 0.786246, 0.778956, 0.771297, 0.763278, 0.755252, 0.746984, 0.738445, 0.729688, 0.721045, 0.712189, 0.703099, 0.694045, 0.684930, 0.675601, 0.666480, 1.000000, 0.999999, 0.999978, 0.999888, 0.999639, 0.999093, 0.996310, 0.994405, 0.990527, 0.985186, 0.978518, 0.969748, 0.959597, 0.948104, 0.935724, 0.923704, 0.912023, 0.901356, 0.891850, 0.883847, 0.877280, 0.872289, 0.868583, 0.865913, 0.864098, 0.862993, 0.862356, 0.862125, 0.862107, 0.862168, 0.862359, 0.862490, 0.862430, 0.862063, 0.861431, 0.860386, 0.858950, 0.857090, 0.854848, 0.852381, 0.849503, 0.846167, 0.842399, 0.838194, 0.833566, 0.828579, 0.823464, 0.817951, 0.812079, 0.805873, 0.799320, 0.792533, 0.785715, 0.778636, 0.771260, 0.763618, 0.755719, 0.747815, 0.739825, 0.731602, 0.723212, 0.714845, 0.706465, 0.697933, 1.000000, 0.999998, 0.999969, 0.999836, 0.999475, 0.997943, 0.995219, 0.991760, 0.986663, 0.979592, 0.970218, 0.959155, 0.946575, 0.933047, 0.920022, 0.907749, 0.896801, 0.887506, 0.880077, 0.874322, 0.870126, 0.867481, 0.865949, 0.865293, 0.865287, 0.865746, 0.866502, 0.867439, 0.868442, 0.869382, 0.870161, 0.870782, 0.871303, 0.871511, 0.871427, 0.870978, 0.870136, 0.868892, 0.867248, 0.865209, 0.862775, 0.859944, 0.857004, 0.853671, 0.849984, 0.845927, 0.841518, 0.836774, 0.831750, 0.826407, 0.821001, 0.815333, 0.809412, 0.803238, 0.796802, 0.790204, 0.783457, 0.776713, 0.769749, 0.762596, 0.755239, 0.747690, 0.740127, 0.732595, 1.000000, 0.999997, 0.999950, 0.999744, 0.999162, 0.996124, 0.992844, 0.987757, 0.980062, 0.969642, 0.957087, 0.942735, 0.927747, 0.913622, 0.900889, 0.890115, 0.881584, 0.875288, 0.870926, 0.868307, 0.867033, 0.866972, 0.867692, 0.868950, 0.870549, 0.872320, 0.874144, 0.875947, 0.877674, 0.879192, 0.880478, 0.881539, 0.882307, 0.882739, 0.882902, 0.882847, 0.882461, 0.881725, 0.880636, 0.879197, 0.877422, 0.875296, 0.872849, 0.870076, 0.866988, 0.863637, 0.860159, 0.856475, 0.852525, 0.848328, 0.843883, 0.839198, 0.834322, 0.829221, 0.823907, 0.818461, 0.812972, 0.807316, 0.801474, 0.795459, 0.789276, 0.783025, 0.776615, 0.770223, 0.999999, 0.999994, 0.999909, 0.999536, 0.997195, 0.994123, 0.988168, 0.979344, 0.967003, 0.951763, 0.934724, 0.917948, 0.902918, 0.890432, 0.880902, 0.874401, 0.870394, 0.868503, 0.868209, 0.869062, 0.870725, 0.873006, 0.875558, 0.878230, 0.880893, 0.883445, 0.885832, 0.888059, 0.890058, 0.891782, 0.893247, 0.894460, 0.895397, 0.896023, 0.896380, 0.896433, 0.896198, 0.895673, 0.894865, 0.893908, 0.892700, 0.891224, 0.889501, 0.887539, 0.885336, 0.882903, 0.880244, 0.877373, 0.874296, 0.871019, 0.867549, 0.863933, 0.860153, 0.856355, 0.852395, 0.848277, 0.844006, 0.839587, 0.835045, 0.830378, 0.825579, 0.820649, 0.815592, 0.810432, 0.999998, 0.999988, 0.999795, 0.998892, 0.994635, 0.987290, 0.975397, 0.958508, 0.938352, 0.917733, 0.899800, 0.885878, 0.876516, 0.871200, 0.869099, 0.869317, 0.871112, 0.873870, 0.877160, 0.880682, 0.884228, 0.887737, 0.891076, 0.894161, 0.896981, 0.899543, 0.901847, 0.903882, 0.905672, 0.907188, 0.908451, 0.909480, 0.910289, 0.910878, 0.911259, 0.911430, 0.911396, 0.911154, 0.910712, 0.910081, 0.909266, 0.908264, 0.907094, 0.905752, 0.904244, 0.902577, 0.900799, 0.898931, 0.896923, 0.894782, 0.892513, 0.890117, 0.887600, 0.884968, 0.882222, 0.879369, 0.876408, 0.873345, 0.870183, 0.866926, 0.863575, 0.860160, 0.856672, 0.853098, 0.999991, 0.999947, 0.999158, 0.992842, 0.980107, 0.957230, 0.928231, 0.901539, 0.882688, 0.872588, 0.869394, 0.870671, 0.874458, 0.879378, 0.884639, 0.889770, 0.894601, 0.898972, 0.902930, 0.906456, 0.909568, 0.912329, 0.914750, 0.916893, 0.918774, 0.920429, 0.921868, 0.923110, 0.924185, 0.925089, 0.925842, 0.926457, 0.926934, 0.927285, 0.927522, 0.927639, 0.927650, 0.927553, 0.927356, 0.927061, 0.926671, 0.926187, 0.925617, 0.924962, 0.924224, 0.923409, 0.922519, 0.921555, 0.920521, 0.919419, 0.918252, 0.917021, 0.915729, 0.914377, 0.912967, 0.911503, 0.909984, 0.908414, 0.906791, 0.905122, 0.903401, 0.901637, 0.899826, 0.897972, 0.987461, 0.940121, 0.871507, 0.898572, 0.916705, 0.926425, 0.931922, 0.935265, 0.937431, 0.938899, 0.939950, 0.940717, 0.941301, 0.941754, 0.942111, 0.942397, 0.942631, 0.942823, 0.942983, 0.943117, 0.943231, 0.943329, 0.943412, 0.943484, 0.943545, 0.943599, 0.943644, 0.943682, 0.943716, 0.943744, 0.943766, 0.943785, 0.943799, 0.943808, 0.943815, 0.943818, 0.943818, 0.943814, 0.943807, 0.943797, 0.943784, 0.943769, 0.943751, 0.943730, 0.943707, 0.943681, 0.943652, 0.943623, 0.943589, 0.943554, 0.943518, 0.943479, 0.943438, 0.943396, 0.943351, 0.943305, 0.943257, 0.943207, 0.943156, 0.943104, 0.943049, 0.942993, 0.942936, 0.942877]; + #end +} diff --git a/Sources/armory/import.hx b/Sources/armory/import.hx new file mode 100644 index 0000000000..04d34bb711 --- /dev/null +++ b/Sources/armory/import.hx @@ -0,0 +1 @@ +import armory.system.Assert.*; diff --git a/Sources/armory/logicnode/ActiveCameraNode.hx b/Sources/armory/logicnode/ActiveCameraNode.hx new file mode 100644 index 0000000000..0af87d53e2 --- /dev/null +++ b/Sources/armory/logicnode/ActiveCameraNode.hx @@ -0,0 +1,10 @@ +package armory.logicnode; + +class ActiveCameraNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { return iron.Scene.active.camera; } +} diff --git a/Sources/armory/logicnode/ActiveSceneNode.hx b/Sources/armory/logicnode/ActiveSceneNode.hx new file mode 100644 index 0000000000..b183f85320 --- /dev/null +++ b/Sources/armory/logicnode/ActiveSceneNode.hx @@ -0,0 +1,10 @@ +package armory.logicnode; + +class ActiveSceneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { return iron.Scene.active.raw.name; } +} diff --git a/Sources/armory/logicnode/AddGroupNode.hx b/Sources/armory/logicnode/AddGroupNode.hx new file mode 100644 index 0000000000..3d8a55e2ef --- /dev/null +++ b/Sources/armory/logicnode/AddGroupNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import kha.arrays.Float32Array; +import iron.object.Object; + +class AddGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var groupName: String = inputs[1].get(); + var objects: Array = inputs[2].get(); + var raw = iron.Scene.active.raw; + var object_names = []; + + // Already exists + for (g in raw.groups) { + if (g.name == groupName) { + runOutput(0); + return; + } + } + + if(objects != null) + for(o in objects) + object_names.push(o.name); + + raw.groups.push({ name: groupName, object_refs: object_names, instance_offset: new Float32Array(3)}); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/AddObjectToGroupNode.hx b/Sources/armory/logicnode/AddObjectToGroupNode.hx new file mode 100644 index 0000000000..10a9aee40e --- /dev/null +++ b/Sources/armory/logicnode/AddObjectToGroupNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.object.Object; + +class AddObjectToGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var groupName: String = inputs[1].get(); + var object: Object = inputs[2].get(); + + iron.Scene.active.getGroup(groupName).push(object); + + runOutput(0); + + } +} diff --git a/Sources/armory/logicnode/AddPhysicsConstraintNode.hx b/Sources/armory/logicnode/AddPhysicsConstraintNode.hx new file mode 100644 index 0000000000..3013f7750d --- /dev/null +++ b/Sources/armory/logicnode/AddPhysicsConstraintNode.hx @@ -0,0 +1,114 @@ +package armory.logicnode; + +import iron.object.Object; + +#if arm_physics +import armory.trait.physics.PhysicsConstraint; +import armory.trait.physics.bullet.PhysicsConstraint.ConstraintType; +#end + +class AddPhysicsConstraintNode extends LogicNode { + + public var property0: String;//Type + public var object: Object; + public var rb1: Object; + public var rb2: Object; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var pivotObject: Object = inputs[1].get(); + rb1 = inputs[2].get(); + rb2 = inputs[3].get(); + + if (pivotObject == null || rb1 == null || rb2 == null) return; + +#if arm_physics + + var disableCollisions: Bool = inputs[4].get(); + var breakable: Bool = inputs[5].get(); + var breakingThreshold: Float = inputs[6].get(); + var type: ConstraintType = 0; + + var con: PhysicsConstraint = pivotObject.getTrait(PhysicsConstraint); + if (con == null) { + switch (property0) { + case "Fixed": type = Fixed; + case "Point": type = Point; + case "Hinge": type = Hinge; + case "Slider": type = Slider; + case "Piston": type = Piston; + case "Generic Spring": type = Generic; + } + + if (!breakable) breakingThreshold = 0.0; + + if (type != Generic) { + + con = new PhysicsConstraint(rb1, rb2, type, disableCollisions, breakingThreshold); + + switch (type) { + case Hinge: + var setLimit: Bool = inputs[7].get(); + var low: Float = inputs[8].get(); + var up: Float = inputs[9].get(); + con.setHingeConstraintLimits(setLimit, low, up); + + case Slider: + var setLimit: Bool = inputs[7].get(); + var low: Float = inputs[8].get(); + var up: Float = inputs[9].get(); + con.setSliderConstraintLimits(setLimit, low, up); + + case Piston: + var setLinLimit: Bool = inputs[7].get(); + var linLow: Float = inputs[8].get(); + var linUp: Float = inputs[9].get(); + var setAngLimit: Bool = inputs[10].get(); + var angLow: Float = inputs[11].get(); + var angUp: Float = inputs[12].get(); + con.setPistonConstraintLimits(setLinLimit, linLow, linUp, setAngLimit, angLow, angUp); + + default: + } + } + else { + var spring: Bool = false; + var prop: PhysicsConstraintNode; + + for (inp in 7...inputs.length) { + prop = inputs[inp].get(); + if (prop == null) continue; + if (prop.isSpring) { + spring = true; + break; + } + } + + if (spring) { + con = new PhysicsConstraint(rb1, rb2, GenericSpring, disableCollisions, breakingThreshold); + } + else { + con = new PhysicsConstraint(rb1, rb2, Generic, disableCollisions, breakingThreshold); + } + + for (inp in 7...inputs.length) { + prop = inputs[inp].get(); + if (prop == null) continue; + + if (prop.isSpring) { + con.setSpringParams(prop.isSpring, prop.value1, prop.value2, prop.axis, prop.isAngular); + } + else { + con.setGenericConstraintLimits(true, prop.value1, prop.value2, prop.axis, prop.isAngular); + } + } + } + pivotObject.addTrait(con); + } +#end + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/AddRigidBodyNode.hx b/Sources/armory/logicnode/AddRigidBodyNode.hx new file mode 100644 index 0000000000..5452f69a70 --- /dev/null +++ b/Sources/armory/logicnode/AddRigidBodyNode.hx @@ -0,0 +1,100 @@ +package armory.logicnode; + +import iron.object.Object; + +#if arm_physics +import armory.trait.physics.RigidBody; +import armory.trait.physics.bullet.RigidBody.Shape; +#end + + +class AddRigidBodyNode extends LogicNode { + + public var property0: String; //Shape + public var property1: Bool; //Advanced + public var object: Object; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + object = inputs[1].get(); + if (object == null) return; + +#if arm_physics + + var mass: Float = inputs[2].get(); + var active: Bool = inputs[3].get(); + var animated: Bool = inputs[4].get(); + var trigger: Bool = inputs[5].get(); + var friction: Float = inputs[6].get(); + var bounciness: Float = inputs[7].get(); + var ccd: Bool = inputs[8].get(); + + var margin: Bool = false; + var marginLen: Float = 0.0; + var linDamp: Float = 0.0; + var angDamp: Float = 0.0; + var angFriction: Float = 0.0; + var useDeactiv: Bool = false; + var linearVelThreshold: Float = 0.0; + var angVelThreshold: Float = 0.0; + var group: Int = 1; + var mask: Int = 1; + + var shape: Shape = 1; + + if (property1) { + margin = inputs[9].get(); + marginLen = inputs[10].get(); + linDamp = inputs[11].get(); + angDamp = inputs[12].get(); + angFriction = inputs[13].get(); + useDeactiv = inputs[14].get(); + linearVelThreshold = inputs[15].get(); + angVelThreshold = inputs[16].get(); + group = inputs[17].get(); + mask = inputs[18].get(); + } + + var rb: RigidBody = object.getTrait(RigidBody); + if ((group < 0) || (group > 32)) group = 1; //Limiting max groups to 32 + if ((mask < 0) || (mask > 32)) mask = 1; //Limiting max masks to 32 + if (rb == null) { + switch (property0) { + case "Box": shape = Box; + case "Sphere": shape = Sphere; + case "Capsule": shape = Capsule; + case "Cone": shape = Cone; + case "Cylinder": shape = Cylinder; + case "Convex Hull": shape = ConvexHull; + case "Mesh": shape = Mesh; + } + + rb = new RigidBody(shape, mass, friction, bounciness, group, mask); + rb.animated = animated; + rb.staticObj = !active; + rb.isTriggerObject(trigger); + + if (property1) { + rb.linearDamping = linDamp; + rb.angularDamping = angDamp; + rb.angularFriction = angFriction; + if (margin) rb.collisionMargin = marginLen; + if (useDeactiv) { + rb.setUpDeactivation(true, linearVelThreshold, angVelThreshold, 0.0); + } + } + + object.addTrait(rb); + } +#end + + runOutput(0); + } + + override function get(from: Int): Object { + return object; + } +} diff --git a/Sources/armory/logicnode/AddTraitNode.hx b/Sources/armory/logicnode/AddTraitNode.hx new file mode 100644 index 0000000000..07d536cd34 --- /dev/null +++ b/Sources/armory/logicnode/AddTraitNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; + +class AddTraitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var traitName: String = inputs[2].get(); + + assert(Error, object != null, "Object should not be null"); + assert(Error, traitName != null, "Trait name should not be null"); + + var cname = Type.resolveClass(Main.projectPackage + "." + traitName); + if (cname == null) cname = Type.resolveClass(Main.projectPackage + ".node." + traitName); + assert(Error, cname != null, 'No trait with the name "$traitName" found, make sure that the trait is exported!'); + assert(Warning, object.getTrait(cname) == null, 'Object already has the trait "$traitName" applied'); + + var trait = Type.createInstance(cname, []); + object.addTrait(trait); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/AlternateNode.hx b/Sources/armory/logicnode/AlternateNode.hx new file mode 100644 index 0000000000..2b18bfcbe3 --- /dev/null +++ b/Sources/armory/logicnode/AlternateNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class AlternateNode extends LogicNode { + + var i = 0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + if(i >= outputs.length) i = 0; + runOutput(i); + ++i; + + } +} diff --git a/Sources/armory/logicnode/AnimActionNode.hx b/Sources/armory/logicnode/AnimActionNode.hx new file mode 100644 index 0000000000..46c692bf80 --- /dev/null +++ b/Sources/armory/logicnode/AnimActionNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class AnimActionNode extends LogicNode { + + public var value: String; + + public function new(tree: LogicTree, value = "") { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/AnimationStateNode.hx b/Sources/armory/logicnode/AnimationStateNode.hx new file mode 100644 index 0000000000..ffd8326fbe --- /dev/null +++ b/Sources/armory/logicnode/AnimationStateNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.object.Object; + +class AnimationStateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + var animation = object.animation; + + if (animation == null) animation = object.getParentArmature(object.name); + + return switch (from) { + case 0: animation.action; + case 1: animation.currentFrame(); + case 2: animation.paused; + default: null; + } + } +} diff --git a/Sources/armory/logicnode/AppendTransformNode.hx b/Sources/armory/logicnode/AppendTransformNode.hx new file mode 100644 index 0000000000..2f573efbee --- /dev/null +++ b/Sources/armory/logicnode/AppendTransformNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Mat4; +#if arm_physics +import armory.trait.physics.RigidBody; +#end + +class AppendTransformNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var matrix: Mat4 = inputs[2].get(); + + if (object == null || matrix == null) return; + + object.transform.multMatrix(matrix); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ApplyForceAtLocationNode.hx b/Sources/armory/logicnode/ApplyForceAtLocationNode.hx new file mode 100644 index 0000000000..95ef79889d --- /dev/null +++ b/Sources/armory/logicnode/ApplyForceAtLocationNode.hx @@ -0,0 +1,38 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyForceAtLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var force: Vec4 = inputs[2].get(); + var localForce: Bool = inputs.length > 3 ? inputs[3].get() : false; + var location: Vec4 = new Vec4().setFrom(inputs[4].get()); + var localLoc: Bool = inputs.length > 5 ? inputs[5].get() : false; + + if (object == null || force == null || location == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + if (!localLoc) { + location.sub(object.transform.world.getLoc()); + } + + !localForce ? rb.applyForce(force, location) : rb.applyForce(object.transform.worldVecToOrientation(force), location); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ApplyForceNode.hx b/Sources/armory/logicnode/ApplyForceNode.hx new file mode 100644 index 0000000000..845337c20b --- /dev/null +++ b/Sources/armory/logicnode/ApplyForceNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyForceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var force: Vec4 = inputs[2].get(); + var local: Bool = inputs.length > 3 ? inputs[3].get() : false; + + if (object == null || force == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + !local ? rb.applyForce(force) : rb.applyForce(object.transform.worldVecToOrientation(force)); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ApplyImpulseAtLocationNode.hx b/Sources/armory/logicnode/ApplyImpulseAtLocationNode.hx new file mode 100644 index 0000000000..6b7fa8cd54 --- /dev/null +++ b/Sources/armory/logicnode/ApplyImpulseAtLocationNode.hx @@ -0,0 +1,37 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyImpulseAtLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var impulse: Vec4 = inputs[2].get(); + var localImpulse: Bool = inputs.length > 3 ? inputs[3].get() : false; + var location: Vec4 = new Vec4().setFrom(inputs[4].get()); + var localLoc: Bool = inputs.length > 5 ? inputs[5].get() : false; + + if (object == null || impulse == null || location == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + if (!localLoc) { + location.sub(object.transform.world.getLoc()); + } + + !localImpulse ? rb.applyImpulse(impulse, location) : rb.applyImpulse(object.transform.worldVecToOrientation(impulse), location); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ApplyImpulseNode.hx b/Sources/armory/logicnode/ApplyImpulseNode.hx new file mode 100644 index 0000000000..a227467358 --- /dev/null +++ b/Sources/armory/logicnode/ApplyImpulseNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyImpulseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var impulse: Vec4 = inputs[2].get(); + var local: Bool = inputs.length > 3 ? inputs[3].get() : false; + + if (object == null || impulse == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + !local ? rb.applyImpulse(impulse) : rb.applyImpulse(object.transform.worldVecToOrientation(impulse)); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ApplyTorqueImpulseNode.hx b/Sources/armory/logicnode/ApplyTorqueImpulseNode.hx new file mode 100644 index 0000000000..d7f72d76a8 --- /dev/null +++ b/Sources/armory/logicnode/ApplyTorqueImpulseNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyTorqueImpulseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var torque: Vec4 = inputs[2].get(); + var local: Bool = inputs.length > 3 ? inputs[3].get() : false; + + if (object == null || torque == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + !local ? rb.applyTorqueImpulse(torque) : rb.applyTorqueImpulse(object.transform.worldVecToOrientation(torque)); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ApplyTorqueNode.hx b/Sources/armory/logicnode/ApplyTorqueNode.hx new file mode 100644 index 0000000000..5ef44fb7a5 --- /dev/null +++ b/Sources/armory/logicnode/ApplyTorqueNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class ApplyTorqueNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var torque: Vec4 = inputs[2].get(); + var local: Bool = inputs.length > 3 ? inputs[3].get() : false; + + if (object == null || torque == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + !local ? rb.applyTorque(torque) : rb.applyTorque(object.transform.worldVecToOrientation(torque)); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/ArrayAddNode.hx b/Sources/armory/logicnode/ArrayAddNode.hx new file mode 100644 index 0000000000..82eb0211c2 --- /dev/null +++ b/Sources/armory/logicnode/ArrayAddNode.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +class ArrayAddNode extends LogicNode { + + var ar: Array; + var array: Array; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + ar = inputs[1].get(); + + if (ar == null) return; + + // "Modify Original" == `false` -> Copy the input array + if (!inputs[2].get()) { + ar = ar.copy(); + } + + array = ar.map(item -> Std.string(item)); + + if (inputs.length > 4) { + for (i in 4...inputs.length) { + var value: Dynamic = inputs[i].get(); + + // "Unique Values" options only supports primitive data types + // for now, a custom indexOf() or contains() method would be + // required to compare values of other types + + //meanwhile an efficient comparison method is defined, it can be compared as a string representation. + var type: Bool = value is Bool || value is Float || value is Int || value is String; + + if (!inputs[3].get() || (type ? ar.indexOf(value) : array.indexOf(Std.string(value))) == -1) { + ar.push(value); + } + } + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return ar; + } +} diff --git a/Sources/armory/logicnode/ArrayBooleanNode.hx b/Sources/armory/logicnode/ArrayBooleanNode.hx new file mode 100644 index 0000000000..8eea5d51de --- /dev/null +++ b/Sources/armory/logicnode/ArrayBooleanNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayBooleanNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Bool = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayColorNode.hx b/Sources/armory/logicnode/ArrayColorNode.hx new file mode 100644 index 0000000000..20f7a17c60 --- /dev/null +++ b/Sources/armory/logicnode/ArrayColorNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class ArrayColorNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Vec4 = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayCompareNode.hx b/Sources/armory/logicnode/ArrayCompareNode.hx new file mode 100644 index 0000000000..84c8dfd629 --- /dev/null +++ b/Sources/armory/logicnode/ArrayCompareNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class ArrayCompareNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar1: Array = inputs[0].get(); + var ar2: Array = inputs[1].get(); + + return ar1.toString() == ar2.toString() ? true : false; + + } +} diff --git a/Sources/armory/logicnode/ArrayConcatNode.hx b/Sources/armory/logicnode/ArrayConcatNode.hx new file mode 100644 index 0000000000..717afb55d7 --- /dev/null +++ b/Sources/armory/logicnode/ArrayConcatNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class ArrayConcatNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar1: Array = inputs[0].get(); + var ar2: Array = inputs[1].get(); + + var ar = ar1.concat(ar2); + + return from == 0 ? ar : ar.length; + + } +} diff --git a/Sources/armory/logicnode/ArrayCountNode.hx b/Sources/armory/logicnode/ArrayCountNode.hx new file mode 100644 index 0000000000..4a4e84dbcb --- /dev/null +++ b/Sources/armory/logicnode/ArrayCountNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +class ArrayCountNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var values: Array = []; + var values_list: Array = []; + var count: Array = []; + var val_count: Array = []; + + for(item in ar){ + if(values.indexOf(Std.string(item)) == -1){ + values_list.push(item); + values.push(Std.string(item)); + count.push(1); + } + else { + count[values.indexOf(Std.string(item))] += 1; + } + } + + for(i in 0...values_list.length) + val_count.push([values_list[i], count[i]]); + + return val_count; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/ArrayDisplayNode.hx b/Sources/armory/logicnode/ArrayDisplayNode.hx new file mode 100644 index 0000000000..824fc8ab50 --- /dev/null +++ b/Sources/armory/logicnode/ArrayDisplayNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +class ArrayDisplayNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var separator: String = inputs[1].get(); + + var prop: String = null; + + if(property0 != 'Item') + prop = inputs[2].get(); + + if(property0 == 'Item') + return ar.join(separator); + else if(property0 == 'Item Field') + return [for (v in ar) Reflect.field(v, prop)].join(separator); + else if(property0 == 'Item Property') + return [for (v in ar) Reflect.field(v.properties.h, prop)].join(separator); + + return null; + + } +} diff --git a/Sources/armory/logicnode/ArrayDistinctNode.hx b/Sources/armory/logicnode/ArrayDistinctNode.hx new file mode 100644 index 0000000000..2b47329ce7 --- /dev/null +++ b/Sources/armory/logicnode/ArrayDistinctNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayDistinctNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var ar_list: Array = []; + var distinct: Array = []; + var duplicated: Array = []; + + for(item in ar) + if(ar_list.indexOf(Std.string(item)) == -1){ + ar_list.push(Std.string(item)); + distinct.push(item); + } + else + duplicated.push(item); + + return from == 0 ? distinct: duplicated; + + } +} diff --git a/Sources/armory/logicnode/ArrayFilterNode.hx b/Sources/armory/logicnode/ArrayFilterNode.hx new file mode 100644 index 0000000000..75aea97c2b --- /dev/null +++ b/Sources/armory/logicnode/ArrayFilterNode.hx @@ -0,0 +1,137 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class ArrayFilterNode extends LogicNode { + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var type: Dynamic; + + if(property0 == 'Item') + type = inputs[1].get(); + else + type = inputs[2].get(); + + var prop: String = inputs[1].get(); + + var arr: Array = null; + + if(property0 == 'Item'){ + if(Std.isOfType(type, Vec4)){ + var value: Vec4 = inputs[1].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> item.equals(value)); + case 'Not Equal': arr = ar.filter(item -> !item.equals(value)); + case 'Between': { + var value2: Vec4 = inputs[2].get(); + arr = ar.filter(item -> value.x <= item.x && value.y <= item.y && value.z <= item.z && item.x <= value2.x && item.y <= value2.y && item.z <= value2.z); + } + } + } + else if(Std.isOfType(type, String)){ + var value: String = inputs[1].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> item == value); + case 'Not Equal': arr = ar.filter(item -> item != value); + case 'Contains': arr = ar.filter(item -> item.indexOf(value) >= 0); + case 'Starts With': arr = ar.filter(item -> StringTools.startsWith(item, value)); + case 'Ends With': arr = ar.filter(item -> StringTools.endsWith(item, value)); + } + } + else{ + var value = inputs[1].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> item == value); + case 'Not Equal': arr = ar.filter(item -> item != value); + case 'Between': { var value2 = inputs[2].get(); arr = ar.filter(item -> value <= item && item <= value2); } + case 'Less': arr = ar.filter(item -> item < value); + case 'Less Equal': arr = ar.filter(item -> item <= value); + case 'Greater': arr = ar.filter(item -> item > value); + case 'Greater Equal': arr = ar.filter(item -> item >= value); + } + } + } + else if(property0 == 'Item Field'){ + if(Std.isOfType(type, Vec4)){ + var value: Vec4 = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item, prop).equals(value)); + case 'Not Equal': arr = ar.filter(item -> !Reflect.field(item, prop).equals(value)); + case 'Between': { + var value2: Vec4 = inputs[2].get(); + arr = ar.filter(item -> value.x <= Reflect.field(item, prop).x && value.y <= Reflect.field(item, prop).y && value.z <= Reflect.field(item, prop).z && Reflect.field(item, prop).x <= value2.x && Reflect.field(item, prop).y <= value2.y && Reflect.field(item, prop).z <= value2.z); + } + } + } + else if(Std.isOfType(type, String)){ + var value: String = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item, prop) == value); + case 'Not Equal': arr = ar.filter(item -> Reflect.field(item, prop) != value); + case 'Contains': arr = ar.filter(item -> Reflect.field(item, prop).indexOf(value) >= 0); + case 'Starts With': arr = ar.filter(item -> StringTools.startsWith(Reflect.field(item, prop), value)); + case 'Ends With': arr = ar.filter(item -> StringTools.endsWith(Reflect.field(item, prop), value)); + } + } + else{ + var value: Dynamic = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item, prop) == value); + case 'Not Equal': arr = ar.filter(item -> Reflect.field(item, prop) != value); + case 'Between': { var value2: Dynamic = inputs[2].get(); arr = ar.filter(item -> value <= Reflect.field(item, prop) && Reflect.field(item, prop) <= value2); } + case 'Less': arr = ar.filter(item -> Reflect.field(item, prop) < value); + case 'Less Equal': arr = ar.filter(item -> Reflect.field(item, prop) <= value); + case 'Greater': arr = ar.filter(item -> Reflect.field(item, prop) > value); + case 'Greater Equal': arr = ar.filter(item -> Reflect.field(item, prop) >= value); + } + } + } + else if(property0 == 'Item Property'){ + if(Std.isOfType(type, Vec4)){ + var value: Vec4 = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop).equals(value)); + case 'Not Equal': arr = ar.filter(item -> !Reflect.field(item.properties.h, prop).equals(value)); + case 'Between': { + var value2: Vec4 = inputs[2].get(); + arr = ar.filter(item -> value.x <= Reflect.field(item.properties.h, prop).x && value.y <= Reflect.field(item.properties.h, prop).y && value.z <= Reflect.field(item.properties.h, prop).z && Reflect.field(item.properties.h, prop).x <= value2.x && Reflect.field(item.properties.h, prop).y <= value2.y && Reflect.field(item.properties.h, prop).z <= value2.z); + } + } + } + else if(Std.isOfType(type, String)){ + var value: String = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) == value); + case 'Not Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) != value); + case 'Contains': arr = ar.filter(item -> Reflect.field(item.properties.h, prop).indexOf(value) >= 0); + case 'Starts With': arr = ar.filter(item -> StringTools.startsWith(Reflect.field(item.properties.h, prop), value)); + case 'Ends With': arr = ar.filter(item -> StringTools.endsWith(Reflect.field(item.properties.h, prop), value)); + } + } + else{ + var value: Dynamic = inputs[2].get(); + switch(property1){ + case 'Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) == value); + case 'Not Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) != value); + case 'Between': { var value2: Dynamic = inputs[2].get(); arr = ar.filter(item -> value <= Reflect.field(item.properties.h, prop) && Reflect.field(item.properties.h, prop) <= value2); } + case 'Less': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) < value); + case 'Less Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) <= value); + case 'Greater': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) > value); + case 'Greater Equal': arr = ar.filter(item -> Reflect.field(item.properties.h, prop) >= value); + } + } + } + + return arr; + + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/ArrayFloatNode.hx b/Sources/armory/logicnode/ArrayFloatNode.hx new file mode 100644 index 0000000000..8d5bcf9843 --- /dev/null +++ b/Sources/armory/logicnode/ArrayFloatNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayFloatNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Float = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayGetNextNode.hx b/Sources/armory/logicnode/ArrayGetNextNode.hx new file mode 100644 index 0000000000..9ed2aaf7ad --- /dev/null +++ b/Sources/armory/logicnode/ArrayGetNextNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +class ArrayGetNextNode extends LogicNode { + + var i = 0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + if (ar == null) return null; + + var value = ar[i]; + + if (i < ar.length - 1) + i++; + else + i = 0; + + return value; + + } +} diff --git a/Sources/armory/logicnode/ArrayGetNode.hx b/Sources/armory/logicnode/ArrayGetNode.hx new file mode 100644 index 0000000000..a49f57ffd9 --- /dev/null +++ b/Sources/armory/logicnode/ArrayGetNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +class ArrayGetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + if (ar == null) return null; + + var i: Int = inputs[1].get(); + + if (i < 0) i = ar.length + i; + if (i < 0 || i > ar.length - 1) { + + var className = Type.getClassName(Type.getClass(tree)); + var traitName = className.substring(className.lastIndexOf(".") + 1); + var objectName = tree.object.name; + + trace('Logic error (object: $objectName, trait: $traitName): Array Get - index out of range'); + + return null; + } + return ar[i]; + } +} diff --git a/Sources/armory/logicnode/ArrayGetPreviousNextNode.hx b/Sources/armory/logicnode/ArrayGetPreviousNextNode.hx new file mode 100644 index 0000000000..ed169abbe5 --- /dev/null +++ b/Sources/armory/logicnode/ArrayGetPreviousNextNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +class ArrayGetPreviousNextNode extends LogicNode { + + var i = 0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + var direction: Bool = inputs[1].get(); + + if (ar == null) return null; + + if(direction) + if (i < ar.length - 1) + i++; + else + i = 0; + else + if (i <= 0) + i = ar.length-1; + else + i--; + + var value = ar[i]; + + return value; + + } +} diff --git a/Sources/armory/logicnode/ArrayInArrayNode.hx b/Sources/armory/logicnode/ArrayInArrayNode.hx new file mode 100644 index 0000000000..4b6019fe63 --- /dev/null +++ b/Sources/armory/logicnode/ArrayInArrayNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class ArrayInArrayNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var array: Array = inputs[0].get(); + array = array.map(item -> Std.string(item)); + var value: Dynamic = inputs[1].get(); + + return array.indexOf(Std.string(value)) != -1; + } +} diff --git a/Sources/armory/logicnode/ArrayIndexNode.hx b/Sources/armory/logicnode/ArrayIndexNode.hx new file mode 100644 index 0000000000..0a8c45f4e8 --- /dev/null +++ b/Sources/armory/logicnode/ArrayIndexNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class ArrayIndexNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var array: Array = inputs[0].get(); + array = array.map(item -> Std.string(item)); + var value: Dynamic = inputs[1].get(); + + return array.indexOf(Std.string(value)); + } +} diff --git a/Sources/armory/logicnode/ArrayIntegerNode.hx b/Sources/armory/logicnode/ArrayIntegerNode.hx new file mode 100644 index 0000000000..2a0c5ddad4 --- /dev/null +++ b/Sources/armory/logicnode/ArrayIntegerNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayIntegerNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Int = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayLengthNode.hx b/Sources/armory/logicnode/ArrayLengthNode.hx new file mode 100644 index 0000000000..b0a3dc1fa5 --- /dev/null +++ b/Sources/armory/logicnode/ArrayLengthNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class ArrayLengthNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + return ar != null ? ar.length : 0; + } +} diff --git a/Sources/armory/logicnode/ArrayLoopNode.hx b/Sources/armory/logicnode/ArrayLoopNode.hx new file mode 100644 index 0000000000..fecbbc92b2 --- /dev/null +++ b/Sources/armory/logicnode/ArrayLoopNode.hx @@ -0,0 +1,40 @@ +package armory.logicnode; + +class ArrayLoopNode extends LogicNode { + + var value: Dynamic; + var index: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + index = -1; + for (val in ar) { + value = val; + index++; + runOutput(0); + + if (tree.loopBreak) { + tree.loopBreak = false; + break; + } + + if (tree.loopContinue) { + tree.loopContinue = false; + continue; + } + } + runOutput(3); + } + + override function get(from: Int): Dynamic { + if (from == 1) + return value; + return index; + } +} diff --git a/Sources/armory/logicnode/ArrayNode.hx b/Sources/armory/logicnode/ArrayNode.hx new file mode 100644 index 0000000000..cfb751ca13 --- /dev/null +++ b/Sources/armory/logicnode/ArrayNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Dynamic = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayObjectNode.hx b/Sources/armory/logicnode/ArrayObjectNode.hx new file mode 100644 index 0000000000..681b8e7770 --- /dev/null +++ b/Sources/armory/logicnode/ArrayObjectNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.object.Object; + +class ArrayObjectNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Object = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayPopNode.hx b/Sources/armory/logicnode/ArrayPopNode.hx new file mode 100644 index 0000000000..94b5b302d9 --- /dev/null +++ b/Sources/armory/logicnode/ArrayPopNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class ArrayPopNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + if (ar == null) return null; + + return ar.pop(); + } +} diff --git a/Sources/armory/logicnode/ArrayRemoveNode.hx b/Sources/armory/logicnode/ArrayRemoveNode.hx new file mode 100644 index 0000000000..31e6450818 --- /dev/null +++ b/Sources/armory/logicnode/ArrayRemoveNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayRemoveNode extends LogicNode { + + var removedValue: Dynamic = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + var i: Int = inputs[2].get(); + if (i < 0) i = ar.length + i; + + removedValue = ar[i]; + ar.splice(i, 1); + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return removedValue; + } +} diff --git a/Sources/armory/logicnode/ArrayRemoveValueNode.hx b/Sources/armory/logicnode/ArrayRemoveValueNode.hx new file mode 100644 index 0000000000..b1bf34a351 --- /dev/null +++ b/Sources/armory/logicnode/ArrayRemoveValueNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +class ArrayRemoveValueNode extends LogicNode { + + var removedValue: Dynamic = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + var val: Dynamic = inputs[2].get(); + + removedValue = val; + ar.remove(val); + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return removedValue; + } +} diff --git a/Sources/armory/logicnode/ArrayResizeNode.hx b/Sources/armory/logicnode/ArrayResizeNode.hx new file mode 100644 index 0000000000..053588ffaa --- /dev/null +++ b/Sources/armory/logicnode/ArrayResizeNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class ArrayResizeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + var len = inputs[2].get(); + + ar.resize(len); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ArrayReverseNode.hx b/Sources/armory/logicnode/ArrayReverseNode.hx new file mode 100644 index 0000000000..694de666cc --- /dev/null +++ b/Sources/armory/logicnode/ArrayReverseNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class ArrayReverseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var arr = ar.copy(); + + arr.reverse(); + + return arr; + + } +} diff --git a/Sources/armory/logicnode/ArraySampleNode.hx b/Sources/armory/logicnode/ArraySampleNode.hx new file mode 100644 index 0000000000..248697a0f6 --- /dev/null +++ b/Sources/armory/logicnode/ArraySampleNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +class ArraySampleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + var sample = inputs[1].get(); + var remove = inputs[2].get(); + + if (ar == null || sample == 0) return null; + + var n = Std.int(Math.min(sample, ar.length)); + var copy = remove ? ar : ar.copy(), result = []; + for (i in 0...n) + result.push(copy.splice(Std.random(copy.length), 1)[0]); + + return result; + } +} diff --git a/Sources/armory/logicnode/ArraySetNode.hx b/Sources/armory/logicnode/ArraySetNode.hx new file mode 100644 index 0000000000..9a113dc22d --- /dev/null +++ b/Sources/armory/logicnode/ArraySetNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class ArraySetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + var i: Int = inputs[2].get(); + var value: Dynamic = inputs[3].get(); + + if (i < 0) ar[ar.length + i] = value; + else ar[i] = value; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ArrayShiftNode.hx b/Sources/armory/logicnode/ArrayShiftNode.hx new file mode 100644 index 0000000000..5a49f7bf4d --- /dev/null +++ b/Sources/armory/logicnode/ArrayShiftNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class ArrayShiftNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + if (ar == null) return null; + + return ar.shift(); + } +} diff --git a/Sources/armory/logicnode/ArrayShuffleNode.hx b/Sources/armory/logicnode/ArrayShuffleNode.hx new file mode 100644 index 0000000000..45850863bf --- /dev/null +++ b/Sources/armory/logicnode/ArrayShuffleNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayShuffleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + + var t = [], array = []; + + for(i in 0...ar.length) + t.push(i); + + while (t.length > 0) { + var pos = Std.random(t.length), index = t[pos]; + t.splice(pos, 1); + array.push(ar[index]); + } + + return array; + } +} + + diff --git a/Sources/armory/logicnode/ArraySliceNode.hx b/Sources/armory/logicnode/ArraySliceNode.hx new file mode 100644 index 0000000000..cbdc1bb22f --- /dev/null +++ b/Sources/armory/logicnode/ArraySliceNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArraySliceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + if (ar == null) return null; + + var i: Int = inputs[1].get(); + var end: Int = inputs[2].get(); + + if (i < 0) i = ar.length + i; + if (i < 0 || i > ar.length - 1) { + var className = Type.getClassName(Type.getClass(tree)); + var traitName = className.substring(className.lastIndexOf(".") + 1); + var objectName = tree.object.name; + trace('Logic error (object: $objectName, trait: $traitName): Array Get - index out of range'); + return null; + } + + return ar.slice(i, end); + } +} diff --git a/Sources/armory/logicnode/ArraySortNode.hx b/Sources/armory/logicnode/ArraySortNode.hx new file mode 100644 index 0000000000..235ca049c8 --- /dev/null +++ b/Sources/armory/logicnode/ArraySortNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class ArraySortNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ar: Array = inputs[0].get(); + var desc: Bool = inputs[1].get(); + + var arr = ar.copy(); + + arr.sort(Reflect.compare); + + if (desc) arr.reverse(); + + return arr; + + } +} diff --git a/Sources/armory/logicnode/ArraySpliceNode.hx b/Sources/armory/logicnode/ArraySpliceNode.hx new file mode 100644 index 0000000000..f1e12a1500 --- /dev/null +++ b/Sources/armory/logicnode/ArraySpliceNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class ArraySpliceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var ar: Array = inputs[1].get(); + if (ar == null) return; + + var i = inputs[2].get(); + var len = inputs[3].get(); + + ar.splice(i, len); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ArrayStringNode.hx b/Sources/armory/logicnode/ArrayStringNode.hx new file mode 100644 index 0000000000..cb47eef66b --- /dev/null +++ b/Sources/armory/logicnode/ArrayStringNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ArrayStringNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: String = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ArrayVectorNode.hx b/Sources/armory/logicnode/ArrayVectorNode.hx new file mode 100644 index 0000000000..fa13854332 --- /dev/null +++ b/Sources/armory/logicnode/ArrayVectorNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class ArrayVectorNode extends LogicNode { + + public var value: Array = []; + var initialized = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (!initialized) { + initialized = true; + for (inp in inputs) { + var val: Vec4 = inp.get(); + value.push(val); + } + } + + return from == 0 ? value : value.length; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/BitwiseMathNode.hx b/Sources/armory/logicnode/BitwiseMathNode.hx new file mode 100644 index 0000000000..4a5f5733dd --- /dev/null +++ b/Sources/armory/logicnode/BitwiseMathNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +class BitwiseMathNode extends LogicNode { + + /** The operation to perform. **/ + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + final op1: Int = inputs[0].get(); + + if (property0 == "negation") { + return ~op1; + } + + final op2: Int = inputs[1].get(); + + return switch (property0) { + case "and": op1 & op2; + case "or": op1 | op2; + case "xor": op1 ^ op2; + + case "left_shift": op1 << op2; + case "right_shift": op1 >> op2; + case "unsigned_right_shift": op1 >>> op2; + + default: 0; + } + } +} diff --git a/Sources/armory/logicnode/BlendActionNode.hx b/Sources/armory/logicnode/BlendActionNode.hx new file mode 100644 index 0000000000..43b3742c71 --- /dev/null +++ b/Sources/armory/logicnode/BlendActionNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.Object; + +class BlendActionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var action1: String = inputs[2].get(); + var action2: String = inputs[3].get(); + var blendFactor: Float = inputs[4].get(); + + if (object == null || action1 == "" || action2 == "") return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.blend(action1, action2, blendFactor); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/BloomGetNode.hx b/Sources/armory/logicnode/BloomGetNode.hx new file mode 100644 index 0000000000..e40562c54a --- /dev/null +++ b/Sources/armory/logicnode/BloomGetNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class BloomGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.bloom_uniforms[0]; + case 1: armory.renderpath.Postprocess.bloom_uniforms[1]; + case 2: armory.renderpath.Postprocess.bloom_uniforms[2]; + case 3: #if rp_bloom Main.bloomRadius #else 0.0 #end; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/BloomSetNode.hx b/Sources/armory/logicnode/BloomSetNode.hx new file mode 100644 index 0000000000..09018df8a9 --- /dev/null +++ b/Sources/armory/logicnode/BloomSetNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class BloomSetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + armory.renderpath.Postprocess.bloom_uniforms[0] = inputs[1].get(); + armory.renderpath.Postprocess.bloom_uniforms[1] = inputs[2].get(); + armory.renderpath.Postprocess.bloom_uniforms[2] = inputs[3].get(); + + #if rp_bloom + Main.bloomRadius = Math.max(inputs[4].get(), 0.0); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/BoneFKNode.hx b/Sources/armory/logicnode/BoneFKNode.hx new file mode 100644 index 0000000000..9d72c876da --- /dev/null +++ b/Sources/armory/logicnode/BoneFKNode.hx @@ -0,0 +1,64 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Vec4; +import iron.object.Object; +import iron.object.BoneAnimation; +import iron.math.Mat4; + +class BoneFKNode extends LogicNode { + + var notified = false; + var m: Mat4 = null; + var w: Mat4 = null; + var iw = Mat4.identity(); + + 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 transform: Mat4 = 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); + + function moveBone() { + + var t2 = Mat4.identity(); + var loc= new Vec4(); + var rot = new Quat(); + var scl = new Vec4(); + + //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) { + anim.notifyOnUpdate(moveBone); + notified = true; + } + + runOutput(0); + + #end + } +} diff --git a/Sources/armory/logicnode/BoneIKNode.hx b/Sources/armory/logicnode/BoneIKNode.hx new file mode 100644 index 0000000000..1f1cfd7669 --- /dev/null +++ b/Sources/armory/logicnode/BoneIKNode.hx @@ -0,0 +1,62 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.object.BoneAnimation; +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) { + super(tree); + } + + override function run(from: Int) { + #if arm_skin + + 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; + if (anim == null) anim = object.getParentArmature(object.name); + + var bone = anim.getBone(boneName); + + if(! poleEnabled) pole = null; + + 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) { + anim.notifyOnUpdate(solveBone); + notified = true; + } + + runOutput(0); + + #end + } +} diff --git a/Sources/armory/logicnode/BooleanNode.hx b/Sources/armory/logicnode/BooleanNode.hx new file mode 100644 index 0000000000..88317f7fff --- /dev/null +++ b/Sources/armory/logicnode/BooleanNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class BooleanNode extends LogicNode { + + public var value: Bool; + + public function new(tree: LogicTree, value = false) { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/BranchNode.hx b/Sources/armory/logicnode/BranchNode.hx new file mode 100644 index 0000000000..f35f40d57a --- /dev/null +++ b/Sources/armory/logicnode/BranchNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class BranchNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var b: Bool = inputs[1].get(); + b ? runOutput(0) : runOutput(1); + } +} diff --git a/Sources/armory/logicnode/CallFunctionNode.hx b/Sources/armory/logicnode/CallFunctionNode.hx new file mode 100644 index 0000000000..fe1a17e61e --- /dev/null +++ b/Sources/armory/logicnode/CallFunctionNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +import iron.object.Object; + +class CallFunctionNode extends LogicNode { + + var result: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Dynamic = inputs[1].get(); + if (object == null) return; + + var funName: String = inputs[2].get(); + var args: Array = []; + + for (i in 3...inputs.length) { + args.push(inputs[i].get()); + } + + var func = Reflect.field(object, funName); + if (func != null) { + result = Reflect.callMethod(object, func, args); + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return result; + } +} diff --git a/Sources/armory/logicnode/CallGroupNode.hx b/Sources/armory/logicnode/CallGroupNode.hx new file mode 100644 index 0000000000..21c2c77f67 --- /dev/null +++ b/Sources/armory/logicnode/CallGroupNode.hx @@ -0,0 +1,9 @@ +package armory.logicnode; + +class CallGroupNode extends LogicNode { + + public var property0: String; + public function new(tree: LogicTree) { + super(tree); + } +} diff --git a/Sources/armory/logicnode/CallHaxeStaticNode.hx b/Sources/armory/logicnode/CallHaxeStaticNode.hx new file mode 100644 index 0000000000..bc4366c464 --- /dev/null +++ b/Sources/armory/logicnode/CallHaxeStaticNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +class CallHaxeStaticNode extends LogicNode { + + var result: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var path: String = inputs[1].get(); + if (path != "") { + var args: Array = []; + + for (i in 2...inputs.length) { + args.push(inputs[i].get()); + } + + var dotIndex = path.lastIndexOf("."); + var classPath = path.substr(0, dotIndex); + var classType = Type.resolveClass(classPath); + var funName = path.substr(dotIndex + 1); + result = Reflect.callMethod(classType, Reflect.field(classType, funName), args); + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return result; + } +} diff --git a/Sources/armory/logicnode/CameraGetNode.hx b/Sources/armory/logicnode/CameraGetNode.hx new file mode 100644 index 0000000000..b22a2f7ada --- /dev/null +++ b/Sources/armory/logicnode/CameraGetNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +class CameraGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.camera_uniforms[0];//Camera: F-Number + case 1: armory.renderpath.Postprocess.camera_uniforms[1];//Camera: Shutter time + case 2: armory.renderpath.Postprocess.camera_uniforms[2];//Camera: ISO + case 3: armory.renderpath.Postprocess.camera_uniforms[3];//Camera: Exposure Compensation + case 4: armory.renderpath.Postprocess.camera_uniforms[4];//Fisheye Distortion + case 5: armory.renderpath.Postprocess.camera_uniforms[5];//DoF AutoFocus §§ If true, it ignores the DoF Distance setting + case 6: armory.renderpath.Postprocess.camera_uniforms[6];//DoF Distance + case 7: armory.renderpath.Postprocess.camera_uniforms[7];//DoF Focal Length mm + case 8: armory.renderpath.Postprocess.camera_uniforms[8];//DoF F-Stop + case 9: armory.renderpath.Postprocess.camera_uniforms[9];//Tonemapping Method + case 10: armory.renderpath.Postprocess.camera_uniforms[10];//Distort + case 11: armory.renderpath.Postprocess.camera_uniforms[11];//Film Grain + case 12: armory.renderpath.Postprocess.camera_uniforms[12];//Sharpen + case 13: armory.renderpath.Postprocess.camera_uniforms[13];//Vignette + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/CameraSetNode.hx b/Sources/armory/logicnode/CameraSetNode.hx new file mode 100644 index 0000000000..68680f24a4 --- /dev/null +++ b/Sources/armory/logicnode/CameraSetNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class CameraSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + armory.renderpath.Postprocess.camera_uniforms[0] = inputs[1].get();//Camera: F-Number + armory.renderpath.Postprocess.camera_uniforms[1] = inputs[2].get();//Camera: Shutter time + armory.renderpath.Postprocess.camera_uniforms[2] = inputs[3].get();//Camera: ISO + armory.renderpath.Postprocess.camera_uniforms[3] = inputs[4].get();//Camera: Exposure Compensation + armory.renderpath.Postprocess.camera_uniforms[4] = inputs[5].get();//Fisheye Distortion + armory.renderpath.Postprocess.camera_uniforms[5] = inputs[6].get();//DoF AutoFocus §§ If true, it ignores the DoF Distance setting + armory.renderpath.Postprocess.camera_uniforms[6] = inputs[7].get();//DoF Distance + armory.renderpath.Postprocess.camera_uniforms[7] = inputs[8].get();//DoF Focal Length mm + armory.renderpath.Postprocess.camera_uniforms[8] = inputs[9].get();//DoF F-Stop + armory.renderpath.Postprocess.camera_uniforms[9] = inputs[10].get();//Tonemapping Method + armory.renderpath.Postprocess.camera_uniforms[10] = inputs[11].get();//Distort + armory.renderpath.Postprocess.camera_uniforms[11] = inputs[12].get();//Film Grain + armory.renderpath.Postprocess.camera_uniforms[12] = inputs[13].get();//Sharpen + armory.renderpath.Postprocess.camera_uniforms[13] = inputs[14].get();//Vignette + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/CanvasGetCheckboxNode.hx b/Sources/armory/logicnode/CanvasGetCheckboxNode.hx new file mode 100644 index 0000000000..5dabc261dc --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetCheckboxNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetCheckboxNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { // Null + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getHandle(inputs[0].get()).selected; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetInputTextNode.hx b/Sources/armory/logicnode/CanvasGetInputTextNode.hx new file mode 100644 index 0000000000..4163de593f --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetInputTextNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetInputTextNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int) { + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getHandle(inputs[0].get()).text; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetLocationNode.hx b/Sources/armory/logicnode/CanvasGetLocationNode.hx new file mode 100644 index 0000000000..bda11c2685 --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetLocationNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetLocationNode extends LogicNode { + + var x: Float; + var y: Float; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e == null) return; + + x = e.x; + y = e.y; + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + if (from == 1) return x; + else if (from == 2) return y; + else return 0; + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetPBNode.hx b/Sources/armory/logicnode/CanvasGetPBNode.hx new file mode 100644 index 0000000000..8b4bafb41d --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetPBNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetPBNode extends LogicNode { + + var at: Int; + var max: Int; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e == null) return; + + at = canvas.getElement(element).progress_at; + max = canvas.getElement(element).progress_total; + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + if (from == 1) return at; + else if (from == 2) return max; + else return 0; + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetPositionNode.hx b/Sources/armory/logicnode/CanvasGetPositionNode.hx new file mode 100644 index 0000000000..0f98660bbc --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetPositionNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetPositionNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { // Null + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getHandle(inputs[0].get()).position; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetRotationNode.hx b/Sources/armory/logicnode/CanvasGetRotationNode.hx new file mode 100644 index 0000000000..4e6d084724 --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetRotationNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetRotationNode extends LogicNode { + + var rad: Float; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e == null) return; + + rad = e.rotation; + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + if (from == 1) return rad; + else return 0; + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetScaleNode.hx b/Sources/armory/logicnode/CanvasGetScaleNode.hx new file mode 100644 index 0000000000..e4e49d322f --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetScaleNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetScaleNode extends LogicNode { + + var width: Int; + var height: Int; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e == null) return; + + height = e.height; + width = e.width; + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + if (from == 1) return height; + else if (from == 2) return width; + else return 0; + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetSliderNode.hx b/Sources/armory/logicnode/CanvasGetSliderNode.hx new file mode 100644 index 0000000000..660d9f40a0 --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetSliderNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetSliderNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { // Null + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getHandle(inputs[0].get()).value; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetTextNode.hx b/Sources/armory/logicnode/CanvasGetTextNode.hx new file mode 100644 index 0000000000..1827d81f7f --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetTextNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetTextNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int) { + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getElement(inputs[0].get()).text; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasGetVisibleNode.hx b/Sources/armory/logicnode/CanvasGetVisibleNode.hx new file mode 100644 index 0000000000..4f941191d1 --- /dev/null +++ b/Sources/armory/logicnode/CanvasGetVisibleNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasGetVisibleNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { // Null + var element: String = inputs[0].get(); + + if (canvas == null) canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if (canvas == null || !canvas.ready) return null; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + return canvas.getElement(element).visible; + } + catch (e: Dynamic) { return null; } + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetAssetNode.hx b/Sources/armory/logicnode/CanvasSetAssetNode.hx new file mode 100644 index 0000000000..fdab494502 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetAssetNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetAssetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var asset = Std.string(inputs[2].get()); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.asset = asset; + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx b/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx new file mode 100644 index 0000000000..eb129ae797 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetCheckBoxNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetCheckBoxNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + var value: Bool; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + function update() { + if (!canvas.ready) return; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + canvas.getHandle(element).selected = value; + tree.removeUpdate(update); + } + catch (e: Dynamic) {} + + runOutput(0); + } + + override function run(from: Int) { + element = inputs[1].get(); + value = inputs[2].get(); + + canvas = CanvasScript.getActiveCanvas(); + + // Ensure canvas is ready + tree.notifyOnUpdate(update); + update(); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetColorNode.hx b/Sources/armory/logicnode/CanvasSetColorNode.hx new file mode 100644 index 0000000000..4bb0305b35 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetColorNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import kha.Color; +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetColorNode extends LogicNode { + + public var property0: String; // Attribute + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var elementName = inputs[1].get(); + var color: iron.math.Vec4 = inputs[2].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var element = canvas.getElement(elementName); + var c = Color.fromFloats(color.x, color.y, color.z, color.w); + + if (element != null) { + switch (property0) { + case "color": element.color = c; + case "color_text": element.color_text = c; + case "color_hover": element.color_hover = c; + case "color_press": element.color_press = c; + case "color_progress": element.color_progress = c; + } + } + + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetInputTextFocusNode.hx b/Sources/armory/logicnode/CanvasSetInputTextFocusNode.hx new file mode 100644 index 0000000000..5d8cb719e5 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetInputTextFocusNode.hx @@ -0,0 +1,43 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetInputTextFocusNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + var focus: Bool; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + + function update() { + if (!canvas.ready) return; + tree.removeUpdate(update); + + var e = canvas.getHandle(element); + if (e == null) return; + + canvas.setCanvasInputTextFocus(e, focus); + + runOutput(0); + } + + override function run(from: Int) { + + element = inputs[1].get(); + focus = inputs[2].get(); + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + // Ensure canvas is ready + tree.notifyOnUpdate(update); + update(); + + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetInputTextNode.hx b/Sources/armory/logicnode/CanvasSetInputTextNode.hx new file mode 100644 index 0000000000..0ca3ef685a --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetInputTextNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetInputTextNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var text = Std.string(inputs[2].get()); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getHandle(element); + if (e != null) e.text = text; + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetLocationNode.hx b/Sources/armory/logicnode/CanvasSetLocationNode.hx new file mode 100644 index 0000000000..b5ddb8af80 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetLocationNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var newX = inputs[2].get(); + var newY = inputs[3].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) { + e.x = newX; + e.y = newY; + } + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetPBNode.hx b/Sources/armory/logicnode/CanvasSetPBNode.hx new file mode 100644 index 0000000000..7c84d2b12d --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetPBNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetPBNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var newAt = inputs[2].get(); + var newMax = inputs[3].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) { + e.progress_at = newAt; + e.progress_total = newMax; + } + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetProgressBarColorNode.hx b/Sources/armory/logicnode/CanvasSetProgressBarColorNode.hx new file mode 100644 index 0000000000..c78d53e292 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetProgressBarColorNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; +import kha.Color; +import iron.math.Vec4; + +@:deprecated("The 'Set Canvas Progress Bar Color' node is deprecated and will be removed in future SDK versions. Please use 'Set Canvas Color' instead.") +class CanvasSetProgressBarColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var color: Vec4 = inputs[2].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.color_progress = Color.fromFloats(color.x, color.y, color.z, color.w); + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetRotationNode.hx b/Sources/armory/logicnode/CanvasSetRotationNode.hx new file mode 100644 index 0000000000..cbed9de92b --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetRotationNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetRotationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var rad = inputs[2].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.rotation = rad; + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetScaleNode.hx b/Sources/armory/logicnode/CanvasSetScaleNode.hx new file mode 100644 index 0000000000..caefcc0f28 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetScaleNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var height = inputs[2].get(); + var width = inputs[3].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) { + e.height = height; + e.width = width; + } + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetSliderNode.hx b/Sources/armory/logicnode/CanvasSetSliderNode.hx new file mode 100644 index 0000000000..5d1fd21ead --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetSliderNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetSliderNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + var value: Float; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + function update() { + if (!canvas.ready) return; + + // This Try/Catch hacks around an issue where the handles are + // not created yet, even though canvas.ready is true. + try { + canvas.getHandle(element).value = value; + tree.removeUpdate(update); + } + catch (e: Dynamic) {} + + runOutput(0); + } + + override function run(from: Int) { + element = inputs[1].get(); + value = inputs[2].get(); + + canvas = CanvasScript.getActiveCanvas(); + + // Ensure canvas is ready + tree.notifyOnUpdate(update); + update(); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetTextColorNode.hx b/Sources/armory/logicnode/CanvasSetTextColorNode.hx new file mode 100644 index 0000000000..c3f34e529e --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetTextColorNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; +import kha.Color; + +@:deprecated("The 'Set Canvas Text Color' node is deprecated and will be removed in future SDK versions. Please use 'Set Canvas Color' instead.") +class CanvasSetTextColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var r = inputs[2].get(); + var g = inputs[3].get(); + var b = inputs[4].get(); + var a = inputs[5].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.color_text = Color.fromFloats(r, g, b, a); + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetTextNode.hx b/Sources/armory/logicnode/CanvasSetTextNode.hx new file mode 100644 index 0000000000..27e7f34ace --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetTextNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetTextNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var text = Std.string(inputs[2].get()); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.text = text; + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CanvasSetVisibleNode.hx b/Sources/armory/logicnode/CanvasSetVisibleNode.hx new file mode 100644 index 0000000000..d88488bce0 --- /dev/null +++ b/Sources/armory/logicnode/CanvasSetVisibleNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class CanvasSetVisibleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + var element = inputs[1].get(); + var visible = inputs[2].get(); + + var canvas = CanvasScript.getActiveCanvas(); + canvas.notifyOnReady(() -> { + var e = canvas.getElement(element); + if (e != null) e.visible = visible; + runOutput(0); + }); + } +#end +} diff --git a/Sources/armory/logicnode/CaseIndexNode.hx b/Sources/armory/logicnode/CaseIndexNode.hx new file mode 100644 index 0000000000..45b63a50e9 --- /dev/null +++ b/Sources/armory/logicnode/CaseIndexNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class CaseIndexNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Dynamic): Int { + var value = inputs[0].get(); + + for (index in 0...inputs.length - 1) { + if (Std.isOfType(value, Vec4) ? (value: Vec4).equals(inputs[index + 1].get()) : (value == inputs[index + 1].get())) { + return index; + } + } + return null; + } +} diff --git a/Sources/armory/logicnode/CaseStringNode.hx b/Sources/armory/logicnode/CaseStringNode.hx new file mode 100644 index 0000000000..a2b696c5e1 --- /dev/null +++ b/Sources/armory/logicnode/CaseStringNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +class CaseStringNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s: String = inputs[0].get(); + if (s == null) return null; + + switch (property0) { + case "Upper Case": + return s.toUpperCase(); + case "Lower Case": + return s.toLowerCase(); + } + + return false; + } +} diff --git a/Sources/armory/logicnode/CastPhysicsRayNode.hx b/Sources/armory/logicnode/CastPhysicsRayNode.hx new file mode 100644 index 0000000000..efc1086bdb --- /dev/null +++ b/Sources/armory/logicnode/CastPhysicsRayNode.hx @@ -0,0 +1,45 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class CastPhysicsRayNode extends LogicNode { + + var v = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vfrom: Vec4 = inputs[0].get(); + var vto: Vec4 = inputs[1].get(); + var mask: Int = inputs[2].get(); + + if (vfrom == null || vto == null) return null; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var hit = physics.rayCast(vfrom, vto, mask); + var rb = (hit != null) ? hit.rb : null; + + if (from == 0) { // Object + if (rb != null) return rb.object; + } + else if (from == 1) { // Hit + var hitPointWorld: Vec4 = rb != null ? physics.hitPointWorld : null; + if (hitPointWorld != null) { + v.set(hitPointWorld.x, hitPointWorld.y, hitPointWorld.z, 1); + return v; + } + } + else { // Normal + var hitNormalWorld: Vec4 = rb != null ? physics.hitNormalWorld : null; + if (hitNormalWorld != null) { + v.set(hitNormalWorld.x, hitNormalWorld.y, hitNormalWorld.z, 0); + return v; + } + } +#end + return null; + } +} diff --git a/Sources/armory/logicnode/CastPhysicsRayOnNode.hx b/Sources/armory/logicnode/CastPhysicsRayOnNode.hx new file mode 100644 index 0000000000..2c8341cfdc --- /dev/null +++ b/Sources/armory/logicnode/CastPhysicsRayOnNode.hx @@ -0,0 +1,56 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; + +class CastPhysicsRayOnNode extends LogicNode { + + var v = new Vec4(); + var hitRb: Object = null; + var hitPos: Vec4 = null; + var hitNormal: Vec4 = null; + + public function new(tree: LogicTree) { + super(tree); + } + + function reset() { + hitRb = null; + hitPos = null; + hitNormal = null; + } + + override function run(from:Int) { + reset(); + + var vfrom: Vec4 = inputs[1].get(); + var vto: Vec4 = inputs[2].get(); + var mask: Int = inputs[3].get(); + +#if arm_physics + if (vfrom != null && vto != null) { + var physics = armory.trait.physics.PhysicsWorld.active; + var hit = physics.rayCast(vfrom, vto, mask); + if(hit != null) { + hitRb = hit.rb.object; + hitPos = new Vec4().setFrom(physics.hitPointWorld); + hitNormal = new Vec4().setFrom(physics.hitNormalWorld); + } + } +#end + runOutput(0); + } + + override function get(from: Int): Dynamic { + if (from == 1) { // Object + return hitRb; + } + else if (from == 2) { // Hit + return hitPos; + } + else { // Normal + return hitNormal; + } + return null; + } +} diff --git a/Sources/armory/logicnode/ChromaticAberrationGetNode.hx b/Sources/armory/logicnode/ChromaticAberrationGetNode.hx new file mode 100644 index 0000000000..0fe43f8031 --- /dev/null +++ b/Sources/armory/logicnode/ChromaticAberrationGetNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class ChromaticAberrationGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.chromatic_aberration_uniforms[0]; + case 1: armory.renderpath.Postprocess.chromatic_aberration_uniforms[1]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/ChromaticAberrationSetNode.hx b/Sources/armory/logicnode/ChromaticAberrationSetNode.hx new file mode 100644 index 0000000000..224dd9e1b6 --- /dev/null +++ b/Sources/armory/logicnode/ChromaticAberrationSetNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class ChromaticAberrationSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + armory.renderpath.Postprocess.chromatic_aberration_uniforms[0] = inputs[1].get(); + armory.renderpath.Postprocess.chromatic_aberration_uniforms[1] = inputs[2].get(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ClampNode.hx b/Sources/armory/logicnode/ClampNode.hx new file mode 100644 index 0000000000..911cf9d4a4 --- /dev/null +++ b/Sources/armory/logicnode/ClampNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import kha.FastFloat; + +class ClampNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): FastFloat { + var value = inputs[0].get(); + var min = inputs[1].get(); + var max = inputs[2].get(); + + return value < min ? min : value > max ? max : value; + } +} diff --git a/Sources/armory/logicnode/ClearConsoleNode.hx b/Sources/armory/logicnode/ClearConsoleNode.hx new file mode 100644 index 0000000000..c667e6abc2 --- /dev/null +++ b/Sources/armory/logicnode/ClearConsoleNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class ClearConsoleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + #if kha_krom + Krom.sysCommand("cls"); + #elseif kha_html5 + js.Syntax.code("console.clear();"); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ClearMapNode.hx b/Sources/armory/logicnode/ClearMapNode.hx new file mode 100644 index 0000000000..b683125adb --- /dev/null +++ b/Sources/armory/logicnode/ClearMapNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + + +class ClearMapNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from: Int) { + var map: Map = inputs[1].get(); + if (map == null) return null; + + map.clear(); + runOutput(0); + return; + } + +} diff --git a/Sources/armory/logicnode/ClearParentNode.hx b/Sources/armory/logicnode/ClearParentNode.hx new file mode 100644 index 0000000000..04f6b89a3a --- /dev/null +++ b/Sources/armory/logicnode/ClearParentNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class ClearParentNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var keepTransform: Bool = inputs[2].get(); + + if (object == null || object.parent == null) return; + + object.setParent(iron.Scene.active.root, false, keepTransform); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ColorNode.hx b/Sources/armory/logicnode/ColorNode.hx new file mode 100644 index 0000000000..4d2b0c5245 --- /dev/null +++ b/Sources/armory/logicnode/ColorNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class ColorNode extends LogicNode { + + var value = new Vec4(); + + public function new(tree: LogicTree, r = 0.8, g = 0.8, b = 0.8, a = 1.0) { + super(tree); + + value.set(r, g, b, a); + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/ColorgradingGetGlobalNode.hx b/Sources/armory/logicnode/ColorgradingGetGlobalNode.hx new file mode 100644 index 0000000000..1e0a9183a7 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingGetGlobalNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class ColorgradingGetGlobalNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.colorgrading_global_uniforms[0][0]; + case 1: armory.renderpath.Postprocess.colorgrading_global_uniforms[1]; + case 2: armory.renderpath.Postprocess.colorgrading_global_uniforms[2]; + case 3: armory.renderpath.Postprocess.colorgrading_global_uniforms[3]; + case 4: armory.renderpath.Postprocess.colorgrading_global_uniforms[4]; + case 5: armory.renderpath.Postprocess.colorgrading_global_uniforms[5]; + case 6: armory.renderpath.Postprocess.colorgrading_global_uniforms[6]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/ColorgradingGetHighlightNode.hx b/Sources/armory/logicnode/ColorgradingGetHighlightNode.hx new file mode 100644 index 0000000000..23b1525cbb --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingGetHighlightNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class ColorgradingGetHighlightNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[0][2]; + case 1: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[0]; + case 2: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[1]; + case 3: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[2]; + case 4: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[3]; + case 5: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[4]; + case 6: armory.renderpath.Postprocess.colorgrading_highlight_uniforms[5]; + default: 0.0; + } + } + +} diff --git a/Sources/armory/logicnode/ColorgradingGetMidtoneNode.hx b/Sources/armory/logicnode/ColorgradingGetMidtoneNode.hx new file mode 100644 index 0000000000..d05497fdb6 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingGetMidtoneNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class ColorgradingGetMidtoneNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[0]; + case 1: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[1]; + case 2: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[2]; + case 3: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[3]; + case 4: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[4]; + case 5: armory.renderpath.Postprocess.colorgrading_midtone_uniforms[5]; + default: 0.0; + } + } + +} diff --git a/Sources/armory/logicnode/ColorgradingGetShadowNode.hx b/Sources/armory/logicnode/ColorgradingGetShadowNode.hx new file mode 100644 index 0000000000..6b5876b042 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingGetShadowNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class ColorgradingGetShadowNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[0][1]; + case 1: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[0]; + case 2: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[1]; + case 3: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[2]; + case 4: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[3]; + case 5: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[4]; + case 6: armory.renderpath.Postprocess.colorgrading_shadow_uniforms[5]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/ColorgradingSetGlobalNode.hx b/Sources/armory/logicnode/ColorgradingSetGlobalNode.hx new file mode 100644 index 0000000000..9325285c61 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingSetGlobalNode.hx @@ -0,0 +1,65 @@ +package armory.logicnode; + +class ColorgradingSetGlobalNode extends LogicNode { + + public var property0:Dynamic; + public var property1:Dynamic; + public var property2:Dynamic; + public var property3:Dynamic; + + var value:Dynamic; + var whitebalance:Dynamic; + var tint:Dynamic; + var saturation:Dynamic; + var contrast:Dynamic; + var gamma:Dynamic; + var gain:Dynamic; + var offset:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(property0 == "Uniform"){ + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][0] = inputs[1].get(); + armory.renderpath.Postprocess.colorgrading_global_uniforms[1][0] = inputs[2].get().x; + armory.renderpath.Postprocess.colorgrading_global_uniforms[1][1] = inputs[2].get().y; + armory.renderpath.Postprocess.colorgrading_global_uniforms[1][2] = inputs[2].get().z; + + for (i in 2...7){ + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][0] = inputs[i+1].get(); + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][1] = inputs[i+1].get(); + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][2] = inputs[i+1].get(); + } + + } else if (property0 == "RGB") { + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][0] = inputs[1].get(); + + for (i in 1...7){ + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][0] = inputs[i+1].get().x; + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][1] = inputs[i+1].get().y; + armory.renderpath.Postprocess.colorgrading_global_uniforms[i][2] = inputs[i+1].get().z; + } + + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + + } else if (property0 == "Preset File") { + return; + } else { + return; + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ColorgradingSetHighlightNode.hx b/Sources/armory/logicnode/ColorgradingSetHighlightNode.hx new file mode 100644 index 0000000000..5bb67f28c5 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingSetHighlightNode.hx @@ -0,0 +1,71 @@ +package armory.logicnode; + +class ColorgradingSetHighlightNode extends LogicNode { + + public var property0:Dynamic; + public var property1:Dynamic; + public var property2:Dynamic; + public var property3:Dynamic; + + var value:Dynamic; + var whitebalance:Dynamic; + var tint:Dynamic; + var saturation:Dynamic; + var contrast:Dynamic; + var gamma:Dynamic; + var gain:Dynamic; + var offset:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(property0 == "Uniform"){ + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][2] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][0] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][1] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][2] = inputs[i+2].get(); + } + + //trace(inputs[6].get()); + + } else if (property0 == "RGB") { + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][2] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][0] = inputs[i+2].get().x; + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][1] = inputs[i+2].get().y; + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[i][2] = inputs[i+2].get().z; + } + + } else if (property0 == "Preset File") { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } else { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } + + //trace(tint.x); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ColorgradingSetMidtoneNode.hx b/Sources/armory/logicnode/ColorgradingSetMidtoneNode.hx new file mode 100644 index 0000000000..56d4585dc2 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingSetMidtoneNode.hx @@ -0,0 +1,67 @@ +package armory.logicnode; + +class ColorgradingSetMidtoneNode extends LogicNode { + + public var property0:Dynamic; + public var property1:Dynamic; + public var property2:Dynamic; + public var property3:Dynamic; + + var value:Dynamic; + var whitebalance:Dynamic; + var tint:Dynamic; + var saturation:Dynamic; + var contrast:Dynamic; + var gamma:Dynamic; + var gain:Dynamic; + var offset:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(property0 == "Uniform"){ + + for (i in 0...4){ + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][0] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][1] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][2] = inputs[i+2].get(); + } + + } else if (property0 == "RGB") { + + armory.renderpath.Postprocess.colorgrading_highlight_uniforms[0][0] = inputs[1].get(); + + for (i in 0...4){ + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][0] = inputs[i+1].get().x; + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][1] = inputs[i+1].get().y; + armory.renderpath.Postprocess.colorgrading_midtone_uniforms[i][2] = inputs[i+1].get().z; + } + + } else if (property0 == "Preset File") { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } else { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } + + //trace(tint.x); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ColorgradingSetShadowNode.hx b/Sources/armory/logicnode/ColorgradingSetShadowNode.hx new file mode 100644 index 0000000000..075fa2b1ba --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingSetShadowNode.hx @@ -0,0 +1,71 @@ +package armory.logicnode; + +class ColorgradingSetShadowNode extends LogicNode { + + public var property0:Dynamic; + public var property1:Dynamic; + public var property2:Dynamic; + public var property3:Dynamic; + + var value:Dynamic; + var whitebalance:Dynamic; + var tint:Dynamic; + var saturation:Dynamic; + var contrast:Dynamic; + var gamma:Dynamic; + var gain:Dynamic; + var offset:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(property0 == "Uniform"){ + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][1] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][0] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][1] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][2] = inputs[i+2].get(); + } + + //trace(inputs[6].get()); + + } else if (property0 == "RGB") { + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][2] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][0] = inputs[i+2].get().x; + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][1] = inputs[i+2].get().y; + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][2] = inputs[i+2].get().z; + } + + } else if (property0 == "Preset File") { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } else { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } + + //trace(tint.x); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ColorgradingShadowNode.hx b/Sources/armory/logicnode/ColorgradingShadowNode.hx new file mode 100644 index 0000000000..e1dddb6721 --- /dev/null +++ b/Sources/armory/logicnode/ColorgradingShadowNode.hx @@ -0,0 +1,71 @@ +package armory.logicnode; + +class ColorgradingShadowNode extends LogicNode { + + public var property0:Dynamic; + public var property1:Dynamic; + public var property2:Dynamic; + public var property3:Dynamic; + + var value:Dynamic; + var whitebalance:Dynamic; + var tint:Dynamic; + var saturation:Dynamic; + var contrast:Dynamic; + var gamma:Dynamic; + var gain:Dynamic; + var offset:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(property0 == "Uniform"){ + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][1] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][0] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][1] = inputs[i+2].get(); + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][2] = inputs[i+2].get(); + } + + //trace(inputs[6].get()); + + } else if (property0 == "RGB") { + + armory.renderpath.Postprocess.colorgrading_global_uniforms[0][2] = inputs[1].get(); + + for (i in 0...5){ + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][0] = inputs[i+2].get().x; + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][1] = inputs[i+2].get().y; + armory.renderpath.Postprocess.colorgrading_shadow_uniforms[i][2] = inputs[i+2].get().z; + } + + } else if (property0 == "Preset File") { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } else { + var value:Dynamic = inputs[0].get(); + var whitebalance:Float = inputs[1].get(); + var tint:iron.math.Vec4 = inputs[2].get(); + var saturation:Float = inputs[3].get(); + var contrast:Float = inputs[4].get(); + var gamma:Float = inputs[5].get(); + var gain:Float = inputs[6].get(); + var offset:Float = inputs[7].get(); + } + + //trace(tint.x); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/CombineColorHSVNode.hx b/Sources/armory/logicnode/CombineColorHSVNode.hx new file mode 100644 index 0000000000..58c2c4e34d --- /dev/null +++ b/Sources/armory/logicnode/CombineColorHSVNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class CombineColorHSVNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var h = inputs[0].get(); + var s = inputs[1].get(); + var v = inputs[2].get(); + var a = inputs[3].get(); + + var r = 0.0; var g = 0.0; var b = 0.0; + + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: { r = v; g = t; b = p; } + case 1: { r = q; g = v; b = p; } + case 2: { r = p; g = v; b = t; } + case 3: { r = p; g = q; b = v; } + case 4: { r = t; g = p; b = v; } + case 5: { r = v; g = p; b = q; } + } + + return new Vec4(r, g, b, a); + } +} diff --git a/Sources/armory/logicnode/CombineColorNode.hx b/Sources/armory/logicnode/CombineColorNode.hx new file mode 100644 index 0000000000..5efb9a6de6 --- /dev/null +++ b/Sources/armory/logicnode/CombineColorNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class CombineColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var r = inputs[0].get(); + var g = inputs[1].get(); + var b = inputs[2].get(); + var a = inputs[3].get(); + + return new Vec4( + r == null ? 0.0 : r, + g == null ? 0.0 : g, + b == null ? 0.0 : b, + a == null ? 0.0 : a + ); + } +} diff --git a/Sources/armory/logicnode/CompareNode.hx b/Sources/armory/logicnode/CompareNode.hx new file mode 100644 index 0000000000..abd92bd64b --- /dev/null +++ b/Sources/armory/logicnode/CompareNode.hx @@ -0,0 +1,57 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class CompareNode extends LogicNode { + + public var property0: String; + public var property1: Float; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Bool { + + var v1: Dynamic = inputs[0].get(); + var v2: Dynamic = inputs[1].get(); + var cond = false; + + switch (property0) { + case "Equal": + cond = Std.isOfType(v1, Vec4) ? (v1: Vec4).equals(v2) : v1 == v2; + case "Not Equal": + cond = Std.isOfType(v1, Vec4) ? !(v1: Vec4).equals(v2) : v1 != v2; + case "Almost Equal": + cond = Std.isOfType(v1, Vec4) ? (v1: Vec4).almostEquals(v2, property1) : Math.abs(v1 - v2) < property1; + case "Greater": + cond = v1 > v2; + case "Greater Equal": + cond = v1 >= v2; + case "Less": + cond = v1 < v2; + case "Less Equal": + cond = v1 <= v2; + case "Between": + var v3: Dynamic = inputs[2].get(); + cond = Std.isOfType(v1, Vec4) ? v2.x <= v1.x && v2.y <= v1.y && v2.z <= v1.z && v1.x <= v3.x && v1.y <= v3.y && v1.z <= v3.z : v2 <= v1 && v1 <= v3; + case "Or": + for (input in inputs) { + if (input.get()) { + cond = true; + break; + } + } + case "And": + cond = true; + for (input in inputs) { + if (!input.get()) { + cond = false; + break; + } + } + } + + return cond; + } +} diff --git a/Sources/armory/logicnode/ConcatenateStringNode.hx b/Sources/armory/logicnode/ConcatenateStringNode.hx new file mode 100644 index 0000000000..8153fe64b3 --- /dev/null +++ b/Sources/armory/logicnode/ConcatenateStringNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class ConcatenateStringNode extends LogicNode { + + public var value: String; + + public function new(tree: LogicTree, value = "") { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + value = ""; + for (inp in inputs) value += inp.get(); + return value; + } +} diff --git a/Sources/armory/logicnode/ContainsStringNode.hx b/Sources/armory/logicnode/ContainsStringNode.hx new file mode 100644 index 0000000000..6bc19e5ee0 --- /dev/null +++ b/Sources/armory/logicnode/ContainsStringNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class ContainsStringNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s1: String = inputs[0].get(); + var s2: String = inputs[1].get(); + if (s1 == null || s2 == null) return null; + + switch (property0) { + case "Contains": + return s1.indexOf(s2) >= 0; + case "Starts With": + return StringTools.startsWith(s1, s2); + case "Ends With": + return StringTools.endsWith(s1, s2); + } + + return false; + } +} diff --git a/Sources/armory/logicnode/CreateMapNode.hx b/Sources/armory/logicnode/CreateMapNode.hx new file mode 100644 index 0000000000..f06fc7b35b --- /dev/null +++ b/Sources/armory/logicnode/CreateMapNode.hx @@ -0,0 +1,114 @@ +package armory.logicnode; +import iron.object.Object; +import iron.math.Vec4; +import armory.system.Event; + + +class CreateMapNode extends LogicNode { + public var property0: String; + public var property1: String; + public var map: Map; + + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + if(property0 == "string"){ + switch (property1) { + case "string": + map = new Map(); + runOutput(0); + case "vector": + map = new Map(); + runOutput(0); + case "float": + map = new Map(); + runOutput(0); + case "integer": + map = new Map(); + runOutput(0); + case "boolean": + map = new Map(); + runOutput(0); + case "dynamic": + map = new Map(); + runOutput(0); + default: throw "Failed to create Map"; + } + } else if (property0 == "int"){ + switch (property1) { + case "string": + map = new Map(); + runOutput(0); + case "vector": + map = new Map(); + runOutput(0); + case "float": + map = new Map(); + runOutput(0); + case "integer": + map = new Map(); + runOutput(0); + case "boolean": + map = new Map(); + runOutput(0); + case "dynamic": + map = new Map(); + runOutput(0); + default: throw "Failed to create Map"; + } + } else if (property0 == "enumvalue"){ + switch (property1) { + case "string": + map = new Map(); + runOutput(0); + case "vector": + map = new Map(); + runOutput(0); + case "float": + map = new Map(); + runOutput(0); + case "integer": + map = new Map(); + runOutput(0); + case "boolean": + map = new Map(); + runOutput(0); + case "dynamic": + map = new Map(); + runOutput(0); + default: throw "Failed to create Map"; + } + } else if (property0 == "object"){ + switch (property1) { + case "string": + map = new Map<{},String>(); + runOutput(0); + case "vector": + map = new Map<{},Vec4>(); + runOutput(0); + case "float": + map = new Map<{},Float>(); + runOutput(0); + case "integer": + map = new Map<{},Int>(); + runOutput(0); + case "boolean": + map = new Map<{},Bool>(); + runOutput(0); + case "dynamic": + map = new Map<{},Dynamic>(); + runOutput(0); + default: throw "Failed to create Map"; + } + } + } + + override function get(from: Int): Map { + return map; + } + +} + diff --git a/Sources/armory/logicnode/CreateRenderTargetNode.hx b/Sources/armory/logicnode/CreateRenderTargetNode.hx new file mode 100644 index 0000000000..198a0b720f --- /dev/null +++ b/Sources/armory/logicnode/CreateRenderTargetNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import kha.Image; +import kha.graphics4.TextureFormat; +import iron.Scene; +import iron.data.MaterialData; +import iron.object.Object; +import armory.trait.internal.UniformsManager; + +class CreateRenderTargetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + + } + + override function run(from: Int) { + 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.removeTextureValue(object, mat, inputs[4].get()); + object = Scene.active.root; + } + + var img = Image.createRenderTarget(inputs[5].get(), inputs[6].get(), TextureFormat.RGBA32); + UniformsManager.setTextureValue(mat, object, inputs[4].get(), img); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/CursorInRegionNode.hx b/Sources/armory/logicnode/CursorInRegionNode.hx new file mode 100644 index 0000000000..23dfd58939 --- /dev/null +++ b/Sources/armory/logicnode/CursorInRegionNode.hx @@ -0,0 +1,115 @@ +package armory.logicnode; + +import iron.math.Vec2; + +class CursorInRegionNode extends LogicNode { + + public var property0: String; + + var lastInside: Null = null; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var mouse = iron.system.Input.getMouse(); + final center = new Vec2(inputs[0].get(), inputs[1].get()); + final size = new Vec2(inputs[2].get(), inputs[3].get()); + final angle: Float = inputs[4].get(); + final lastPoint = new Vec2(mouse.lastX, mouse.lastY); + final point = new Vec2(mouse.x, mouse.y); + + switch (property0) { + case "rectangle": + if(lastInside == null) { + lastInside = detectPointInRectangle(center, size, angle, lastPoint); + } + final inside = detectPointInRectangle(center, size, angle, point); + + //On Enter + if(!lastInside && inside) runOutput(0); + + //On Exit + if(lastInside && !inside) runOutput(1); + + lastInside = inside; + + case "ellipse": + if(lastInside == null) { + lastInside = detectPointInEllipse(center, size, angle, lastPoint); + } + final inside = detectPointInEllipse(center, size, angle, point); + + //On Enter + if(!lastInside && inside) runOutput(0); + + //On Exit + if(lastInside && !inside) runOutput(1); + + lastInside = inside; + } + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + if(from == 2) { + final center = new Vec2(inputs[0].get(), inputs[1].get()); + final size = new Vec2(inputs[2].get(), inputs[3].get()); + final angle = inputs[4].get(); + final point = new Vec2(mouse.x, mouse.y); + + switch (property0) { + case "rectangle": + return detectPointInRectangle(center, size, angle, point); + case "ellipse": + return detectPointInEllipse(center, size, angle, point); + } + } + + return null; + } + + //Rotate cursor location and calculate relative location to shape center + inline function alignRotatePoint(point: Vec2, center: Vec2, angle: Float): Vec2 { + var relativePoint = point.clone(); + relativePoint.sub(center); + + final sin = Math.sin(angle); + final cos = Math.cos(angle); + + final xnew = relativePoint.x * cos - relativePoint.y * sin; + final ynew = relativePoint.x * sin + relativePoint.y * cos; + + relativePoint.x = xnew; + relativePoint.y = ynew; + + return relativePoint; + } + + function detectPointInRectangle(center: Vec2, size: Vec2, angle: Float, point: Vec2): Bool { + final relativePoint = alignRotatePoint(point, center, -angle); + final magX = Math.abs(relativePoint.x); + final magY = Math.abs(relativePoint.y); + if(magX <= size.x/2 && magY <= size.y/2){ + return true; + } + + return false; + } + + function detectPointInEllipse(center: Vec2, size: Vec2, angle: Float, point: Vec2): Bool { + final relativePoint = alignRotatePoint(point, center, -angle); + final magX = (relativePoint.x * relativePoint.x) / (0.25 * size.x * size.x); + final magY = (relativePoint.y * relativePoint.y) / (0.25 * size.y * size.y); + if(magX + magY <= 1.0){ + return true; + } + + return false; + } +} + diff --git a/Sources/armory/logicnode/DefaultIfNullNode.hx b/Sources/armory/logicnode/DefaultIfNullNode.hx new file mode 100644 index 0000000000..1cd8cf286d --- /dev/null +++ b/Sources/armory/logicnode/DefaultIfNullNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class DefaultIfNullNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var v1 = inputs[0].get(); + var v2 = inputs[1].get(); + + v1 != null ? return v1 : return v2; + } + +} diff --git a/Sources/armory/logicnode/DegToRadNode.hx b/Sources/armory/logicnode/DegToRadNode.hx new file mode 100644 index 0000000000..a6c107c7e0 --- /dev/null +++ b/Sources/armory/logicnode/DegToRadNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class DegToRadNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var deg: Float = inputs[0].get(); + return deg * 0.0174532924; + } +} diff --git a/Sources/armory/logicnode/DetectMobileBrowserNode.hx b/Sources/armory/logicnode/DetectMobileBrowserNode.hx new file mode 100644 index 0000000000..68d9185c87 --- /dev/null +++ b/Sources/armory/logicnode/DetectMobileBrowserNode.hx @@ -0,0 +1,23 @@ +// This node does not work with Krom. "Browser compilation only" node. + +package armory.logicnode; + +import kha.System; + +class DetectMobileBrowserNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) { + #if kha_html5 + return kha.SystemImpl.mobile; + #else + return false; + #end + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/DisplayInfoNode.hx b/Sources/armory/logicnode/DisplayInfoNode.hx new file mode 100644 index 0000000000..264ce43662 --- /dev/null +++ b/Sources/armory/logicnode/DisplayInfoNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class DisplayInfoNode extends LogicNode { + + static inline var displayIndex = 0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) return kha.Display.all[displayIndex].width; + else return kha.Display.all[displayIndex].height; + } +} diff --git a/Sources/armory/logicnode/DrawArcNode.hx b/Sources/armory/logicnode/DrawArcNode.hx new file mode 100644 index 0000000000..9ac22b79bd --- /dev/null +++ b/Sources/armory/logicnode/DrawArcNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; + +using zui.GraphicsExtension; + +class DrawArcNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawArcNode"); + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + final segments = inputs[4].get(); + final cx = inputs[5].get(); + final cy = inputs[6].get(); + final radius = inputs[7].get(); + final sAngle = inputs[8].get(); + final eAngle = inputs[9].get(); + final ccw = inputs[10].get(); + + if (inputs[2].get()) { + RenderToTexture.g.fillArc(cx, cy, radius, sAngle, eAngle, ccw, segments); + } else { + RenderToTexture.g.drawArc(cx, cy, radius, sAngle, eAngle, inputs[3].get(), ccw, segments); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawCameraNode.hx b/Sources/armory/logicnode/DrawCameraNode.hx new file mode 100644 index 0000000000..628f14fdd8 --- /dev/null +++ b/Sources/armory/logicnode/DrawCameraNode.hx @@ -0,0 +1,104 @@ +package armory.logicnode; + +import iron.RenderPath; +import iron.Scene; +import iron.math.Vec2; +import iron.object.CameraObject; + +import armory.renderpath.RenderPathCreator; + +class DrawCameraNode extends LogicNode { + static inline var numStaticInputs = 2; + + var cameras: Array; + var renderTargets: Array; + var positions: Array; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + switch (from) { + case 0: // Start + if (cameras == null) { + final numDynamicInputs = inputs.length - numStaticInputs; + final numCams = Std.int(numDynamicInputs / 5); + + // Preallocate + cameras = []; + cameras.resize(numCams); + + positions = []; + positions.resize(numCams); + for (i in 0...positions.length) { + positions[i] = new Vec2(); + } + + renderTargets = []; + renderTargets.resize(numCams); + } + + for (i in 0...cameras.length) { + cameras[i] = inputs[numStaticInputs + i * 5].get(); + positions[i].set( + inputs[numStaticInputs + i * 5 + 1].get(), + inputs[numStaticInputs + i * 5 + 2].get() + ); + + // TODO: implement proper rendertarget cache/pool + renderTargets[i] = kha.Image.createRenderTarget( + inputs[numStaticInputs + i * 5 + 3].get(), // w + inputs[numStaticInputs + i * 5 + 4].get(), // h + kha.graphics4.TextureFormat.RGBA32, + kha.graphics4.DepthStencilFormat.NoDepthAndStencil + ); + } + + tree.notifyOnRender(render); + tree.notifyOnRender2D(render2D); + runOutput(0); + + case 1: // Stop + tree.removeRender(render); + tree.removeRender2D(render2D); + runOutput(1); + } + } + + function render(g:kha.graphics4.Graphics) { + final rpPaused = RenderPath.active.paused; + RenderPath.active.paused = false; + + final sceneCam = iron.Scene.active.camera; + + for (i in 0...cameras.length) { + final cam = cameras[i]; + + final oldRT = cam.renderTarget; + cam.renderTarget = renderTargets[i]; + + iron.Scene.active.camera = cam; + cam.renderFrame(g); + + cam.renderTarget = oldRT; + } + + iron.Scene.active.camera = sceneCam; + RenderPath.active.paused = rpPaused; + } + + function render2D(g: kha.graphics2.Graphics) { + for(i in 0...cameras.length) { + final rt = renderTargets[i]; + + final posX = positions[i].x; + final posY = positions[i].y; + + g.color = 0xff000000; + g.fillRect(posX, posY, rt.width, rt.height); + g.color = 0xffffffff; + g.drawScaledImage(rt, posX, posY, rt.width, rt.height); + } + } +} diff --git a/Sources/armory/logicnode/DrawCameraTextureNode.hx b/Sources/armory/logicnode/DrawCameraTextureNode.hx new file mode 100644 index 0000000000..1a0473fbdf --- /dev/null +++ b/Sources/armory/logicnode/DrawCameraTextureNode.hx @@ -0,0 +1,56 @@ +package armory.logicnode; + +import iron.Scene; +import iron.object.CameraObject; + +import armory.renderpath.RenderPathCreator; + +class DrawCameraTextureNode extends LogicNode { + + var cam: CameraObject; + var rt: kha.Image; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + switch (from) { + case 0: // Start + final o = inputs[3].get(); + assert(Error, Std.isOfType(o, iron.object.MeshObject), "Object must be a mesh object!"); + final mo = cast(o, iron.object.MeshObject); + final matSlot = inputs[4].get(); + assert(Error, matSlot < o.materials.length, 'Object "${mo.name}" does not have a material slot with index $matSlot!'); + assert(Error, matSlot >= 0, 'Material slot must not be negative. Current value: $matSlot.'); + + final c = inputs[2].get(); + assert(Error, Std.isOfType(c, CameraObject), "Camera must be a camera object!"); + cam = cast(c, CameraObject); + rt = kha.Image.createRenderTarget(iron.App.w(), iron.App.h()); + + assert(Error, mo.materials[matSlot].contexts[0].textures != null, 'Object "${mo.name}" has no diffuse texture to render to'); + mo.materials[matSlot].contexts[0].textures[0] = rt; // Override diffuse texture + + tree.notifyOnRender(render); + runOutput(0); + + case 1: // Stop + tree.removeRender(render); + runOutput(1); + } + } + + function render(g: kha.graphics4.Graphics) { + final sceneCam = iron.Scene.active.camera; + final oldRT = cam.renderTarget; + + iron.Scene.active.camera = cam; + cam.renderTarget = rt; + + cam.renderFrame(g); + + cam.renderTarget = oldRT; + iron.Scene.active.camera = sceneCam; + } +} diff --git a/Sources/armory/logicnode/DrawCircleNode.hx b/Sources/armory/logicnode/DrawCircleNode.hx new file mode 100644 index 0000000000..822efd4caa --- /dev/null +++ b/Sources/armory/logicnode/DrawCircleNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; + +using zui.GraphicsExtension; + +class DrawCircleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawCircleNode"); + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + final segments = inputs[4].get(); + final cx = inputs[5].get(); + final cy = inputs[6].get(); + final radius = inputs[7].get(); + + if (inputs[2].get()) { + RenderToTexture.g.fillCircle(cx, cy, radius, segments); + } + else { + RenderToTexture.g.drawCircle(cx, cy, radius, inputs[3].get(), segments); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawCurveNode.hx b/Sources/armory/logicnode/DrawCurveNode.hx new file mode 100644 index 0000000000..213bfd1914 --- /dev/null +++ b/Sources/armory/logicnode/DrawCurveNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; + +using zui.GraphicsExtension; + +class DrawCurveNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawCurveNode"); + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + RenderToTexture.g.drawCubicBezier( + [inputs[4].get(), inputs[6].get(), inputs[8].get(), inputs[10].get()], + [inputs[5].get(), inputs[7].get(), inputs[9].get(), inputs[11].get()], + inputs[3].get(), inputs[2].get() + ); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawEllipseNode.hx b/Sources/armory/logicnode/DrawEllipseNode.hx new file mode 100644 index 0000000000..85adf67e23 --- /dev/null +++ b/Sources/armory/logicnode/DrawEllipseNode.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +import iron.math.Vec4; +import kha.Color; +import armory.renderpath.RenderToTexture; + +using zui.GraphicsExtension; + +class DrawEllipseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawEllipseNode"); + + final colorVec: Vec4 = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + final filled: Bool = inputs[2].get(); + final strength: Float = inputs[3].get(); + final segments: Int = inputs[4].get(); + final cx: Float = inputs[5].get(); + final cy: Float = inputs[6].get(); + final width: Float = inputs[7].get(); + final height: Float = inputs[8].get(); + final angle: Float = inputs[9].get(); + final scale = height / width; + final scaleInv = width / height; + + RenderToTexture.g.scale(1.0, scale); + RenderToTexture.g.rotate(angle, cx, cy); + + if (filled) { + RenderToTexture.g.fillCircle(cx, cy * scaleInv, 0.5 * width, segments); + } + else { + RenderToTexture.g.drawCircle(cx, cy * scaleInv, 0.5 * width, strength, segments); + } + + RenderToTexture.g.rotate(-angle, cx, cy); + RenderToTexture.g.scale(1.0, scaleInv); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawImageNode.hx b/Sources/armory/logicnode/DrawImageNode.hx new file mode 100644 index 0000000000..5bf5b53fce --- /dev/null +++ b/Sources/armory/logicnode/DrawImageNode.hx @@ -0,0 +1,53 @@ +package armory.logicnode; + +import iron.math.Vec4; +import kha.Image; +import kha.Color; +import armory.renderpath.RenderToTexture; + +class DrawImageNode extends LogicNode { + var img: Image; + var lastImgName = ""; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawImageNode"); + + final imgName: String = inputs[1].get(); + final colorVec: Vec4 = inputs[2].get(); + final anchorH: Int = inputs[3].get(); + final anchorV: Int = inputs[4].get(); + final x: Float = inputs[5].get(); + final y: Float = inputs[6].get(); + final width: Float = inputs[7].get(); + final height: Float = inputs[8].get(); + final angle: Float = inputs[9].get(); + + final drawx = x - 0.5 * width * anchorH; + final drawy = y - 0.5 * height * anchorV; + + RenderToTexture.g.rotate(angle, x, y); + + if (imgName != lastImgName) { + // Load new image + lastImgName = imgName; + iron.data.Data.getImage(imgName, (image: Image) -> { + img = image; + }); + } + + if (img == null) { + runOutput(0); + return; + } + + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + RenderToTexture.g.drawScaledImage(img, drawx, drawy, width, height); + RenderToTexture.g.rotate(-angle, x, y); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawImageSequenceNode.hx b/Sources/armory/logicnode/DrawImageSequenceNode.hx new file mode 100644 index 0000000000..265ce25863 --- /dev/null +++ b/Sources/armory/logicnode/DrawImageSequenceNode.hx @@ -0,0 +1,106 @@ +package armory.logicnode; + +import kha.Color; +import kha.Image; +import kha.Scheduler; + +class DrawImageSequenceNode extends LogicNode { + + var images: Array = []; + var currentImgIdx = 0; + var timetaskID = -1; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + switch (from) { + case 0: // Start + if (timetaskID != -1) { + // Do nothing if already running + return; + } else { + // We could still be rendering the last image, reset in this case + tree.removeRender2D(render2D); + currentImgIdx = 0; + } + + final startIndex = inputs[9].get(); + final endIndex = inputs[10].get(); + assert(Error, startIndex >= 0, "Start Index must not be negative!"); + assert(Error, endIndex >= 0, "End Index must not be negative!"); + assert(Error, startIndex <= endIndex, "Start Index must not be larger than End Index!"); + + final numImages = endIndex + 1 - startIndex; + images.resize(numImages); + + final imagePrefix = inputs[2].get(); + final imageExtension = inputs[3].get(); + + final waitForLoad = inputs[13].get(); + var numLoaded = 0; + for (i in startIndex...endIndex + 1) { + iron.data.Data.getImage(imagePrefix + i + '.' + imageExtension, (image: Image) -> { + images[i - startIndex] = image; + numLoaded++; + + if (waitForLoad && numLoaded == numImages) { + startTimetask(); + runOutput(0); + } + }); + } + + if (!waitForLoad) { + startTimetask(); + runOutput(0); + } + + tree.notifyOnRender2D(render2D); + + case 1: // Stop + tree.removeRender2D(render2D); + currentImgIdx = 0; + stopTimetask(); + runOutput(1); + } + } + + inline function startTimetask() { + final rate = inputs[11].get(); + timetaskID = Scheduler.addTimeTask(() -> { + currentImgIdx++; + if (currentImgIdx == images.length) { + final loop = inputs[12].get(); + if (loop) { + currentImgIdx = 0; + } else { + currentImgIdx--; + stopTimetask(); + runOutput(1); + } + } + }, rate, rate); // Rate is also first offset so that we start at array index 0 + } + + inline function stopTimetask() { + if (timetaskID != -1) { + Scheduler.removeTimeTask(timetaskID); + timetaskID = -1; + } + } + + function render2D(g:kha.graphics2.Graphics) { + if (images[currentImgIdx] == null) { + return; + } + + final colorVec = inputs[4].get(); + g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + trace(currentImgIdx); + + g.drawScaledImage(images[currentImgIdx], inputs[5].get(), inputs[6].get(), inputs[7].get(), inputs[8].get()); + } +} diff --git a/Sources/armory/logicnode/DrawLineNode.hx b/Sources/armory/logicnode/DrawLineNode.hx new file mode 100644 index 0000000000..d95b880873 --- /dev/null +++ b/Sources/armory/logicnode/DrawLineNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; + +class DrawLineNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawLineNode"); + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + RenderToTexture.g.drawLine(inputs[3].get(), inputs[4].get(), inputs[5].get(), inputs[6].get(), inputs[2].get()); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawPolygonNode.hx b/Sources/armory/logicnode/DrawPolygonNode.hx new file mode 100644 index 0000000000..da2a3cf387 --- /dev/null +++ b/Sources/armory/logicnode/DrawPolygonNode.hx @@ -0,0 +1,49 @@ +package armory.logicnode; + +import kha.Color; +import kha.math.Vector2; +import armory.renderpath.RenderToTexture; + +using zui.GraphicsExtension; + +class DrawPolygonNode extends LogicNode { + static inline var numStaticInputs = 6; + + var vertices: Array; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawPolygonNode"); + + if (vertices == null) { + final numDynamicInputs = inputs.length - numStaticInputs; + final numPoints = numDynamicInputs >>> 1; + + // Preallocate + vertices = []; + vertices.resize(numPoints); + for (i in 0...vertices.length) { + vertices[i] = new Vector2(); + } + } + + for (i in 0...vertices.length) { + vertices[i].x = inputs[numStaticInputs + i * 2 + 0].get(); + vertices[i].y = inputs[numStaticInputs + i * 2 + 1].get(); + } + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + if (inputs[2].get()) { + RenderToTexture.g.fillPolygon(inputs[4].get(), inputs[5].get(), vertices); + } else { + RenderToTexture.g.drawPolygon(inputs[4].get(), inputs[5].get(), vertices, inputs[3].get()); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawRectNode.hx b/Sources/armory/logicnode/DrawRectNode.hx new file mode 100644 index 0000000000..b9f2c4b920 --- /dev/null +++ b/Sources/armory/logicnode/DrawRectNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.math.Vec4; +import kha.Color; +import armory.renderpath.RenderToTexture; + +class DrawRectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawRectNode"); + + final colorVec: Vec4 = inputs[1].get(); + final filled: Bool = inputs[2].get(); + final strength: Float = inputs[3].get(); + final anchorH: Int = inputs[4].get(); + final anchorV: Int = inputs[5].get(); + final x: Float = inputs[6].get(); + final y: Float = inputs[7].get(); + final width: Float = inputs[8].get(); + final height: Float = inputs[9].get(); + final angle: Float = inputs[10].get(); + + final drawx = x - 0.5 * width * anchorH; + final drawy = y - 0.5 * height * anchorV; + + RenderToTexture.g.rotate(angle, x, y); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + if (filled) { + RenderToTexture.g.fillRect(drawx, drawy, width, height); + } else { + RenderToTexture.g.drawRect(drawx, drawy, width, height, strength); + } + + RenderToTexture.g.rotate(-angle, x, y); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawStringNode.hx b/Sources/armory/logicnode/DrawStringNode.hx new file mode 100644 index 0000000000..e381f7817d --- /dev/null +++ b/Sources/armory/logicnode/DrawStringNode.hx @@ -0,0 +1,55 @@ +package armory.logicnode; + +import kha.Font; +import kha.Color; +import armory.renderpath.RenderToTexture; + +#if arm_ui +import armory.ui.Canvas; +#end + +class DrawStringNode extends LogicNode { + var font: Font; + var lastFontName = ""; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawStringNode"); + + var string:String = Std.string(inputs[1].get()); + var fontName = inputs[2].get(); + if (fontName == "") { + #if arm_ui + fontName = Canvas.defaultFontName; + #else + return; // No default font is exported, there is nothing we can do here + #end + } + + if (fontName != lastFontName) { + // Load new font + lastFontName = fontName; + iron.data.Data.getFont(fontName, (f: Font) -> { + font = f; + }); + } + + if (font == null) { + runOutput(0); + return; + } + + final colorVec = inputs[4].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + RenderToTexture.g.fontSize = inputs[3].get(); + RenderToTexture.g.font = font; + + RenderToTexture.g.drawString(string, inputs[5].get(), inputs[6].get()); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawTextAreaStringNode.hx b/Sources/armory/logicnode/DrawTextAreaStringNode.hx new file mode 100644 index 0000000000..4a524df5e3 --- /dev/null +++ b/Sources/armory/logicnode/DrawTextAreaStringNode.hx @@ -0,0 +1,130 @@ +package armory.logicnode; + +import kha.Font; +import kha.Color; +import armory.renderpath.RenderToTexture; + +import kha.graphics2.VerTextAlignment; +import kha.graphics2.HorTextAlignment; + +using zui.GraphicsExtension; + +#if arm_ui +import armory.ui.Canvas; +#end + +class DrawTextAreaStringNode extends LogicNode { + var font: Font; + var lastFontName = ""; + + public var property0: String; + public var property1: String; + public var property2: String; + + var index: Int; + var max: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawTextAreaStringNode"); + + var string:String = Std.string(inputs[1].get()); + var length:Int = inputs[3].get(); + + var horA = TextLeft; + var verA = TextTop; + + + var fontName = inputs[2].get(); + if (fontName == "") { + #if arm_ui + fontName = Canvas.defaultFontName; + #else + return; // No default font is exported, there is nothing we can do here + #end + } + + if (fontName != lastFontName) { + // Load new font + lastFontName = fontName; + iron.data.Data.getFont(fontName, (f: Font) -> { + font = f; + }); + } + + if (font == null) { + runOutput(0); + return; + } + + var len = string.length; + + var ar_lines = []; + + var ar_words = string.split(' '); + + if(property0 == 'Lines') + max = Std.int(len/length); + else max = length; + + while(ar_words.length > 0){ + var str_line = ''; + + while(str_line.length <= max && ar_words.length > 0){ + str_line +=' '+ar_words.shift(); + } + + ar_lines.push(str_line); + + } + + var spacing = inputs[4].get(); + + final colorVec = inputs[6].get(); + final colorVecB = inputs[7].get(); + + RenderToTexture.g.fontSize = inputs[5].get(); + RenderToTexture.g.font = font; + + index = 0; + + var height = RenderToTexture.g.font.height(RenderToTexture.g.fontSize); + + var yoffset = 0.0; + + switch(property2){ + case 'TextTop': verA = TextTop; + case 'TextMiddle': { verA = TextMiddle; yoffset = -height * 0.5; } + case 'TextBottom': { verA = TextBottom; yoffset = -height; } + } + + + for (line in ar_lines){ + + var width = RenderToTexture.g.font.width(RenderToTexture.g.fontSize, line); + + var xoffset = 0.0; + + switch(property1){ + case 'TextLeft': horA = TextLeft; + case 'TextCenter': {horA = TextCenter; xoffset = -width * 0.5; } + case 'TextRight': {horA = TextRight; xoffset = -width; } + } + + RenderToTexture.g.color = Color.fromFloats(colorVecB.x, colorVecB.y, colorVecB.z, colorVecB.w); + + RenderToTexture.g.fillRect(inputs[8].get()+xoffset, inputs[9].get()+yoffset+index*height*spacing, width, height); + + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + RenderToTexture.g.drawAlignedString(line, inputs[8].get(), inputs[9].get()+index*height*spacing, horA, verA); + ++index; + + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DrawToMaterialImageNode.hx b/Sources/armory/logicnode/DrawToMaterialImageNode.hx new file mode 100644 index 0000000000..21114f663d --- /dev/null +++ b/Sources/armory/logicnode/DrawToMaterialImageNode.hx @@ -0,0 +1,43 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; +import armory.trait.internal.UniformsManager; + +class DrawToMaterialImageNode extends LogicNode { + + var img: kha.Image = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object = inputs[1].get(); + var mat = inputs[2].get(); + var node = inputs[3].get(); + + img = UniformsManager.textureLink(object, mat, inputs[3].get()); + + assert(Error, img != null, 'Image $node does not exist or is empty'); + + assert(Error, img.depth != null, 'Image is not a render target. Use Create Render Target Node to create an image render target'); + + RenderToTexture.ensureEmptyRenderTarget("DrawToMaterialImageNode"); + img.g2.begin(inputs[4].get(), Color.Transparent); + RenderToTexture.g = img.g2; + runOutput(0); + RenderToTexture.g = null; + img.g2.end(); + } + + override function get(from: Int): Dynamic { + if(img == null) return null; + + switch(from){ + case 1: return img.width; + case 2: return img.height; + default: return null; + } + } +} diff --git a/Sources/armory/logicnode/DrawTriangleNode.hx b/Sources/armory/logicnode/DrawTriangleNode.hx new file mode 100644 index 0000000000..9daaf0de94 --- /dev/null +++ b/Sources/armory/logicnode/DrawTriangleNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import kha.Color; +import armory.renderpath.RenderToTexture; + +class DrawTriangleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawTriangleNode"); + + final colorVec = inputs[1].get(); + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + + final strength = inputs[3].get(); + final x1 = inputs[4].get(); + final y1 = inputs[5].get(); + final x2 = inputs[6].get(); + final y2 = inputs[7].get(); + final x3 = inputs[8].get(); + final y3 = inputs[9].get(); + + if (inputs[2].get()) { + RenderToTexture.g.fillTriangle(x1, y1, x2, y2, x3, y3); + } else { + RenderToTexture.g.drawLine(x1, y1, x2, y2, strength); + RenderToTexture.g.drawLine(x2, y2, x3, y3, strength); + RenderToTexture.g.drawLine(x3, y3, x1, y1, strength); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/DynamicNode.hx b/Sources/armory/logicnode/DynamicNode.hx new file mode 100644 index 0000000000..715b8e1be3 --- /dev/null +++ b/Sources/armory/logicnode/DynamicNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class DynamicNode extends LogicNode { + + public var value: Dynamic; + + public function new(tree: LogicTree, value: Dynamic = null) { + super(tree); + this.value = value == null ? {} : value; + } + + override function get(from: Int): Dynamic { + return value; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/ExpressionNode.hx b/Sources/armory/logicnode/ExpressionNode.hx new file mode 100644 index 0000000000..d27fb08f57 --- /dev/null +++ b/Sources/armory/logicnode/ExpressionNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +class ExpressionNode extends LogicNode { + + public var property0: String; + var result: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + #if hscript + var expr = property0; + var parser = new hscript.Parser(); + var ast = parser.parseString(expr); + var interp = new hscript.Interp(); + result = interp.execute(ast); + #end + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return result; + } +} diff --git a/Sources/armory/logicnode/FloatDeltaInterpolateNode.hx b/Sources/armory/logicnode/FloatDeltaInterpolateNode.hx new file mode 100644 index 0000000000..a13797079c --- /dev/null +++ b/Sources/armory/logicnode/FloatDeltaInterpolateNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import kha.FastFloat; + +class FloatDeltaInterpolateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + + } + + override function get(from: Int): FastFloat { + var fromValue = inputs[0].get(); + var toValue = inputs[1].get(); + var deltaTime = inputs[2].get(); + var rate = inputs[3].get(); + + var sign = toValue > fromValue ? 1.0 : -1.0; + var value = fromValue + deltaTime * rate * sign; + var min = Math.min(fromValue, toValue); + var max = Math.max(fromValue, toValue); + return value < min ? min : value > max ? max : value; + } +} diff --git a/Sources/armory/logicnode/FloatNode.hx b/Sources/armory/logicnode/FloatNode.hx new file mode 100644 index 0000000000..6df907a9ff --- /dev/null +++ b/Sources/armory/logicnode/FloatNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class FloatNode extends LogicNode { + + public var value: Float; + + public function new(tree: LogicTree, value = 0.0) { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/FunctionNode.hx b/Sources/armory/logicnode/FunctionNode.hx new file mode 100644 index 0000000000..7f2f67b947 --- /dev/null +++ b/Sources/armory/logicnode/FunctionNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class FunctionNode extends LogicNode { + + @:allow(armory.logicnode.LogicTree) + var args: Array = []; + @:allow(armory.logicnode.LogicTree) + var result: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + @:allow(armory.logicnode.LogicTree) + override function run(from: Int) { + runOutput(0); + } + + override function get(from: Int): Dynamic { + return this.args[from - 1]; + } +} diff --git a/Sources/armory/logicnode/FunctionOutputNode.hx b/Sources/armory/logicnode/FunctionOutputNode.hx new file mode 100644 index 0000000000..87b95c0a35 --- /dev/null +++ b/Sources/armory/logicnode/FunctionOutputNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class FunctionOutputNode extends LogicNode { + + @:allow(armory.logicnode.LogicTree) + var result: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + this.result = inputs[1].get(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/GamepadCoordsNode.hx b/Sources/armory/logicnode/GamepadCoordsNode.hx new file mode 100644 index 0000000000..a1a8660c8a --- /dev/null +++ b/Sources/armory/logicnode/GamepadCoordsNode.hx @@ -0,0 +1,44 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GamepadCoordsNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + if (from == 0) { + coords.x = gamepad.leftStick.x; + coords.y = gamepad.leftStick.y; + return coords; + } + else if (from == 1) { + coords.x = gamepad.rightStick.x; + coords.y = gamepad.rightStick.y; + return coords; + } + else if (from == 2) { + coords.x = gamepad.leftStick.movementX; + coords.y = gamepad.leftStick.movementY; + return coords; + } + else if (from == 3) { + coords.x = gamepad.rightStick.movementX; + coords.y = gamepad.rightStick.movementY; + return coords; + } + else if (from == 4) { + return gamepad.down("l2"); + } + else if (from == 5) { + return gamepad.down("r2"); + } + return null; + } +} diff --git a/Sources/armory/logicnode/GamepadSticksNode.hx b/Sources/armory/logicnode/GamepadSticksNode.hx new file mode 100644 index 0000000000..8d2bad8dbd --- /dev/null +++ b/Sources/armory/logicnode/GamepadSticksNode.hx @@ -0,0 +1,130 @@ +package armory.logicnode; + +class GamepadSticksNode extends LogicNode { + + public var property0: String; + public var property1: String; + public var property2: String; + var started = false; + var previousb = false; + + var gstarted = false; + var gpreviousb = false; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + if (gamepad == null) return; + var b = false; + + if(property1 == 'LeftStick') + switch (property2) { + case 'up': + b = gamepad.leftStick.y == -1; + case 'down': + b = gamepad.leftStick.y == 1; + case 'left': + b = gamepad.leftStick.x == -1; + case 'right': + b = gamepad.leftStick.x == 1; + case 'up-left': + b = gamepad.leftStick.y == -1 && gamepad.leftStick.x == -1; + case 'up-right': + b = gamepad.leftStick.y == -1 && gamepad.leftStick.x == 1; + case 'down-left': + b = gamepad.leftStick.y == 1 && gamepad.leftStick.x == -1; + case 'down-right': + b = gamepad.leftStick.y == 1 && gamepad.leftStick.x == 1; + } + else + switch (property2) { + case 'up': + b = gamepad.rightStick.y == -1; + case 'down': + b = gamepad.rightStick.y == 1; + case 'left': + b = gamepad.rightStick.x == -1; + case 'right': + b = gamepad.rightStick.x == 1; + case 'up-left': + b = gamepad.rightStick.y == -1 && gamepad.rightStick.x == -1; + case 'up-right': + b = gamepad.rightStick.y == -1 && gamepad.rightStick.x == 1; + case 'down-left': + b = gamepad.rightStick.y == 1 && gamepad.rightStick.x == -1; + case 'down-right': + b = gamepad.rightStick.y == 1 && gamepad.rightStick.x == 1; + } + + if (b) previousb = b; + if (b != previousb) started = false; + + if (property0 == 'Started' && b && !started){started = true; runOutput(0);} + else if (property0 == 'Down' && b){previousb = b; runOutput(0);} + else if (property0 == 'Released' && b != previousb){previousb = b; runOutput(0);} + + } + + override function get(from: Int): Dynamic { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + + if (gamepad == null) return false; + var b = false; + + if(property1 == 'LeftStick') + switch (property2) { + case 'up': + b = gamepad.leftStick.y == -1; + case 'down': + b = gamepad.leftStick.y == 1; + case 'left': + b = gamepad.leftStick.x == -1; + case 'right': + b = gamepad.leftStick.x == 1; + case 'up-left': + b = gamepad.leftStick.y == -1 && gamepad.leftStick.x == -1; + case 'up-right': + b = gamepad.leftStick.y == -1 && gamepad.leftStick.x == 1; + case 'down-left': + b = gamepad.leftStick.y == 1 && gamepad.leftStick.x == -1; + case 'down-right': + b = gamepad.leftStick.y == 1 && gamepad.leftStick.x == 1; + } + else + switch (property2) { + case 'up': + b = gamepad.rightStick.y == -1; + case 'down': + b = gamepad.rightStick.y == 1; + case 'left': + b = gamepad.rightStick.x == -1; + case 'right': + b = gamepad.rightStick.x == 1; + case 'up-left': + b = gamepad.rightStick.y == -1 && gamepad.rightStick.x == -1; + case 'up-right': + b = gamepad.rightStick.y == -1 && gamepad.rightStick.x == 1; + case 'down-left': + b = gamepad.rightStick.y == 1 && gamepad.rightStick.x == -1; + case 'down-right': + b = gamepad.rightStick.y == 1 && gamepad.rightStick.x == 1; + } + + if (b) gpreviousb = b; + if (b != gpreviousb) gstarted = false; + + if (property0 == 'Started' && b && !gstarted){gstarted = true; return true;} + else if (property0 == 'Down' && b){gpreviousb = b; return true;} + else if (property0 == 'Released' && b != gpreviousb){gpreviousb = b; return true;} + + return false; + + } +} diff --git a/Sources/armory/logicnode/GateNode.hx b/Sources/armory/logicnode/GateNode.hx new file mode 100644 index 0000000000..da1efc7527 --- /dev/null +++ b/Sources/armory/logicnode/GateNode.hx @@ -0,0 +1,56 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GateNode extends LogicNode { + + public var property0: String; + public var property1: Float; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Dynamic = inputs[1].get(); + var v2: Dynamic = inputs[2].get(); + var cond = false; + + switch (property0) { + case "Equal": + cond = Std.isOfType(v1, Vec4) ? (v1: Vec4).equals(v2) : v1 == v2; + case "Not Equal": + cond = Std.isOfType(v1, Vec4) ? !(v1: Vec4).equals(v2) : v1 != v2; + case "Almost Equal": + cond = Std.isOfType(v1, Vec4) ? (v1: Vec4).almostEquals(v2, property1) : Math.abs(v1 - v2) < property1; + case "Greater": + cond = v1 > v2; + case "Greater Equal": + cond = v1 >= v2; + case "Less": + cond = v1 < v2; + case "Less Equal": + cond = v1 <= v2; + case "Between": + var v3: Dynamic = inputs[3].get(); + cond = Std.isOfType(v1, Vec4) ? v2.x <= v1.x && v2.y <= v1.y && v2.z <= v1.z && v1.x <= v3.x && v1.y <= v3.y && v1.z <= v3.z : v2 <= v1 && v1 <= v3; + case "Or": + for (i in 1...inputs.length) { + if (inputs[i].get()) { + cond = true; + break; + } + } + case "And": + cond = true; + for (i in 1...inputs.length) { + if (!inputs[i].get()) { + cond = false; + break; + } + } + } + + cond ? runOutput(0) : runOutput(1); + } +} diff --git a/Sources/armory/logicnode/GetAgentDataNode.hx b/Sources/armory/logicnode/GetAgentDataNode.hx new file mode 100644 index 0000000000..163b138838 --- /dev/null +++ b/Sources/armory/logicnode/GetAgentDataNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetAgentDataNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Float { + var object: Object = inputs[0].get(); + + assert(Error, object != null, "The object to naviagte should not be null"); + +#if arm_navigation + var agent: armory.trait.NavAgent = object.getTrait(armory.trait.NavAgent); + assert(Error, agent != null, "The object does not have NavAgent Trait"); + if(from == 0) return agent.speed; + else return agent.turnDuration; +#else + return null; +#end + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx b/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx new file mode 100644 index 0000000000..98fdf44c1a --- /dev/null +++ b/Sources/armory/logicnode/GetBoneFkIkOnlyNode.hx @@ -0,0 +1,33 @@ +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; + + #else + return null; + + #end + } +} diff --git a/Sources/armory/logicnode/GetBoneTransformNode.hx b/Sources/armory/logicnode/GetBoneTransformNode.hx new file mode 100644 index 0000000000..01e408ec92 --- /dev/null +++ b/Sources/armory/logicnode/GetBoneTransformNode.hx @@ -0,0 +1,33 @@ +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); + + #else + return null; + + #end + } +} diff --git a/Sources/armory/logicnode/GetCameraAspectNode.hx b/Sources/armory/logicnode/GetCameraAspectNode.hx new file mode 100644 index 0000000000..046a6a867c --- /dev/null +++ b/Sources/armory/logicnode/GetCameraAspectNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class GetCameraAspectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var camera: CameraObject = inputs[0].get(); + + if (camera == null) return null; + + return camera.data.raw.aspect != null ? camera.data.raw.aspect : iron.App.w() / iron.App.h(); + } +} diff --git a/Sources/armory/logicnode/GetCameraFovNode.hx b/Sources/armory/logicnode/GetCameraFovNode.hx new file mode 100644 index 0000000000..5c8e9b9654 --- /dev/null +++ b/Sources/armory/logicnode/GetCameraFovNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class GetCameraFovNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var camera: CameraObject = inputs[0].get(); + + if (camera == null) return 0.0; + + return camera.data.raw.fov; + } +} diff --git a/Sources/armory/logicnode/GetCameraScaleNode.hx b/Sources/armory/logicnode/GetCameraScaleNode.hx new file mode 100644 index 0000000000..59f6a5bbb4 --- /dev/null +++ b/Sources/armory/logicnode/GetCameraScaleNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class GetCameraScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var camera: CameraObject = inputs[0].get(); + + if (camera == null) return null; + + return camera.data.raw.ortho[1]*2; + } +} diff --git a/Sources/armory/logicnode/GetCameraStartEndNode.hx b/Sources/armory/logicnode/GetCameraStartEndNode.hx new file mode 100644 index 0000000000..bdbb3b0161 --- /dev/null +++ b/Sources/armory/logicnode/GetCameraStartEndNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class GetCameraStartEndNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var camera: CameraObject = inputs[0].get(); + + if (camera == null) return null; + + return from == 0 ? camera.data.raw.near_plane : camera.data.raw.far_plane; + } +} diff --git a/Sources/armory/logicnode/GetCameraTypeNode.hx b/Sources/armory/logicnode/GetCameraTypeNode.hx new file mode 100644 index 0000000000..f1d1886644 --- /dev/null +++ b/Sources/armory/logicnode/GetCameraTypeNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class GetCameraTypeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var camera: CameraObject = inputs[0].get(); + + if (camera == null) return null; + + return camera.data.raw.ortho != null ? 1 : 0; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/GetChildNode.hx b/Sources/armory/logicnode/GetChildNode.hx new file mode 100644 index 0000000000..82777cdcdc --- /dev/null +++ b/Sources/armory/logicnode/GetChildNode.hx @@ -0,0 +1,53 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetChildNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var childName: String = inputs[1].get(); + + if (object == null || childName == null) return null; + + switch (property0) { + case "By Name": + return object.getChild(childName); + case "Contains": + return contains(object, childName); + case "Starts With": + return startsWith(object, childName); + case "Ends With": + return endsWith(object, childName); + } + + return null; + } + + function contains(o: Object, name: String): Object { + for (c in o.children) { + if (c.name.indexOf(name) >= 0) return c; + } + return null; + } + + function startsWith(o: Object, name: String): Object { + for (c in o.children) { + if (StringTools.startsWith(c.name, name)) return c; + } + return null; + } + + function endsWith(o: Object, name: String): Object { + for (c in o.children) { + if (StringTools.endsWith(c.name, name)) return c; + } + return null; + } +} diff --git a/Sources/armory/logicnode/GetChildrenNode.hx b/Sources/armory/logicnode/GetChildrenNode.hx new file mode 100644 index 0000000000..8e6230e6ff --- /dev/null +++ b/Sources/armory/logicnode/GetChildrenNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetChildrenNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.children; + } +} diff --git a/Sources/armory/logicnode/GetContactsNode.hx b/Sources/armory/logicnode/GetContactsNode.hx new file mode 100644 index 0000000000..1968341793 --- /dev/null +++ b/Sources/armory/logicnode/GetContactsNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class GetContactsNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rbs = physics.getContacts(object.getTrait(RigidBody)); + var obs = []; + + if (rbs != null) for (rb in rbs) if (rb != null) obs.push(rb.object); + return obs; +#end + + return null; + } +} diff --git a/Sources/armory/logicnode/GetCursorLocationNode.hx b/Sources/armory/logicnode/GetCursorLocationNode.hx new file mode 100644 index 0000000000..5811f6a4f8 --- /dev/null +++ b/Sources/armory/logicnode/GetCursorLocationNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class GetCursorLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + return switch (from) { + case 0: mouse.x; + case 1: mouse.y; + case 2: mouse.x * -1; + case 3: mouse.y * -1; + default: null; + } + } +} diff --git a/Sources/armory/logicnode/GetCursorStateNode.hx b/Sources/armory/logicnode/GetCursorStateNode.hx new file mode 100644 index 0000000000..c83db7a5a3 --- /dev/null +++ b/Sources/armory/logicnode/GetCursorStateNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetCursorStateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + return switch (from) { + case 0: + mouse.hidden && mouse.locked ? return true : return false; + case 1: + mouse.hidden; + case 2: + mouse.locked; + default: + null; + } + } +} diff --git a/Sources/armory/logicnode/GetDateTimeNode.hx b/Sources/armory/logicnode/GetDateTimeNode.hx new file mode 100644 index 0000000000..71ba92a689 --- /dev/null +++ b/Sources/armory/logicnode/GetDateTimeNode.hx @@ -0,0 +1,67 @@ +package armory.logicnode; + +class GetDateTimeNode extends LogicNode { + public var property0: String; + public var value: Int; + public var timeStamp: Float; + public var timezoneOffset: Int; + public var weekDay: Int; + public var day: Int; + public var month: Int; + public var year: Int; + public var hours: Int; + public var minutes: Int; + public var seconds: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var dateTime = Date.now(); + timeStamp = dateTime.getTime(); + timezoneOffset = dateTime.getTimezoneOffset(); + weekDay = dateTime.getDay(); + day = dateTime.getDate(); + month = dateTime.getMonth(); + year = dateTime.getFullYear(); + hours = dateTime.getHours(); + minutes = dateTime.getMinutes(); + seconds = dateTime.getSeconds(); + runOutput(0); + } + + override function get(from: Int): Dynamic { + if(property0 == "all"){ + return switch (from) { + case 1: timeStamp; + case 2: timezoneOffset; + case 3: weekDay; + case 4: day; + case 5: month; + case 6: year; + case 7: hours; + case 8: minutes; + case 9: seconds; + default: null; + } + } else if (property0 == "formatted"){ + return DateTools.format(Date.now(), inputs[0].get()); + } else { + var dateTime = Date.now(); + return switch (property0) { + case "now": dateTime; + case "timestamp": dateTime.getTime(); + case "timezoneOffset": dateTime.getTimezoneOffset(); + case "weekDay": dateTime.getDay(); + case "day": dateTime.getDate(); + case "month": dateTime.getMonth(); + case "year": dateTime.getFullYear(); + case "hours": dateTime.getHours(); + case "minutes": dateTime.getMinutes(); + case "seconds": dateTime.getSeconds(); + default: null; + } + } + } +} diff --git a/Sources/armory/logicnode/GetDebugConsoleSettings.hx b/Sources/armory/logicnode/GetDebugConsoleSettings.hx new file mode 100644 index 0000000000..ea6778289a --- /dev/null +++ b/Sources/armory/logicnode/GetDebugConsoleSettings.hx @@ -0,0 +1,26 @@ +package armory.logicnode; +import armory.trait.internal.DebugConsole; + +class GetDebugConsoleSettings extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + #if arm_debug + switch(from) { + case 0: return armory.trait.internal.DebugConsole.getVisible(); + case 1: return armory.trait.internal.DebugConsole.getScale(); + case 2: { + switch (armory.trait.internal.DebugConsole.getPosition()) { + case PositionStateEnum.Left: return "Left"; + case PositionStateEnum.Center: return "Center"; + case PositionStateEnum.Right: return "Right"; + } + } + } + #end + return null; + } +} diff --git a/Sources/armory/logicnode/GetDimensionNode.hx b/Sources/armory/logicnode/GetDimensionNode.hx new file mode 100644 index 0000000000..f2fe8b65a7 --- /dev/null +++ b/Sources/armory/logicnode/GetDimensionNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetDimensionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.transform.dim; + } +} diff --git a/Sources/armory/logicnode/GetDistanceNode.hx b/Sources/armory/logicnode/GetDistanceNode.hx new file mode 100644 index 0000000000..003708719d --- /dev/null +++ b/Sources/armory/logicnode/GetDistanceNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetDistanceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object1: Object = inputs[0].get(); + var object2: Object = inputs[1].get(); + + if (object1 == null || object2 == null) return 0; + + return iron.math.Vec4.distance(object1.transform.world.getLoc(), object2.transform.world.getLoc()); + } +} diff --git a/Sources/armory/logicnode/GetFPSNode.hx b/Sources/armory/logicnode/GetFPSNode.hx new file mode 100644 index 0000000000..32004a5a77 --- /dev/null +++ b/Sources/armory/logicnode/GetFPSNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class GetFPSNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) { + var fps = Math.round(1 / iron.system.Time.realDelta); + if ((fps == Math.POSITIVE_INFINITY) || (fps == Math.NEGATIVE_INFINITY) || (Math.isNaN(fps))) { + return 0; + } + return fps; + } + return null; + } +} diff --git a/Sources/armory/logicnode/GetFirstContactNode.hx b/Sources/armory/logicnode/GetFirstContactNode.hx new file mode 100644 index 0000000000..aa03a077ce --- /dev/null +++ b/Sources/armory/logicnode/GetFirstContactNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class GetFirstContactNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + if (object == null) return null; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rbs = physics.getContacts(object.getTrait(RigidBody)); + + if (rbs != null && rbs.length > 0) return rbs[0].object; +#end + + return null; + } +} diff --git a/Sources/armory/logicnode/GetGamepadStartedNode.hx b/Sources/armory/logicnode/GetGamepadStartedNode.hx new file mode 100644 index 0000000000..30a362a025 --- /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[1].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/GetGlobalCanvasFontSizeNode.hx b/Sources/armory/logicnode/GetGlobalCanvasFontSizeNode.hx new file mode 100644 index 0000000000..5d670a0351 --- /dev/null +++ b/Sources/armory/logicnode/GetGlobalCanvasFontSizeNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class GetGlobalCanvasFontSizeNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + return canvas.getCanvasFontSize(); + } +#end +} \ No newline at end of file diff --git a/Sources/armory/logicnode/GetGlobalCanvasScaleNode.hx b/Sources/armory/logicnode/GetGlobalCanvasScaleNode.hx new file mode 100644 index 0000000000..03bec63fad --- /dev/null +++ b/Sources/armory/logicnode/GetGlobalCanvasScaleNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class GetGlobalCanvasScaleNode extends LogicNode { + + var canvas: CanvasScript; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function get(from: Int): Dynamic { + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + return canvas.getUiScale(); + } +#end +} diff --git a/Sources/armory/logicnode/GetGravityNode.hx b/Sources/armory/logicnode/GetGravityNode.hx new file mode 100644 index 0000000000..db5f705fad --- /dev/null +++ b/Sources/armory/logicnode/GetGravityNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class GetGravityNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + + return physics.getGravity(); +#end + + return null; + } +} diff --git a/Sources/armory/logicnode/GetGroupNode.hx b/Sources/armory/logicnode/GetGroupNode.hx new file mode 100644 index 0000000000..5edd8685ed --- /dev/null +++ b/Sources/armory/logicnode/GetGroupNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class GetGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var groupName: String = inputs[0].get(); + return iron.Scene.active.getGroup(groupName); + } +} diff --git a/Sources/armory/logicnode/GetHaxePropertyNode.hx b/Sources/armory/logicnode/GetHaxePropertyNode.hx new file mode 100644 index 0000000000..f2aa65a122 --- /dev/null +++ b/Sources/armory/logicnode/GetHaxePropertyNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class GetHaxePropertyNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Dynamic = inputs[0].get(); + var property: String = inputs[1].get(); + + if (object == null) return null; + + return Reflect.getProperty(object, property); + } +} diff --git a/Sources/armory/logicnode/GetInputMapKeyNode.hx b/Sources/armory/logicnode/GetInputMapKeyNode.hx new file mode 100644 index 0000000000..251a6d1222 --- /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 0000000000..ebe752e570 --- /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 new file mode 100644 index 0000000000..006844736a --- /dev/null +++ b/Sources/armory/logicnode/GetLocationNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var relative: Bool = inputs[1].get(); + + if (object == null) return null; + + 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/GetMapValueNode.hx b/Sources/armory/logicnode/GetMapValueNode.hx new file mode 100644 index 0000000000..4824b00a86 --- /dev/null +++ b/Sources/armory/logicnode/GetMapValueNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + + +class GetMapValueNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var map: Map = inputs[0].get(); + if (map == null) return null; + + var key: Dynamic = inputs[1].get(); + return map.get(key); + } +} diff --git a/Sources/armory/logicnode/GetMaterialNode.hx b/Sources/armory/logicnode/GetMaterialNode.hx new file mode 100644 index 0000000000..4554569120 --- /dev/null +++ b/Sources/armory/logicnode/GetMaterialNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.object.MeshObject; +import iron.object.DecalObject; + +class GetMaterialNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var object = inputs[0].get(); + + assert(Error, object != null, "The object input must not be null"); + + #if rp_decals + if (Std.isOfType(object, DecalObject)) { + var decal = cast(object, DecalObject); + return decal.material; + } + #end + + if (Std.isOfType(object, MeshObject)) { + var mesh = cast(object, MeshObject); + var slot: Int = inputs[1].get(); + + if (mesh == null) return null; + + return mesh.materials[slot]; + } + + return null; + } +} diff --git a/Sources/armory/logicnode/GetMeshNode.hx b/Sources/armory/logicnode/GetMeshNode.hx new file mode 100644 index 0000000000..0c4a576e3a --- /dev/null +++ b/Sources/armory/logicnode/GetMeshNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class GetMeshNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: MeshObject = inputs[0].get(); + + if (object == null) return null; + + return object.data; + } +} diff --git a/Sources/armory/logicnode/GetMouseLockNode.hx b/Sources/armory/logicnode/GetMouseLockNode.hx new file mode 100644 index 0000000000..961a582928 --- /dev/null +++ b/Sources/armory/logicnode/GetMouseLockNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetMouseLockNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + return mouse.locked; + } +} diff --git a/Sources/armory/logicnode/GetMouseMovementNode.hx b/Sources/armory/logicnode/GetMouseMovementNode.hx new file mode 100644 index 0000000000..a1b58a04af --- /dev/null +++ b/Sources/armory/logicnode/GetMouseMovementNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import kha.FastFloat; + +class GetMouseMovementNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + var multX: FastFloat = inputs[0].get(); + var multY: FastFloat = inputs[1].get(); + var multWheelDelta: FastFloat = inputs[2].get(); + + return switch (from) { + case 0: mouse.movementX; + case 1: mouse.movementY; + case 2: mouse.movementX * multX; + case 3: mouse.movementY * multY; + case 4: mouse.wheelDelta; + case 5: mouse.wheelDelta * multWheelDelta; + default: 0; + } + } +} diff --git a/Sources/armory/logicnode/GetMouseStartedNode.hx b/Sources/armory/logicnode/GetMouseStartedNode.hx new file mode 100644 index 0000000000..8f2d5f3219 --- /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/GetMouseVisibleNode.hx b/Sources/armory/logicnode/GetMouseVisibleNode.hx new file mode 100644 index 0000000000..2de8ed808a --- /dev/null +++ b/Sources/armory/logicnode/GetMouseVisibleNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.system.Input; + +class GetMouseVisibleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + + if (mouse.hidden == false) return true; + + return false; + } +} diff --git a/Sources/armory/logicnode/GetNameNode.hx b/Sources/armory/logicnode/GetNameNode.hx new file mode 100644 index 0000000000..dc49cf8dc4 --- /dev/null +++ b/Sources/armory/logicnode/GetNameNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetNameNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return ""; + + return object.name; + } +} diff --git a/Sources/armory/logicnode/GetObjectByUidNode.hx b/Sources/armory/logicnode/GetObjectByUidNode.hx new file mode 100644 index 0000000000..b7c8f3baca --- /dev/null +++ b/Sources/armory/logicnode/GetObjectByUidNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.data.SceneFormat; +import iron.object.Object; + +class GetObjectByUidNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var objectUid: Int = inputs[0].get(); + + var obj = iron.Scene.active.getChildren(true); + + for (obji in obj) if (obji.uid == objectUid) return obji; + + return null; + + } +} diff --git a/Sources/armory/logicnode/GetObjectGroupNode.hx b/Sources/armory/logicnode/GetObjectGroupNode.hx new file mode 100644 index 0000000000..ca446d2d56 --- /dev/null +++ b/Sources/armory/logicnode/GetObjectGroupNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetObjectGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var object: Object = inputs[0].get(); + + var col: Array = []; + + if(object == null) return null; + + var raw = iron.Scene.active.raw; + + for (g in raw.groups){ + if(iron.Scene.active.getGroup(g.name).indexOf(object) != -1) col.push(g.name); + + } + + return from == 0 ? col : col.length; + } +} diff --git a/Sources/armory/logicnode/GetObjectNode.hx b/Sources/armory/logicnode/GetObjectNode.hx new file mode 100644 index 0000000000..08c30c4cab --- /dev/null +++ b/Sources/armory/logicnode/GetObjectNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.data.SceneFormat; +import iron.object.Object; + +class GetObjectNode extends LogicNode { + + /** Scene from which to take the object **/ + public var property0: Null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var objectName: String = inputs[0].get(); + + return iron.Scene.active.getChild(objectName); + } +} diff --git a/Sources/armory/logicnode/GetObjectOffscreenNode.hx b/Sources/armory/logicnode/GetObjectOffscreenNode.hx new file mode 100644 index 0000000000..6424d17119 --- /dev/null +++ b/Sources/armory/logicnode/GetObjectOffscreenNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetObjectOffscreenNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return switch (from) { + case 0: object.culled; + case 1: object.culledMesh; + case 2: object.culledShadow; + default: null; + } + + } +} diff --git a/Sources/armory/logicnode/GetObjectTraitsNode.hx b/Sources/armory/logicnode/GetObjectTraitsNode.hx new file mode 100644 index 0000000000..8a5d0e3260 --- /dev/null +++ b/Sources/armory/logicnode/GetObjectTraitsNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetObjectTraitsNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + if (object == null) return null; + return object.traits; + } +} diff --git a/Sources/armory/logicnode/GetParentNode.hx b/Sources/armory/logicnode/GetParentNode.hx new file mode 100644 index 0000000000..8c9c7ace9d --- /dev/null +++ b/Sources/armory/logicnode/GetParentNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetParentNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.parent; + } +} diff --git a/Sources/armory/logicnode/GetPointVelocityNode.hx b/Sources/armory/logicnode/GetPointVelocityNode.hx new file mode 100644 index 0000000000..48889be2f9 --- /dev/null +++ b/Sources/armory/logicnode/GetPointVelocityNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class GetPointVelocityNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var point: Vec4 = inputs[1].get(); + + if (object == null || point == null) + return null; + + #if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + return rb.getPointVelocity(point.x, point.y, point.z); + #end + + return null; + } + +} diff --git a/Sources/armory/logicnode/GetPropertyNode.hx b/Sources/armory/logicnode/GetPropertyNode.hx new file mode 100644 index 0000000000..0d3c7a7ecc --- /dev/null +++ b/Sources/armory/logicnode/GetPropertyNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetPropertyNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var property: String = inputs[1].get(); + + if (from == 0) { + if (object == null || object.properties == null) return null; + return object.properties.get(property); + } + else { + return property; + } + } +} diff --git a/Sources/armory/logicnode/GetRigidBodyDataNode.hx b/Sources/armory/logicnode/GetRigidBodyDataNode.hx new file mode 100644 index 0000000000..08bd67d956 --- /dev/null +++ b/Sources/armory/logicnode/GetRigidBodyDataNode.hx @@ -0,0 +1,51 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class GetRigidBodyDataNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + +#if arm_physics + var rb = object.getTrait(RigidBody); + + return switch (from) { + case 0: + rb == null ? return false : return true; + case 1: + rb.group; + case 2: + rb.mask; + case 3: + rb.animated; + case 4: + rb.staticObj; + case 5: + rb.angularDamping; + case 6: + rb.linearDamping; + case 7: + rb.friction; + case 8: + rb.mass; + //case 9: ; // collision shape + //case 10: ; // activation state + //case 11: ; // is gravity enabled + //case 12: ; // angular factor + //case 13: ; // linear factor + default: + null; + } +#end + + return null; + } +} diff --git a/Sources/armory/logicnode/GetRotationNode.hx b/Sources/armory/logicnode/GetRotationNode.hx new file mode 100644 index 0000000000..4fb8537df7 --- /dev/null +++ b/Sources/armory/logicnode/GetRotationNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Quat; +import iron.math.Vec4; + +class GetRotationNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) { + return null; + } + + + switch(property0){ + case "Local": + return object.transform.rot; + case "Global":{ + var useless1 = new Vec4(); + var ret = new Quat(); + object.transform.world.decompose(useless1, ret, useless1); + return ret; + }} + return null; + } +} diff --git a/Sources/armory/logicnode/GetScaleNode.hx b/Sources/armory/logicnode/GetScaleNode.hx new file mode 100644 index 0000000000..ecaed09d89 --- /dev/null +++ b/Sources/armory/logicnode/GetScaleNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.transform.scale; + } +} diff --git a/Sources/armory/logicnode/GetSystemLanguage.hx b/Sources/armory/logicnode/GetSystemLanguage.hx new file mode 100644 index 0000000000..5a8563527c --- /dev/null +++ b/Sources/armory/logicnode/GetSystemLanguage.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class GetSystemLanguage extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) return kha.System.language; + return null; + } +} diff --git a/Sources/armory/logicnode/GetSystemName.hx b/Sources/armory/logicnode/GetSystemName.hx new file mode 100644 index 0000000000..97be58b6cc --- /dev/null +++ b/Sources/armory/logicnode/GetSystemName.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +class GetSystemName extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var systemName: String = kha.System.systemId; + + return switch (from) { + case 0: systemName; + case 1: equalsCI(systemName, 'Windows'); + case 2: equalsCI(systemName, 'Linux'); + case 3: equalsCI(systemName, 'Mac'); + case 4: equalsCI(systemName, 'HTML5'); + case 5: equalsCI(systemName, 'Android'); + default: null; + } + } + + static inline function equalsCI(a: String, b: String): Bool { + return a.toLowerCase() == b.toLowerCase(); + } +} diff --git a/Sources/armory/logicnode/GetTilesheetStateNode.hx b/Sources/armory/logicnode/GetTilesheetStateNode.hx new file mode 100644 index 0000000000..65a03bb841 --- /dev/null +++ b/Sources/armory/logicnode/GetTilesheetStateNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class GetTilesheetStateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: MeshObject = inputs[0].get(); + + if (object == null) return null; + + var tilesheet = object.tilesheet; + + return switch (from) { + case 0: tilesheet.action.name; + case 1: tilesheet.frame; + case 2: tilesheet.paused; + default: null; + } + } +} diff --git a/Sources/armory/logicnode/GetTouchLocationNode.hx b/Sources/armory/logicnode/GetTouchLocationNode.hx new file mode 100644 index 0000000000..c6f22bf1b2 --- /dev/null +++ b/Sources/armory/logicnode/GetTouchLocationNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GetTouchLocationNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var touch = iron.system.Input.getSurface(); + + return switch (from) { + case 0: touch.x; + case 1: touch.y; + case 2: touch.x * -1; + case 3: touch.y * -1; + default: null; + } + } +} diff --git a/Sources/armory/logicnode/GetTouchMovementNode.hx b/Sources/armory/logicnode/GetTouchMovementNode.hx new file mode 100644 index 0000000000..1ca4a075fe --- /dev/null +++ b/Sources/armory/logicnode/GetTouchMovementNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GetTouchMovementNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var touch = iron.system.Input.getSurface(); + var multX: Float = inputs[0].get(); + var multY: Float = inputs[1].get(); + + return switch (from) { + case 0: touch.movementX; + case 1: touch.movementY; + case 2: touch.movementX * multX; + case 3: touch.movementY * multY; + default: null; + } + } +} diff --git a/Sources/armory/logicnode/GetTraitNameNode.hx b/Sources/armory/logicnode/GetTraitNameNode.hx new file mode 100644 index 0000000000..ca04f375bb --- /dev/null +++ b/Sources/armory/logicnode/GetTraitNameNode.hx @@ -0,0 +1,44 @@ +package armory.logicnode; + +import iron.Trait; + +class GetTraitNameNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var trait: Dynamic = inputs[0].get(); + if (trait == null) return null; + switch (from) { + // Name + case 0: { + // Check CanvasScript + var cname = cast Type.resolveClass("armory.trait.internal.CanvasScript"); + if (Std.isOfType(trait, cname)) { + return trait.cnvName; + } + // Check WasmScript + var cname = cast Type.resolveClass("armory.trait.internal.WasmScript"); + if (Std.isOfType(trait, cname)) { + return trait.wasmName; + } + // Other + var res_arr = (Type.getClassName(Type.getClass(trait))).split("."); + return res_arr[res_arr.length - 1]; + } + // Class Type + case 1: { + var cname = Type.getClassName(Type.getClass(trait)); + if (cname.indexOf("CanvasScript") > -1) return "Canvas"; + if (cname.indexOf("WasmScript") > -1) return "Wasm"; + if (cname.indexOf("armory.trait.") > -1) return "Bundle"; + if (cname.indexOf("arm.node.") > -1) return "LogicNode"; + if (cname.indexOf("Trait") > -1) return "Haxe"; + return null; + } + } + return null; + } +} diff --git a/Sources/armory/logicnode/GetTraitNode.hx b/Sources/armory/logicnode/GetTraitNode.hx new file mode 100644 index 0000000000..c9ddf41662 --- /dev/null +++ b/Sources/armory/logicnode/GetTraitNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetTraitNode extends LogicNode { + + var cname: Class = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var name: String = inputs[1].get(); + + if (object == null) return null; + if (cname == null) cname = cast Type.resolveClass(Main.projectPackage + "." + name); + if (cname == null) cname = cast Type.resolveClass(Main.projectPackage + ".node." + name); + + return object.getTrait(cname); + } +} diff --git a/Sources/armory/logicnode/GetTraitPausedNode.hx b/Sources/armory/logicnode/GetTraitPausedNode.hx new file mode 100644 index 0000000000..36371cf551 --- /dev/null +++ b/Sources/armory/logicnode/GetTraitPausedNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class GetTraitPausedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var trait: Dynamic = inputs[0].get(); + + if (trait == null) return null; + + return trait.paused; + } +} diff --git a/Sources/armory/logicnode/GetTransformNode.hx b/Sources/armory/logicnode/GetTransformNode.hx new file mode 100644 index 0000000000..379df0aee4 --- /dev/null +++ b/Sources/armory/logicnode/GetTransformNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetTransformNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.transform.world; + } +} diff --git a/Sources/armory/logicnode/GetUidNode.hx b/Sources/armory/logicnode/GetUidNode.hx new file mode 100644 index 0000000000..4ee2abd91e --- /dev/null +++ b/Sources/armory/logicnode/GetUidNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetUidNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return object.uid; + } +} diff --git a/Sources/armory/logicnode/GetVelocityNode.hx b/Sources/armory/logicnode/GetVelocityNode.hx new file mode 100644 index 0000000000..2742d7d647 --- /dev/null +++ b/Sources/armory/logicnode/GetVelocityNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class GetVelocityNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var localLinear: Bool = inputs[1].get(); + var localAngular: Bool = inputs[2].get(); + + if (object == null) return null; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + + if (from == 0) { + !localLinear ? return rb.getLinearVelocity() : return object.transform.getWorldVectorAlongLocalAxis(rb.getLinearVelocity()); + } + + else { + !localAngular ? return rb.getAngularVelocity() : return object.transform.getWorldVectorAlongLocalAxis(rb.getAngularVelocity()); + } +#end + + return null; + } + +} diff --git a/Sources/armory/logicnode/GetVisibleNode.hx b/Sources/armory/logicnode/GetVisibleNode.hx new file mode 100644 index 0000000000..d3dddd3e9c --- /dev/null +++ b/Sources/armory/logicnode/GetVisibleNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.object.Object; + +class GetVisibleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return switch (from) { + case 0: object.visible; + case 1: object.visibleMesh; + case 2: object.visibleShadow; + default: null; + } + + } +} diff --git a/Sources/armory/logicnode/GetWorldNode.hx b/Sources/armory/logicnode/GetWorldNode.hx new file mode 100644 index 0000000000..c7fa234e69 --- /dev/null +++ b/Sources/armory/logicnode/GetWorldNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; + +class GetWorldNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + + if (object == null) return null; + + return switch (property0) { + case "Right": object.transform.world.right(); + case "Look": object.transform.world.look(); + case "Up": object.transform.world.up(); + default: null; + } + } +} diff --git a/Sources/armory/logicnode/GlobalObjectNode.hx b/Sources/armory/logicnode/GlobalObjectNode.hx new file mode 100644 index 0000000000..c227079a72 --- /dev/null +++ b/Sources/armory/logicnode/GlobalObjectNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class GlobalObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return iron.Scene.global; + } +} diff --git a/Sources/armory/logicnode/GoToLocationNode.hx b/Sources/armory/logicnode/GoToLocationNode.hx new file mode 100644 index 0000000000..cdb4b0bc5e --- /dev/null +++ b/Sources/armory/logicnode/GoToLocationNode.hx @@ -0,0 +1,82 @@ +package armory.logicnode; + +#if arm_physics +import armory.trait.physics.bullet.PhysicsWorld; +#end +import armory.trait.navigation.Navigation; +import iron.object.Object; +import iron.math.Vec4; + +class GoToLocationNode extends LogicNode { + + var object: Object; + var location: Vec4; + var speed: Float; + var turnDuration: Float; + var heightOffset: Float; + var useRaycast: Bool; + var rayCastDepth: Float; + var rayCastMask: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + object = inputs[1].get(); + location = inputs[2].get(); + speed = inputs[3].get(); + turnDuration = inputs[4].get(); + heightOffset = inputs[5].get(); + useRaycast = inputs[6].get(); + rayCastDepth = inputs[7].get(); + rayCastMask = inputs[8].get(); + + assert(Error, object != null, "The object input not be null"); + assert(Error, location != null, "The location to navigate to must not be null"); + assert(Error, speed != null, "Speed of Nav Agent should not be null"); + assert(Warning, speed >= 0, "Speed of Nav Agent should be positive"); + assert(Error, turnDuration != null, "Turn Duration of Nav Agent should not be null"); + assert(Warning, turnDuration >= 0, "Turn Duration of Nav Agent should be positive"); + +#if arm_navigation + var from = object.transform.world.getLoc(); + var to = location; + + assert(Error, Navigation.active.navMeshes.length > 0, "No Navigation Mesh Present"); + Navigation.active.navMeshes[0].findPath(from, to, function(path: Array) { + var agent: armory.trait.NavAgent = object.getTrait(armory.trait.NavAgent); + assert(Error, agent != null, "Object does not have a NavAgent trait"); + agent.speed = speed; + agent.turnDuration = turnDuration; + agent.heightOffset = heightOffset; + agent.tickPos = tickPos; + agent.tickRot = tickRot; + agent.setPath(path); + }); +#end + + runOutput(0); + } + + function tickPos(){ + #if arm_physics + if(useRaycast) setAgentHeight(); + #end + runOutput(1); + } + + function tickRot(){ + runOutput(2); + } + + function setAgentHeight(){ + #if arm_physics + var fromLoc = object.transform.world.getLoc(); + var toLoc = fromLoc.clone(); + toLoc.z += rayCastDepth; + var hit = PhysicsWorld.active.rayCast(fromLoc, toLoc, rayCastMask); + if(hit != null) object.transform.loc.z = hit.pos.z + heightOffset; + #end + } +} diff --git a/Sources/armory/logicnode/GroupInputsNode.hx b/Sources/armory/logicnode/GroupInputsNode.hx new file mode 100644 index 0000000000..485ae409ae --- /dev/null +++ b/Sources/armory/logicnode/GroupInputsNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class GroupInputsNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + runOutput(from); + } + + override function get(from: Int): Dynamic { + return inputs[from].get(); + } +} diff --git a/Sources/armory/logicnode/GroupNode.hx b/Sources/armory/logicnode/GroupNode.hx new file mode 100644 index 0000000000..3f8184c011 --- /dev/null +++ b/Sources/armory/logicnode/GroupNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +import iron.Scene; + +class GroupNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return Scene.active.getGroup(property0); + } +} diff --git a/Sources/armory/logicnode/GroupOutputsNode.hx b/Sources/armory/logicnode/GroupOutputsNode.hx new file mode 100644 index 0000000000..21817753d8 --- /dev/null +++ b/Sources/armory/logicnode/GroupOutputsNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class GroupOutputsNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + runOutput(from); + } + + override function get(from: Int): Dynamic { + return inputs[from].get(); + } +} diff --git a/Sources/armory/logicnode/HasContactArrayNode.hx b/Sources/armory/logicnode/HasContactArrayNode.hx new file mode 100644 index 0000000000..b5efcdb8f5 --- /dev/null +++ b/Sources/armory/logicnode/HasContactArrayNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class HasContactArrayNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object1: Object = inputs[0].get(); + var objects: Array = inputs[1].get(); + if (object1 == null || objects == null) return false; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb1 = object1.getTrait(RigidBody); + var rbs = physics.getContacts(rb1); + + if (rb1 != null && rbs != null) { + for (object2 in objects) { + var rb2 = object2.getTrait(RigidBody); + for (rb in rbs) { + if (rb == rb2) { + return true; + } + } + } + } +#end + return false; + } +} diff --git a/Sources/armory/logicnode/HasContactNode.hx b/Sources/armory/logicnode/HasContactNode.hx new file mode 100644 index 0000000000..ab10941c30 --- /dev/null +++ b/Sources/armory/logicnode/HasContactNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class HasContactNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object1: Object = inputs[0].get(); + var object2: Object = inputs[1].get(); + + if (object1 == null || object2 == null) return false; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb2 = object2.getTrait(RigidBody); + var rbs = physics.getContacts(object1.getTrait(RigidBody)); + if (rbs != null) for (rb in rbs) if (rb == rb2) return true; +#end + return false; + } +} diff --git a/Sources/armory/logicnode/IntFromBooleanNode.hx b/Sources/armory/logicnode/IntFromBooleanNode.hx new file mode 100644 index 0000000000..e85920d929 --- /dev/null +++ b/Sources/armory/logicnode/IntFromBooleanNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class IntFromBooleanNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var bool = inputs[0].get(); + + return bool ? 1 : 0; + + } +} diff --git a/Sources/armory/logicnode/IntegerNode.hx b/Sources/armory/logicnode/IntegerNode.hx new file mode 100644 index 0000000000..9cd704bbd7 --- /dev/null +++ b/Sources/armory/logicnode/IntegerNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class IntegerNode extends LogicNode { + + public var value: Int; + + public function new(tree: LogicTree, value = 0) { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/InverseNode.hx b/Sources/armory/logicnode/InverseNode.hx new file mode 100644 index 0000000000..d4627e6dda --- /dev/null +++ b/Sources/armory/logicnode/InverseNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class InverseNode extends LogicNode { + + var c = false; + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnUpdate(update); + } + + override function run(from: Int) { + c = true; + } + + function update() { + if (!c) runOutput(0); + c = false; + } +} diff --git a/Sources/armory/logicnode/IsFalseNode.hx b/Sources/armory/logicnode/IsFalseNode.hx new file mode 100644 index 0000000000..b8d7b1f282 --- /dev/null +++ b/Sources/armory/logicnode/IsFalseNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class IsFalseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Bool = inputs[1].get(); + if (!v1) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/IsNoneNode.hx b/Sources/armory/logicnode/IsNoneNode.hx new file mode 100644 index 0000000000..7eafe4b05b --- /dev/null +++ b/Sources/armory/logicnode/IsNoneNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class IsNoneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Dynamic = inputs[1].get(); + if (v1 == null) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/IsNotNoneNode.hx b/Sources/armory/logicnode/IsNotNoneNode.hx new file mode 100644 index 0000000000..2fc51a43f3 --- /dev/null +++ b/Sources/armory/logicnode/IsNotNoneNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class IsNotNoneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Dynamic = inputs[1].get(); + if (v1 != null) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/IsRigidBodyActiveNode.hx b/Sources/armory/logicnode/IsRigidBodyActiveNode.hx new file mode 100644 index 0000000000..8467d51718 --- /dev/null +++ b/Sources/armory/logicnode/IsRigidBodyActiveNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class IsRigidBodyActiveNode extends LogicNode { + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { +#if arm_physics + final object: iron.object.Object = inputs[0].get(); + + if (object == null) { + return false; + } + + final rb = object.getTrait(armory.trait.physics.RigidBody); + return rb != null && rb.isActive(); +#else + return false; +#end + } +} diff --git a/Sources/armory/logicnode/IsTrueNode.hx b/Sources/armory/logicnode/IsTrueNode.hx new file mode 100644 index 0000000000..a2a0b73cce --- /dev/null +++ b/Sources/armory/logicnode/IsTrueNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class IsTrueNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Bool = inputs[1].get(); + if (v1) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/KeyInterpolateNode.hx b/Sources/armory/logicnode/KeyInterpolateNode.hx new file mode 100644 index 0000000000..7f1c0f06a6 --- /dev/null +++ b/Sources/armory/logicnode/KeyInterpolateNode.hx @@ -0,0 +1,32 @@ +package armory.logicnode; + +import kha.FastFloat; + +class KeyInterpolateNode extends LogicNode { + + var value: FastFloat = 0.0; + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnInit(init); + tree.notifyOnUpdate(update); + } + + function init() { + value = clamp(inputs[1].get()); + } + + function update() { + final sign = inputs[0].get() ? 1.0 : -1.0; + final rate = inputs[2].get(); + value = clamp(value + rate * sign); + } + + override function get(from: Int): FastFloat { + return value; + } + + inline function clamp(value: FastFloat): FastFloat { + return value < 0.0 ? 0.0 : value > 1.0 ? 1.0 : value; + } +} diff --git a/Sources/armory/logicnode/LengthStringNode.hx b/Sources/armory/logicnode/LengthStringNode.hx new file mode 100644 index 0000000000..f6128366e4 --- /dev/null +++ b/Sources/armory/logicnode/LengthStringNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class LengthStringNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s: String = inputs[0].get(); + if (s == null) return null; + + return s.length; + } +} diff --git a/Sources/armory/logicnode/LenstextureGetNode.hx b/Sources/armory/logicnode/LenstextureGetNode.hx new file mode 100644 index 0000000000..1ce42731ed --- /dev/null +++ b/Sources/armory/logicnode/LenstextureGetNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class LenstextureGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.lenstexture_uniforms[0]; + case 1: armory.renderpath.Postprocess.lenstexture_uniforms[1]; + case 2: armory.renderpath.Postprocess.lenstexture_uniforms[2]; + case 3: armory.renderpath.Postprocess.lenstexture_uniforms[3]; + case 4: armory.renderpath.Postprocess.lenstexture_uniforms[4]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/LenstextureSetNode.hx b/Sources/armory/logicnode/LenstextureSetNode.hx new file mode 100644 index 0000000000..94d1bac15e --- /dev/null +++ b/Sources/armory/logicnode/LenstextureSetNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class LenstextureSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + armory.renderpath.Postprocess.lenstexture_uniforms[0] = inputs[1].get(); + armory.renderpath.Postprocess.lenstexture_uniforms[1] = inputs[2].get(); + armory.renderpath.Postprocess.lenstexture_uniforms[2] = inputs[3].get(); + armory.renderpath.Postprocess.lenstexture_uniforms[3] = inputs[4].get(); + armory.renderpath.Postprocess.lenstexture_uniforms[4] = inputs[5].get(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/LetterboxGetNode.hx b/Sources/armory/logicnode/LetterboxGetNode.hx new file mode 100644 index 0000000000..113328b394 --- /dev/null +++ b/Sources/armory/logicnode/LetterboxGetNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class LetterboxGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + return switch (from) { + case 0: armory.renderpath.Postprocess.letterbox_uniforms[0]; + case 1: armory.renderpath.Postprocess.letterbox_uniforms[1][0]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/LetterboxSetNode.hx b/Sources/armory/logicnode/LetterboxSetNode.hx new file mode 100644 index 0000000000..a22f2caf71 --- /dev/null +++ b/Sources/armory/logicnode/LetterboxSetNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class LetterboxSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + armory.renderpath.Postprocess.letterbox_uniforms[0][0] = inputs[1].get().x; + armory.renderpath.Postprocess.letterbox_uniforms[0][1] = inputs[1].get().y; + armory.renderpath.Postprocess.letterbox_uniforms[0][2] = inputs[1].get().z; + armory.renderpath.Postprocess.letterbox_uniforms[1][0] = inputs[2].get(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/LoadUrlNode.hx b/Sources/armory/logicnode/LoadUrlNode.hx new file mode 100644 index 0000000000..7bf8854994 --- /dev/null +++ b/Sources/armory/logicnode/LoadUrlNode.hx @@ -0,0 +1,16 @@ +// This node does not work with Krom. "Browser compilation only" node. + +package armory.logicnode; + +import kha.System; + +class LoadUrlNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + System.loadUrl(inputs[1].get()); + } +} diff --git a/Sources/armory/logicnode/LogicNode.hx b/Sources/armory/logicnode/LogicNode.hx new file mode 100644 index 0000000000..6c48fde39b --- /dev/null +++ b/Sources/armory/logicnode/LogicNode.hx @@ -0,0 +1,187 @@ +package armory.logicnode; + +#if arm_patch @:keep @:keepSub #end +class LogicNode { + + var tree: LogicTree; + var inputs: Array = []; + var outputs: Array> = []; + + #if (arm_debug || arm_patch) + public var name = ""; + + #if (arm_debug) + public function watch(b: Bool) { // Watch in debug console + var nodes = armory.trait.internal.DebugConsole.watchNodes; + b ? nodes.push(this) : nodes.remove(this); + } + #end + #end + + public function new(tree: LogicTree) { + this.tree = tree; + } + + /** + Resize the inputs array to a given size to minimize dynamic + reallocation and over-allocation later. + **/ + inline function preallocInputs(amount: Int) { + this.inputs.resize(amount); + } + + /** + Resize the outputs array to a given size to minimize dynamic + reallocation and over-allocation later. + **/ + inline function preallocOutputs(amount: Int) { + this.outputs.resize(amount); + for (i in 0...outputs.length) { + outputs[i] = []; + } + } + + /** + Add a link between to nodes to the tree. + **/ + public static function addLink(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int): LogicNodeLink { + var link = new LogicNodeLink(fromNode, toNode, fromIndex, toIndex); + + if (toNode.inputs.length <= toIndex) { + toNode.inputs.resize(toIndex + 1); + } + toNode.inputs[toIndex] = link; + + var fromNodeOuts = fromNode.outputs; + var outLen = fromNodeOuts.length; + if (outLen <= fromIndex) { + fromNodeOuts.resize(fromIndex + 1); + + // Initialize with empty arrays + for (i in outLen...fromIndex + 1) { + fromNodeOuts[i] = []; + } + } + fromNodeOuts[fromIndex].push(link); + + return link; + } + + #if arm_patch + /** + Removes a link from the tree. + **/ + static function removeLink(link: LogicNodeLink) { + link.fromNode.outputs[link.fromIndex].remove(link); + + // Reuse the same link and connect a default input node to it. + // That's why this function is only available in arm_patch mode, we need + // access to the link's type and value. + link.fromNode = LogicNode.createSocketDefaultNode(link.toNode.tree, link.toType, link.toValue); + link.fromIndex = 0; + } + + /** + Removes all inputs and their links from this node. + Warning: this function changes the amount of node inputs to 0! + **/ + function clearInputs() { + for (link in inputs) { + link.fromNode.outputs[link.fromIndex].remove(link); + } + inputs.resize(0); + } + + /** + Removes all outputs and their links from this node. + Warning: this function changes the amount of node inputs to 0! + **/ + function clearOutputs() { + for (links in outputs) { + for (link in links) { + var defaultNode = LogicNode.createSocketDefaultNode(tree, link.toType, link.toValue); + link.fromNode = defaultNode; + link.fromIndex = 0; + defaultNode.outputs[0] = [link]; + } + } + outputs.resize(0); + } + + /** + Creates a default node for a socket so that get() and set() can be + used without null checks. + Loosely equivalent to `make_logic.build_default_node()` in Python. + **/ + static inline function createSocketDefaultNode(tree: LogicTree, socketType: String, value: Dynamic): LogicNode { + // Make sure to not add these nodes to the LogicTree.nodes array as they + // won't be garbage collected then if unlinked later. + return switch (socketType) { + case "VECTOR": new armory.logicnode.VectorNode(tree, value[0], value[1], value[2]); + case "RGBA": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2], value[3]); + case "RGB": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2]); + case "VALUE": new armory.logicnode.FloatNode(tree, value); + case "INT": new armory.logicnode.IntegerNode(tree, value); + case "BOOLEAN": new armory.logicnode.BooleanNode(tree, value); + case "STRING": new armory.logicnode.StringNode(tree, value); + case "NONE": new armory.logicnode.NullNode(tree); + case "OBJECT": new armory.logicnode.ObjectNode(tree, value); + default: new armory.logicnode.DynamicNode(tree, value); + } + } + #end + + /** + Called when this node is activated. + @param from impulse index + **/ + function run(from: Int) {} + + /** + Call to activate node connected to the output. + @param i output index + **/ + function runOutput(i: Int) { + if (i >= outputs.length) return; + for (outLink in outputs[i]) { + outLink.toNode.run(outLink.toIndex); + } + } + + @:allow(armory.logicnode.LogicNodeLink) + function get(from: Int): Dynamic { return this; } + + @:allow(armory.logicnode.LogicNodeLink) + function set(value: Dynamic) {} +} + +@:allow(armory.logicnode.LogicNode) +@:allow(armory.logicnode.LogicTree) +class LogicNodeLink { + + var fromNode: LogicNode; + var toNode: LogicNode; + var fromIndex: Int; + var toIndex: Int; + + #if arm_patch + var fromType: String; + var toType: String; + var toValue: Dynamic; + #end + + inline function new(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int) { + this.fromNode = fromNode; + this.toNode = toNode; + this.fromIndex = fromIndex; + this.toIndex = toIndex; + } + + inline function get(): Dynamic { + return fromNode.get(fromIndex); + } + + inline function set(value: Dynamic) { + fromNode.set(value); + } +} diff --git a/Sources/armory/logicnode/LogicTree.hx b/Sources/armory/logicnode/LogicTree.hx new file mode 100644 index 0000000000..104cd8bfa4 --- /dev/null +++ b/Sources/armory/logicnode/LogicTree.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +class LogicTree extends iron.Trait { + + #if arm_patch + /** + Stores all trait instances of the tree via its name. + **/ + public static var nodeTrees = new Map>(); + + /** + [node name => logic node] for later node replacement for live patching. + **/ + public var nodes: Map; + #end + + public var loopBreak = false; // Trigger break from loop nodes + public var loopContinue = false; // Trigger Continue from loop nodes + + public function new() { + super(); + + #if arm_patch + nodes = new Map(); + #end + } + + public function add() {} + + public var paused = false; + + public function pause() { + if (paused) return; + paused = true; + + if (_update != null) for (f in _update) iron.App.removeUpdate(f); + if (_lateUpdate != null) for (f in _lateUpdate) iron.App.removeLateUpdate(f); + } + + public function resume() { + if (!paused) return; + paused = false; + + if (_update != null) for (f in _update) iron.App.notifyOnUpdate(f); + if (_lateUpdate != null) for (f in _lateUpdate) iron.App.notifyOnLateUpdate(f); + } +} diff --git a/Sources/armory/logicnode/LookAtNode.hx b/Sources/armory/logicnode/LookAtNode.hx new file mode 100644 index 0000000000..537e7c3950 --- /dev/null +++ b/Sources/armory/logicnode/LookAtNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.Quat; + +class LookAtNode extends LogicNode { + + public var property0: String; + var v1 = new Vec4(); + var v2 = new Vec4(); + var q = new Quat(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vfrom: Vec4 = inputs[0].get(); + var vto: Vec4 = inputs[1].get(); + + if (vfrom == null || vto == null) return null; + + switch (property0) { + case "X": + v1.set(1, 0, 0); + case "-X": + v1.set(-1, 0, 0); + case "Y": + v1.set(0, 1, 0); + case "-Y": + v1.set(0, -1, 0); + case "Z": + v1.set(0, 0, 1); + case "-Z": + v1.set(0, 0, -1); + } + v2.setFrom(vto).sub(vfrom).normalize(); + + q.fromTo(v1, v2); + return q; + } +} diff --git a/Sources/armory/logicnode/LoopBreakNode.hx b/Sources/armory/logicnode/LoopBreakNode.hx new file mode 100644 index 0000000000..d2cccd8eb9 --- /dev/null +++ b/Sources/armory/logicnode/LoopBreakNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class LoopBreakNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + tree.loopBreak = true; + } +} diff --git a/Sources/armory/logicnode/LoopContinueNode.hx b/Sources/armory/logicnode/LoopContinueNode.hx new file mode 100644 index 0000000000..b48fdf0c3a --- /dev/null +++ b/Sources/armory/logicnode/LoopContinueNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class LoopContinueNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + tree.loopContinue = true; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/LoopNode.hx b/Sources/armory/logicnode/LoopNode.hx new file mode 100644 index 0000000000..0fae23fafb --- /dev/null +++ b/Sources/armory/logicnode/LoopNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +class LoopNode extends LogicNode { + + var index: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + index = 0; + var from: Int = inputs[1].get(); + var to: Int = inputs[2].get(); + for (i in from...to) { + index = i; + runOutput(0); + + if (tree.loopBreak) { + tree.loopBreak = false; + break; + } + + if (tree.loopContinue) { + tree.loopContinue = false; + continue; + } + } + runOutput(2); + } + + override function get(from: Int): Dynamic { + return index; + } +} diff --git a/Sources/armory/logicnode/MapKeyExistsNode.hx b/Sources/armory/logicnode/MapKeyExistsNode.hx new file mode 100644 index 0000000000..193cafeae0 --- /dev/null +++ b/Sources/armory/logicnode/MapKeyExistsNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + + +class MapKeyExistsNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from: Int) { + var map: Map = inputs[1].get(); + if (map == null) return null; + + var key: Dynamic = inputs[2].get(); + + if (map.exists(key)) { + runOutput(0); + return; + } else { + runOutput(1); + return; + } + } + +} diff --git a/Sources/armory/logicnode/MapLoopNode.hx b/Sources/armory/logicnode/MapLoopNode.hx new file mode 100644 index 0000000000..3e66c27792 --- /dev/null +++ b/Sources/armory/logicnode/MapLoopNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; +import iron.math.Vec4; + + +class MapLoopNode extends LogicNode { + public var key: Dynamic; + public var value: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var map: Map = inputs[1].get(); + if (map == null) return; + //var keys:Array = Reflect.fields(map); + for (k in map.keys()) { + key = k; + value = map[k]; + runOutput(0); + + if (tree.loopBreak) { + tree.loopBreak = false; + break; + } + + if (tree.loopContinue) { + tree.loopContinue = false; + continue; + } + } + runOutput(3); + } + + override function get(from: Int): Dynamic { + if (from == 1) + return key; + return value; + } +} diff --git a/Sources/armory/logicnode/MapRangeNode.hx b/Sources/armory/logicnode/MapRangeNode.hx new file mode 100644 index 0000000000..23d7aa7938 --- /dev/null +++ b/Sources/armory/logicnode/MapRangeNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class MapRangeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): kha.FastFloat { + var value = inputs[0].get(); + var fromMin = inputs[1].get(); + var fromMax = inputs[2].get(); + var toMin = inputs[3].get(); + var toMax = inputs[4].get(); + + if (value == null || fromMin == null || fromMax == null || toMin == null || toMax == null) return null; + + //Implements https://stackoverflow.com/a/5732390 + var slope = (toMax - toMin) / (fromMax - fromMin); + return toMin + slope * (value - fromMin); + } +} diff --git a/Sources/armory/logicnode/MaskNode.hx b/Sources/armory/logicnode/MaskNode.hx new file mode 100644 index 0000000000..763536983d --- /dev/null +++ b/Sources/armory/logicnode/MaskNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +class MaskNode extends LogicNode { + + public var value: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var ret = 0; + for (v in 0...20) { + var bit: Bool = inputs[v].get(); + if (bit) ret |= (1 << v); + } + return ret; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/MaterialNode.hx b/Sources/armory/logicnode/MaterialNode.hx new file mode 100644 index 0000000000..b891411247 --- /dev/null +++ b/Sources/armory/logicnode/MaterialNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.data.MaterialData; + +class MaterialNode extends LogicNode { + + public var property0: String; + public var value: MaterialData = null; + + public function new(tree: LogicTree) { + super(tree); + + iron.Scene.active.notifyOnInit(function() { + get(0); + }); + } + + override function get(from: Int): Dynamic { + if (property0 != null) { + iron.data.Data.getMaterial(iron.Scene.active.raw.name, property0, function(mat: MaterialData) { + value = mat; + }); + } + + return value; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/MathExpressionNode.hx b/Sources/armory/logicnode/MathExpressionNode.hx new file mode 100644 index 0000000000..b2c4e39644 --- /dev/null +++ b/Sources/armory/logicnode/MathExpressionNode.hx @@ -0,0 +1,1914 @@ +package armory.logicnode; +import haxe.io.Bytes; +import haxe.io.BytesInput; +import haxe.io.BytesOutput; + +/** + * extending TermNode with various math operations transformation and more. + * by Sylvio Sell, Rostock 2017 + * + **/ + +class TermTransform { + + static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; + static var newValue:Float->TermNode = TermNode.newValue; + + /* + * Simplify: trims the length of a math expression + * + */ + static public inline function simplify(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _simplify(tnew); + return tnew; + } + + static inline function _simplify(t:TermNode):Void { + _expand(t); + + var len:Int = -1; + var len_old:Int = 0; + while (len != len_old) { + if (t.isName && t.left != null) { + simplifyStep(t.left); + } + else { + simplifyStep(t); + } + len_old = len; + len = t.length(); + } + } + + // TODO: removing this calls in subfunctions could be better to understand algorithms-recursions !!! + static function isEqualAfterSimplify(t1:TermNode, t2:TermNode):Bool { + // ----> take care, _simplify changes both TermNodes on call !!! + _simplify(t1); + _simplify(t2); + return t1.isEqual(t2, false, true); + } + + static function simplifyStep(t:TermNode):Void { + if (!t.isOperation) return; + + if (t.left != null) { + if (t.left.isValue) { + if (t.right == null) { + // setValue(result); // calculate operation with one value + return; + } + else if (t.right.isValue) { + t.setValue(t.result); // calculate result of operation with values on both sides + return; + } + } + } + + switch(t.symbol) { + case '+': + if (t.left.isValue && t.left.value == 0) t.copyNodeFrom(t.right); // 0+a -> a + else if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a+0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)+ln(b) -> ln(a*b) + t.setOperation('ln', + newOperation('*', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b+c/b -> (a+c)/b + newOperation('+', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { // a/b+c/d -> (a*d+c*b)/(b*d) + t.setOperation('/', + newOperation('+', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '+') { + _factorize(t); + } + + case '-': + if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a-0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)-ln(b) -> ln(a/b) + t.setOperation('ln', + newOperation('/', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b-c/b -> (a-c)/b + newOperation('-', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { //a/b-c/d -> (a*d-c*b)/(b*d) + t.setOperation('/', + newOperation('-', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '-') { + _factorize(t); + } + + case '*': + if (t.left.isValue) { + if (t.left.value == 1) t.copyNodeFrom(t.right); // 1*a -> a + else if (t.left.value == 0) t.setValue(0); // 0*a -> 0 + } + else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a*1 -> a + else if (t.right.value == 0) t.setValue(0); // a*0 -> a + } + else if (t.left.symbol == '/') { // (a/b)*c -> (a*c)/b + t.setOperation('/', + newOperation('*', t.right.copy(), t.left.left.copy()), + t.left.right.copy() + ); + } + else if (t.right.symbol == '/') { // a*(b/c) -> (a*b)/c + t.setOperation('/', + newOperation('*', t.left.copy(), t.right.left.copy()), + t.right.right.copy() + ); + } + else { + arrangeMultiplication(t); + } + + case '/': + if (isEqualAfterSimplify(t.left, t.right)) { // x/x -> 1 + t.setValue(1); + } + else { + if (t.left.isValue && t.left.value == 0) t.setValue(0); // 0/a -> 0 + else if (t.right.symbol == '/') { + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.right.isValue && t.right.value == 1) t.copyNodeFrom(t.left); // a/1 -> a + else if (t.left.symbol == '/') { // (1/x)/b -> 1/(x*b) + t.setOperation('/', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } + else if (t.right.symbol == '/') { // b/(1/x) -> b*x + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.left.symbol == '-' && t.left.left.isValue && t.left.left.value == 0) + { + t.setOperation('-', newValue(0), + newOperation('/', t.left.right.copy(), t.right.copy()) + ); + } + else{ // a*b/b -> a + simplifyfraction(t); + } + } + + case '^': + if (t.left.isValue) { + if (t.left.value == 1) t.setValue(1); // 1^a -> 1 + else if (t.left.value == 0) t.setValue(0); // 0^a -> 0 + } else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a^1 -> a + else if (t.right.value == 0) t.setValue(1); // a^0 -> 1 + } + else if (t.left.symbol == '^') { // (a^b)^c -> a^(b*c) + t.setOperation('^', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } + + case 'ln': + if (t.left.symbol == 'e') t.setValue(1); + case 'log': + if (isEqualAfterSimplify(t.left, t.right)) { + t.setValue(1); + } + else { + t.setOperation('/', // log(a,b) -> ln(b)/ln(a) + newOperation('ln', t.right.copy()), + newOperation('ln', t.left.copy()) + ); + } + } + if (t.left != null) simplifyStep(t.left); + if (t.right != null) simplifyStep(t.right); + } + + /* + * put all subterms separated by * into an array + * + */ + static function traverseMultiplication(t:TermNode, p:Array) + { + if (t.symbol != "*") { + p.push(t); + } + else { + traverseMultiplication(t.left, p); + traverseMultiplication(t.right, p); + } + } + + /* + * build tree consisting of multiple * from array + * + */ + static function traverseMultiplicationBack(t:TermNode, p:Array) + { + if (p.length > 2) { + t.setOperation('*', newValue(1), p.pop()); + traverseMultiplicationBack(t.left, p); + } + else if (p.length == 2) { + t.setOperation('*', p[0].copy(), p[1].copy()); + p.pop(); + p.pop(); + } + else { + t.set(p.pop()); + } + } + + /* + * put all subterms separated by * into an array + * + */ + static function traverseAddition(t:TermNode, p:Array, ?negative:Bool=false) + { + if (t.symbol == "+" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p); + } + else if (t.symbol == "-" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p, true); + } + else if (t.symbol == "+" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p, true); + } + else if (t.symbol == "-" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p); + } + else if (negative == true && !t.isValue || negative == true && t.isValue && t.value != 0) { + p.push(newOperation('-', newValue(0), t)); + } + else if (!t.isValue || t.isValue && t.value != 0) { + p.push(t); + } + return(p); + } + + /* + * build tree consisting of multiple - and + from array + * + */ + static function traverseAdditionBack(t:TermNode, p:Array) + { + if(p.length > 1) { + if (p[p.length-1].symbol == "-") { + t.set(p.pop()); + } + else { + t.setOperation("+", newValue(0), p.pop()); + } + traverseAdditionBack(t.left, p); + } + else if(p.length == 1){ + t.set(p.pop()); + } + } + + /* + * reduce a fraction + * + */ + static function simplifyfraction(t:TermNode) + { + var numerator:Array = new Array(); + traverseMultiplication(t.left, numerator); + var denominator:Array = new Array(); + traverseMultiplication(t.right, denominator); + for (n in numerator) { + for (d in denominator) { + if (isEqualAfterSimplify(n, d)) { + numerator.remove(n); + denominator.remove(d); + } + } + } + if (numerator.length > 1) { + traverseMultiplicationBack(t.left, numerator); + } + else if (numerator.length == 1) { + t.setOperation('/', numerator.pop(), newValue(1)); + } + else if (numerator.length == 0) { + t.left.setValue(1); + } + if (denominator.length > 1) { + traverseMultiplicationBack(t.right, denominator); + } + else if (denominator.length == 1) { + t.setOperation('/', t.left.copy(), denominator.pop()); + } + else if (denominator.length == 0) { + t.right.setValue(1); + } + } + + /* + * expands a mathmatical expression recursivly into a polynomial + * + */ + static public function expand(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _expand(tnew); + return tnew; + } + + static function _expand(t:TermNode):Void { + + var len:Int = -1; + var len_old:Int = 0; + while(len != len_old) { + if (t.symbol == '*') { + expandStep(t); + } + else { + if(t.left != null) { + _expand(t.left); + } + if(t.right != null) { + _expand(t.right); + + } + } + len_old = len; + len = t.length(); + } + } + + /* + * expands a mathmatical expression into a polynomial -> use only if top symbol=* + * + */ + static function expandStep(t:TermNode):Void + { + var left:TermNode = t.left; + var right:TermNode = t.right; + + if (left.symbol == "+" || left.symbol == "-") { + if (right.symbol == "+" || right.symbol == "-") { + if (left.symbol == "+" && right.symbol == "+") { // (a+b)*(c+d) + t.setOperation('+', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "+" && right.symbol == "-") { // (a+b)*(c-d) + t.setOperation('+', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "+") { // (a-b)*(c+d) + t.setOperation('-', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "-") { // (a-b)*(c-d) + t.setOperation('-', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + } + else + { + if (left.symbol == "+") { // (a+b)*c + t.setOperation('+', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } + else if (left.symbol == "-") { // (a-b)*c + t.setOperation('-', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } + } + } + else if (right.symbol == "+" || right.symbol == "-") { + if (right.symbol == "+") { // a*(b+c) + t.setOperation('+', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); + } + else if (right.symbol == "-") { // a*(b-c) + t.setOperation('-', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); + } + } + } + + /* + * factorize a term: a*c+a*b -> a*(c+b) + * + */ + static public function factorize(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _factorize(tnew); + return tnew; + } + + static function _factorize(t:TermNode):Void { + var mult_matrix:Array> = new Array(); + var add:Array = new Array(); + + // build matrix - addition in columns - multiplication in rows + traverseAddition(t, add); + var add_length_old:Int = 0; + for(i in add) { + if(i.symbol == "-") { + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1].right, mult_matrix[mult_matrix.length-1]); + } + else { + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1], mult_matrix[mult_matrix.length-1]); + } + } + + // find and extract common factors + var part_of_all:Array = new Array(); + factorize_extract_common(mult_matrix, part_of_all); + if(part_of_all.length != 0) { + var new_add:Array = new Array(); + var helper:TermNode = new TermNode(); + for(i in mult_matrix) { + traverseMultiplicationBack(helper, i); + var v:TermNode = new TermNode(); + v.set(helper); + new_add.push(v); + } + for(i in 0...add.length) { + if(add[i].symbol == '-' && add[i].left.value == 0) { + new_add[i].setOperation('-', newValue(0), new_add[i].copy()); + } + } + + t.setOperation('*', new TermNode(), new TermNode()); + traverseMultiplicationBack(t.left, part_of_all); + traverseAdditionBack(t.right, new_add); + } + } + + // delete common factors of mult_matrix and add them to part_of_all + static function factorize_extract_common(mult_matrix:Array>, part_of_all:Array):Void { + var bool:Bool = false; + var matrix_length_old:Int = -1; + var i:TermNode=new TermNode(); + var exponentiation_counter:Int = 0; + while(matrix_length_old != mult_matrix[0].length) { + matrix_length_old = mult_matrix[0].length; + for(p in mult_matrix[0]) { + if(p.symbol == '^') { + i.set(p.left); + exponentiation_counter++; + } + else if(p.symbol == '-' && p.left.isValue && p.left.value == 0) { + i.set(p.right); + } + else { + i.set(p); + } + for(j in 1...mult_matrix.length) { + bool = false; + for(h in mult_matrix[j]) { + if(isEqualAfterSimplify(h, i)) { + bool = true; + break; + } + else if(h.symbol == '^' && isEqualAfterSimplify(h.left , i)) { + bool=true; + exponentiation_counter++; + break; + + } + else if(h.symbol == '-' && h.left.isValue && h.left.value == 0 && isEqualAfterSimplify(h.right, i)) { + bool=true; + break; + } + } + if(bool == false) { + break; + } + } + if(bool == true && exponentiation_counter < mult_matrix.length) { + part_of_all.push(new TermNode()); + part_of_all[part_of_all.length-1].set(i); + var helper:TermNode = new TermNode(); + helper.set(i); + delete_last_from_matrix(mult_matrix, helper); + break; + } + } + } + } + + // deletes d from every row in mult_matrix once + static function delete_last_from_matrix(mult_matrix:Array>, d:TermNode):Void { + for(i in mult_matrix) { + if(i.length>1) { + for(j in 1...i.length+1) { + if(isEqualAfterSimplify(i[i.length-j], d)) { // a*x -> a + for(h in 0...j-1) { + i[i.length-j+h].set(i[i.length-j+h+1]); + } + i.pop(); + break; + } + else if(i[i.length-j].symbol == '^' && isEqualAfterSimplify(i[i.length-j].left, d)) { // x^n -> x^(n-1) + i[i.length-j].right.set(newOperation('-', i[i.length-j].right.copy(), newValue(1))); + break; + } + else if(i[i.length-j].symbol == '-' && i[i.length-j].left.isValue && i[i.length-j].left.value == 0 && isEqualAfterSimplify(i[i.length-j].right, d)) { + i[i.length-j].right.set(newValue(1)); + break; + } + } + } + else if(i[0].symbol == '^' && isEqualAfterSimplify(i[0].left, d)) { // x^n -> x^(n-1) + i[0].right.set(newOperation('-', i[0].right.copy(), newValue(1))); + } + else { + i[0].set(newValue(1)); + } + } + } + + // compare function for Array.sort() + static function formsort_compare(t1:TermNode, t2:TermNode):Int + { + if (formsort_priority(t1) > formsort_priority(t2)) { + return -1; + } + else if (formsort_priority(t1) < formsort_priority(t2)) { + return 1; + } + else{ + if (t1.isValue && t2.isValue) { + if (t1.value >= t2.value) { + return(-1); + } + else{ + return(1); + } + } + else if (t1.isOperation && t2.isOperation) { + if(t1.right != null && t2.right != null) { + return(formsort_compare(t1.right, t2.right)); + } + else { + return(formsort_compare(t1.left, t2.left)); + } + } + else return 0; + } + } + + // priority function for formsort_compare() + static function formsort_priority(t:TermNode):Float + { + return switch(t.symbol) + { + case s if (t.isParam): t.symbol.charCodeAt(0); + case s if (t.isName): t.symbol.charCodeAt(0); + case s if (t.isValue): 1+0.00001*t.value; + case s if (TermNode.twoSideOpRegFull.match(s)) : + if(t.symbol == '-' && t.left.value == 0) { + formsort_priority(t.right); + } + else { + formsort_priority(t.left)+formsort_priority(t.right)*0.001; + } + case s if (TermNode.oneParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.indexOf(s); + case s if (TermNode.twoParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.indexOf(s); + case s if (TermNode.constantOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.indexOf(s); + + default: -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.length; + } + } + + /* + * sort a Tree consisting of products + * + */ + static function arrangeMultiplication(t:TermNode):Void + { + var mult:Array = new Array(); + traverseMultiplication(t, mult); + mult.sort(formsort_compare); + traverseMultiplicationBack(t, mult); + } + + /* + * sort a Tree consisting of addition and subtraction + * + */ + static function arrangeAddition(t:TermNode):Void + { + var addlength_old:Int = -1; + var add:Array = new Array(); + traverseAddition(t, add); + add.sort(formsort_compare); + while(add.length != addlength_old) { + addlength_old = add.length; + for(i in 0...add.length-1) { + if(isEqualAfterSimplify(add[i], add[i+1])) { + add[i].setOperation('*', add[i].copy(), newValue(2)); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].symbol == '*' && add[i+1].symbol == '*' && add[i].right.isValue && add[i+1].right.isValue && isEqualAfterSimplify(add[i].left, add[i+1].left)) { + add[i].right.setValue(add[i].right.value+add[i+1].right.value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].isValue && add[i+1].isValue) { + add[i].setValue(add[i].value+add[i+1].value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if((add[i].symbol == '-' && add[i].left.isValue && add[i].left.value == 0 && isEqualAfterSimplify(add[i].right, add[i+1])) || (add[i+1].symbol == '-' && add[i+1].left.isValue && add[i+1].left.value == 0 && isEqualAfterSimplify(add[i+1].right, add[i]))) { + for(j in 0...add.length-i-2) { + add[i+j] = add[i+j+2]; + } + add.pop(); + add.pop(); + if(add.length == 0){ + add.push(newValue(0)); + } + break; + } + } + + if(add[0].symbol == '-' && add[0].left.value == 0) { + for(i in add) { + if(i.symbol == '-' && i.left.value == 0) { + i.set(i.right); + } + else { + i.setOperation('-', newValue(0), i.copy()); + } + } + t.setOperation('-', newValue(0), new TermNode()); + traverseAdditionBack(t.right, add); + return; + } + + } + traverseAdditionBack(t, add); + } +} + +/** + * symbolic derivation + * by Sylvio Sell, Rostock 2017 + * + **/ + +class TermDerivate { + + static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; + static var newValue:Float->TermNode = TermNode.newValue; + + /* + * creates a new term that is derivate of a given term + * + */ + static public inline function derivate(t:TermNode, p:String):TermNode { + return switch (t.symbol) + { + case s if (t.isName): TermNode.newName( t.symbol, derivate(t.left, p) ); + case s if (t.isValue || TermNode.constantOpRegFull.match(s)): newValue(0); + case s if (t.isParam): (t.symbol == p) ? newValue(1) : newValue(0); + case '+' | '-': + newOperation(t.symbol, derivate(t.left, p), derivate(t.right, p)); + case '*': + newOperation('+', + newOperation('*', derivate(t.left, p), t.right.copy()), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ); + case '/': + newOperation('/', + newOperation('-', + newOperation('*', derivate(t.left, p), t.right.copy()), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ), + newOperation('^', t.right.copy(), newValue(2) ) + ); + case '^': + if (t.left.symbol == 'e') + newOperation('*', derivate(t.right, p), + newOperation('^', newOperation('e'), t.left.copy()) + ); + else + newOperation('*', + newOperation('^', t.left.copy(), t.right.copy()), + newOperation('*', + t.right.copy(), + newOperation('ln', t.left.copy()) + ).derivate(p) + ); + case 'sin': + newOperation('*', derivate(t.left, p), + newOperation('cos', t.left.copy()) + ); + case 'cos': + newOperation('*', derivate(t.left, p), + newOperation('-', newValue(0), + newOperation('sin', t.left.copy() ) + ) + ); + case 'tan': + newOperation('*', derivate(t.left, p), + newOperation('+', newValue(1), + newOperation('^', + newOperation('tan', t.left.copy() ), + newValue(2) + ) + ) + ); + case 'cot': + newOperation('/', + newValue(1), + newOperation('tan', t.left.copy()) + ).derivate(p); + case 'atan': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), + newOperation('+', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ) + ) + ); + case 'atan2': + newOperation('/', + newOperation('-', + newOperation('*', t.right.copy(), derivate(t.left, p)), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ), + newOperation('+', + newOperation('*', t.left.copy(), t.left.copy()), + newOperation('*', t.right.copy(), t.right.copy()) + ) + ); + case 'asin': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), + newOperation('^', + newOperation('-', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ), newOperation('/', newValue(1), newValue(2)) + ) + ) + ); + case 'acos': + newOperation('*', derivate(t.left, p), + newOperation('-', newValue(0), + newOperation('/', newValue(1), + newOperation('^', + newOperation('-', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ), newOperation('/', newValue(1), newValue(2)) + ) + ) + ) + ); + case 'log': + newOperation('/', + newOperation('ln', t.right.copy()), + newOperation('ln', t.left.copy()) + ).derivate(p); + case 'ln': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), t.left.copy()) + ); + case 'abs': + newOperation('*', derivate(t.left, p), + newOperation('/', t.left.copy(), newOperation('abs', t.left.copy()) ) + ); + + default: throw('derivation of "${t.symbol}" not implemented'); + } + + } +} + +/** + * knot of a Tree to do math operations at runtime + * by Sylvio Sell, Rostock 2017 + * + **/ + +typedef OperationNode = {symbol:String, left:TermNode, right:TermNode, leftOperation:OperationNode, rightOperation:OperationNode, precedence:Int}; + +class TermNode { + + /* + * Properties + * + */ + var operation:TermNode->Float; // operation function pointer + public var symbol:String; //operator like "+" or parameter name like "x" + + public var left:TermNode; // left branch of tree + public var right:TermNode; // right branch of tree + + public var value:Float; // leaf of the tree + + public var name(get, set):String; // name is stored into a param-TermNode at root of the tree + inline function get_name():String return (isName) ? symbol : null; + public static inline function checkValidName(name:String) if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); + inline function set_name(name:String):String { + if (name == null && isName) { + copyNodeFrom(left); + } + else { + //if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); + checkValidName(name); + if (isName) symbol = name else setName(name, copyNode()); + } + return name; + } + + /* + * returns depth of parameter bindings + * + */ + public inline function depth():Int { + if (isName && left != null) return left._depth(); + else return _depth(); + } + public inline function _depth():Int { + var l:Int = 0; + var r:Int = 0; + var d:Int = 0; + if (isParam) { + if (left == null) d = 0; + else if (!left.isName) d = 1; + } + else if (isName) d = 1; + + if (left != null) l = left._depth(); + if (right != null) r = right._depth(); + + return( d + ((l>r) ? l : r)); + } + + + /* + * Check Type of TermNode + * + */ + public var isName(get, null):Bool; // true -> root TermNode that holds name + inline function get_isName():Bool return Reflect.compareMethods(operation, opName); + + public var isParam(get, null):Bool; // true -> it's a parameter + inline function get_isParam():Bool return Reflect.compareMethods(operation, opParam); + + public var isValue(get, null):Bool; // true -> it's a value (no left and right) + inline function get_isValue():Bool return Reflect.compareMethods(operation, opValue); + + public var isOperation(get, null):Bool; // true -> it's a operation TermNode + inline function get_isOperation():Bool return !(isName||isParam||isValue); + + /* + * Calculates result of all Operations + * throws error if there is unbind param + */ + public var result(get, null):Float; // result of tree calculation + inline function get_result():Float return operation(this); + + /* + * Constructors + * + */ + public function new() {} + + public static inline function newName(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setName(name, term); + return t; + } + + public static inline function newParam(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setParam(name, term); + return t; + } + + public static inline function newValue(f:Float):TermNode { + var t:TermNode = new TermNode(); + t.setValue(f); + return t; + } + + public static inline function newOperation(s:String, ?left:TermNode, ?right:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setOperation(s, left, right); + return t; + } + + + /* + * atomic methods + * + */ + public inline function set(term:TermNode):TermNode { + // TODO: new param to keep the existing bindings if there is same parameters + if (isName) { + if (!term.isName) left = term.copy(); + else if (term.left != null) left = term.left.copy(); + else left = null; + } + else { + if (!term.isName) copyNodeFrom(term.copy()); + else if (term.left != null) copyNodeFrom(term.left.copy()); + //else return null; // TODO: check if that can ever been! + } + return this; + } + + public inline function setName(name:String, ?term:TermNode) { + operation = opName; + symbol = name; + left = term; right = null; + } + + public inline function setParam(name:String, ?term:TermNode) { + operation = opParam; + symbol = name; + left = term; right = null; + } + + public inline function setValue(f:Float):Void { + operation = opValue; + symbol = null; + value = f; + left = null; right = null; + } + + public inline function setOperation(s:String, ?left:TermNode, ?right:TermNode):Void { + operation = MathOp.get(s); + if (operation != null) + { + symbol = s; + this.left = left; + this.right = right; + } + else throw ('"$s" is no valid operation.'); + } + + /* + * returns an array of parameter-names + * + */ + public inline function params():Array { + var ret:Array = new Array(); + if (isParam) { + ret.push(symbol); + } + else { + if (left != null ) { + for (i in left.params()) if (ret.indexOf(i) < 0) ret.push(i); + } + if (right != null) { + for (i in right.params()) if (ret.indexOf(i) < 0) ret.push(i); + } + } + return ret; + } + + /* + * check if term has a param + * + */ + public function hasParam(paramName:String):Bool { + if (isParam && symbol == paramName) return true; + if (left != null ) + if (left.hasParam(paramName)) return true; + if (right != null) + if (right.hasParam(paramName)) return true; + return false; + } + + + /* + * bind terms to parameters + * + */ + public inline function bind(params:Map):TermNode { + if (isParam) { + if (params.exists(symbol)) left = params.get(symbol); + } + else { + if (left != null) left.bind(params); + if (right != null) right.bind(params); + } + return this; + } + + + /* + * unbind terms that is bind to parameter-names + * + */ + public inline function unbind(params:Array):TermNode { + if (isParam) { + if (params.indexOf(symbol) >= 0) left = null; + } + else { + if (left != null) left.unbind(params); + if (right != null) right.unbind(params); + } + return this; + } + + + /* + * unbind terms + * + */ + public inline function unbindTerm(params:Array):TermNode { + if (isParam) { + if (left != null) { + if (params.indexOf(left) >= 0) left = null; + } + } + else { + if (left != null) left.unbindTerm(params); + if (right != null) right.unbindTerm(params); + } + return this; + } + + /* + * check if a term is binded to + * + */ + public function hasBinding(term:TermNode):Bool { + if (isParam && left == term) return true; + if (left != null) + if (left.hasBinding(term)) return true; + if (right != null) + if (right.hasBinding(term)) return true; + return false; + } + + /* + * unbind all terms that is bind to parameter-names + * + */ + public inline function unbindAll():TermNode { + if (isParam) left = null; + else { + if (left != null) left.unbindAll(); + if (right != null) right.unbindAll(); + } + return this; + } + + + /* + * returns a new Term where all bindings are resolved down to + * the specified depth + */ + public inline function resolveAll(depth:Int = -1):TermNode { + // TODO: check better way + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, (left!=null) ? left.resolveAll(depth) : null); + else if (isParam) { + if (left == null) return TermNode.newParam(symbol, left); + else if (depth == 0) + return (left.isName) ? left.left : left; + else if (depth > 0) + return (left.isName) ? left.left.copy(depth).resolveAll(depth-1) : left.copy(depth).resolveAll(depth - 1); + else + return (left.isName) ? left.left.resolveAll(depth - 1) : left.resolveAll(depth - 1); + } + else return TermNode.newOperation(symbol, (left!=null) ? left.resolveAll(depth) : null, (right!=null) ? right.resolveAll(depth) : null); + } + + /* + * returns a recursive copy by starting with this TermNode + * depth can be used to define how deep it should copy the param-linked formulas + * + */ + public function copy(depth:Int = -1):TermNode + { + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, (left!=null) ? left.copy(depth) : null); + else if (isParam) return TermNode.newParam(symbol, (left!=null) ? ((depth == 0) ? left : left.copy(depth-1)) : null); + else return TermNode.newOperation(symbol, (left!=null) ? left.copy(depth) : null, (right!=null) ? right.copy(depth) : null); + } + + /* + * returns a clone of this TermNode only + * + */ + function copyNode():TermNode + { + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, left); + else if (isParam) return TermNode.newParam(symbol, left); + else return TermNode.newOperation(symbol, left, right); + } + + /* + * copy all from other TermNode to this + * + */ + public inline function copyNodeFrom(t:TermNode):Void { + if (t.isValue) setValue(t.value); + else if (t.isName) setName(t.symbol, t.left); + else if (t.isParam) setParam(t.symbol, t.left); + else setOperation(t.symbol, t.left, t.right); + } + + + /* + * number of TermNodes inside Tree + * + */ + public function length(?depth:Null=null):Int { + if (depth == null) depth = -1; + return switch(symbol) { + case s if (isValue): 1; + case s if (isName): (left == null) ? 0 : left.length(depth); + case s if (isParam): (depth == 0 || left == null) ? 1 : left.length(depth-1); + case s if (constantOpRegFull.match(s)): 1; + case s if (oneParamOpRegFull.match(s)): 1 + left.length(depth); + default: 1 + left.length(depth) + right.length(depth); + } + } + + + /* + * returns true if other term is equal in data and structure + * + */ + public function isEqual(t:TermNode, ?compareNames=false, ?compareParams=false):Bool + { + if ( !compareNames && (isName || t.isName) ) { + if (isName && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isName && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isName && t.isName); + } + + if ( !compareParams && (isParam || t.isParam) ) { + if (isParam && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isParam && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isParam && t.isParam); + } + + var is_equal:Bool = false; + + if (isValue && t.isValue) + is_equal = (value==t.value); + else if ( (isName && t.isName) || (isParam && t.isParam) || (isOperation && t.isOperation) ) + is_equal = (symbol==t.symbol); + + if (left != null) { + if (t.left != null) is_equal = is_equal && left.isEqual(t.left, compareNames, compareParams); + else is_equal = false; + } + if (right != null) { + if (t.right != null) is_equal = is_equal && right.isEqual(t.right, compareNames, compareParams); + else is_equal = false; + } + + return is_equal; + } + + /* + * static Function Pointers (to stored in this.operation) + * + */ + static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else throw('Empty function "${t.symbol}".'); + static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else throw('Missing parameter "${t.symbol}".'); + static function opValue(t:TermNode):Float return t.value; + + static var MathOp:MapFloat> = [ + // two side operations + "+" => function(t) return t.left.result + t.right.result, + "-" => function(t) return t.left.result - t.right.result, + "*" => function(t) return t.left.result * t.right.result, + "/" => function(t) return t.left.result / t.right.result, + "^" => function(t) return Math.pow(t.left.result, t.right.result), + "%" => function(t) return t.left.result % t.right.result, + + // function without params (constants) + "e" => function(t) return Math.exp(1), + "pi" => function(t) return Math.PI, + + // function with one param + "abs" => function(t) return Math.abs(t.left.result), + "ln" => function(t) return Math.log(t.left.result), + "sin" => function(t) return Math.sin(t.left.result), + "cos" => function(t) return Math.cos(t.left.result), + "tan" => function(t) return Math.tan(t.left.result), + "cot" => function(t) return 1/Math.tan(t.left.result), + "asin" => function(t) return Math.asin(t.left.result), + "acos" => function(t) return Math.acos(t.left.result), + "atan" => function(t) return Math.atan(t.left.result), + + // function with two params + "atan2"=> function(t) return Math.atan2(t.left.result, t.right.result), + "log" => function(t) return Math.log(t.right.result) / Math.log(t.left.result), + "max" => function(t) return Math.max(t.left.result, t.right.result), + "min" => function(t) return Math.min(t.left.result, t.right.result), + ]; + + static var twoSideOp_ = "^,/,*,-,+,%"; // <- order here determines the operator precedence + static var constantOp_ = "e,pi"; // functions without parameters like "e() or pi()" + static var oneParamOp_ = "abs,ln,sin,cos,tan,cot,asin,acos,atan"; // functions with one parameter like "sin(2)" + static var twoParamOp_ = "atan2,log,max,min"; // functions with two parameters like "max(a,b)" + + static public var twoSideOp :Array = twoSideOp_.split(','); + static public var constantOp:Array = constantOp_.split(','); + static public var oneParamOp:Array = oneParamOp_.split(','); + static public var twoParamOp:Array = twoParamOp_.split(','); + + static var precedence:Map = [ for (i in 0...twoSideOp.length) twoSideOp[i] => i ]; + + + + /* + * Regular Expressions for parsing + * + */ + static var clearSpacesReg:EReg = ~/\s+/g; + static var trailingSpacesReg:EReg = ~/^(\s+)/; + + static var numberReg:EReg = ~/^([-+]?\d+\.?\d*)/; + static var paramReg:EReg = ~/^([a-z]+)/i; + + static var constantOpReg:EReg = new EReg("^(" + constantOp.join("|") + ")\\(\\)" , "i"); + static var oneParamOpReg:EReg = new EReg("^(" + oneParamOp.join("|") + ")\\(" , "i"); + static var twoParamOpReg:EReg = new EReg("^(" + twoParamOp.join("|") + ")\\(" , "i"); + static var twoSideOpReg: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")" , ""); + + static public var constantOpRegFull:EReg = new EReg("^(" + constantOp.join("|") + ")$" , "i"); + static public var oneParamOpRegFull:EReg = new EReg("^(" + oneParamOp.join("|") + ")$" , "i"); + static public var twoParamOpRegFull:EReg = new EReg("^(" + twoParamOp.join("|") + ")$" , "i"); + static public var twoSideOpRegFull: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")$" , ""); + + static var nameReg:EReg = ~/^([a-z]+)(\s*[:=]\s*)/i; + static var nameRegFull:EReg = ~/^([a-z]+)$/i; + + static var signReg:EReg = ~/^([-+\s]+)/i; + + public static inline function trailingSpaces(s:String):Int { + if (trailingSpacesReg.match(s)) return(trailingSpacesReg.matched(1).length); + else return 0; + } + /* + * Build Tree up from String Math Expression + * + */ + public static inline function fromString(s:String, ?bindings:Map):TermNode { + var errPos:Int = 0; + errPos = trailingSpaces(s); s = s.substr(errPos); + //s = clearSpacesReg.replace(s, ''); // clear all whitespaces + if (nameReg.match(s)) { + var name:String = nameReg.matched(1); + s = s.substr(name.length + nameReg.matched(2).length); + errPos += name.length + nameReg.matched(2).length; + if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); + return newName(name, parseString(s, errPos, bindings)); + } + if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); + return parseString(s, errPos, bindings); + } + + static function parseString(s:String, errPos:Int, ?params:Map):TermNode { + var t:TermNode = null; + var operations:Array = new Array(); + var e, f:String; + var negate:Bool; + var spaces:Int = 0; + + while (s.length != 0) // read in terms from left + { + negate = false; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (numberReg.match(s)) { // float number + e = numberReg.matched(1); + t = newValue(Std.parseFloat(e)); + } + else if (constantOpReg.match(s)) { // like e() or pi() + e = constantOpReg.matched(1); + t = newOperation(e); + e+= "()"; + } + else if (oneParamOpReg.match(s)) { // like sin(...) + f = oneParamOpReg.matched(1); errPos += f.length; + s = "("+oneParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + t = newOperation(f, parseString(e.substring(1, e.length - 1), errPos+1, params) ); + } + else if (twoParamOpReg.match(s)) { // like atan2(... , ...) + f = twoParamOpReg.matched(1); errPos += f.length; + s = "("+twoParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + var p1:String = e.substring(1, comataPos); + var p2:String = e.substring(comataPos + 1, e.length - 1); + if (comataPos == -1) throw({"msg":f+"() needs two parameter separated by comma.","pos":errPos}); + t = newOperation(f, parseString(p1, errPos+1, params), parseString(p2, errPos+1 + comataPos, params) ); + } + else if (paramReg.match(s)) { // parameter + e = paramReg.matched(1); + t = newParam(e, (params==null) ? null : params.get(e)); + } + else if (signReg.match(s)) { // start with +- + e = signReg.matched(1); + s = s.substr(e.length); errPos += e.length; + e = ~/[\s+]/g.replace(e, ''); + if (e.length % 2 > 0) { + //s = "0-" + s; + if (numberReg.match(s)) { // followed by float number + e = numberReg.matched(1); + t = newValue(-Std.parseFloat(e)); + } else { // negative signed + t = newValue(0); + s = "-" + s; + e = ""; + negate = true; + } + } else continue; // positive signed + } + else if (twoSideOpReg.match(s)) { // start with other two side op + throw({"msg":"Missing left operand.","pos":errPos}); + } + else { + e = getBrackets(s, errPos); // term inside brackets + t = parseString(e.substring(1, e.length - 1), errPos+1, params); + } + + s = s.substr(e.length); errPos += e.length; + + if (operations.length > 0) operations[operations.length - 1].right = t; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (twoSideOpReg.match(s)) { // two side operation symbol + e = twoSideOpReg.matched(1); errPos += e.length; + s = twoSideOpReg.matchedRight(); + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + operations.push( { symbol:e, left:t, right:null, leftOperation:null, rightOperation:null, precedence:((negate) ? -1 :precedence.get(e)) } ); + if (operations.length > 1) { + operations[operations.length - 2].rightOperation = operations[operations.length - 1]; + operations[operations.length - 1].leftOperation = operations[operations.length - 2]; + } + } else if (s.length > 0) { + if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.","pos":errPos}); + if (!(s.indexOf("(") == 0 || numberReg.match(s) || paramReg.match(s) || constantOpReg.match(s) || oneParamOpReg.match(s) || twoParamOpReg.match(s))) + throw({"msg":"Wrong char.","pos":errPos}); + else throw({"msg":"Missing operation.","pos":errPos}); + } + } + + if ( operations.length > 0 ) { + if ( operations[operations.length-1].right == null ) throw({"msg":"Missing right operand.","pos":errPos-spaces}); + else { + operations.sort(function(a:OperationNode, b:OperationNode):Int + { + if (a.precedence < b.precedence) return -1; + if (a.precedence > b.precedence) return 1; + return 0; + }); + for (op in operations) { + t = TermNode.newOperation(op.symbol, op.left, op.right); + if (op.leftOperation != null && op.rightOperation != null) { + op.rightOperation.leftOperation = op.leftOperation; + op.leftOperation.rightOperation = op.rightOperation; + } + if (op.leftOperation != null) op.leftOperation.right = t; + if (op.rightOperation != null) op.rightOperation.left = t; + } + return t; + } + } + else return t; + } + + static var comataPos:Int; + static function getBrackets(s:String, errPos:Int):String { + var pos:Int = 1; + if (s.indexOf("(") == 0) // check that s starts with opening bracket + { + if (~/^\(\s*\)/.match(s)) throw({"msg":"Empty brackets.", "pos":errPos}); + + var i,j,k:Int; + var openBrackets:Int = 1; + comataPos = -1; + while ( openBrackets > 0 ) + { + i = s.indexOf("(", pos); + j = s.indexOf(")", pos); + + // check for commata position + if (openBrackets == 1 && comataPos == -1) { + k = s.indexOf(",", pos); + if (k0) comataPos = k; + } + + if ((i>0 && j>0 && i0 && j<0)) { // found open bracket + openBrackets++; pos = i + 1; + } + else if ((j>0 && i>0 && j0 && i<0)) { // found close bracket + openBrackets--; pos = j + 1; + } else { // no close or open found + throw({"msg":"Wrong bracket nesting.","pos":errPos}); + } + } + return s.substring(0, pos); + } + if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.", "pos":errPos}); + else throw({"msg":"Wrong char.","pos":errPos}); + } + + + /* + * Puts out Math Expression as a String + * + */ + public function toString(?depth:Null = null, ?plOut:String = null):String { + var t:TermNode = this; + if (isName) t = left; + var options:Int; + switch (plOut) { + case 'glsl': options = noNeg|forceFloat|forcePow|forceMod|forceLog|forceAtan|forceConst; + default: options = 0; + } + return (left != null || !isName) ? t._toString(depth, options) : ''; + } + // options + public static inline var noNeg:Int = 1; + public static inline var forceFloat:Int = 2; + public static inline var forcePow:Int = 4; + public static inline var forceMod:Int = 8; + public static inline var forceLog:Int = 16; + public static inline var forceAtan:Int = 32; + public static inline var forceConst:Int = 64; + + inline function _toString(depth:Null, options:Int, ?isFirst:Bool=true):String { + if (depth == null) depth = -1; + return switch(symbol) { + case s if (isValue): floatToString(value, options); + //case s if (isName && isFirst): (left == null) ? symbol : left.toString(depth, false); + case s if (isName): (depth == 0 || left == null) ? symbol : left._toString(depth-1, options, false); + case s if (isParam): (depth == 0 || left == null) ? symbol : left._toString(depth-((left.isName)?0:1), options, false); + case s if (twoSideOpRegFull.match(s)) : + if (symbol == "-" && left.isValue && left.value == 0 && options&noNeg == 0) symbol + right._toString(depth, options, false); + else if (symbol == "^" && options&forcePow > 0) 'pow' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else if (symbol == "%" && options&forceMod > 0) 'mod' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else ((isFirst)?"":"(") + left._toString(depth, options, false) + symbol + right._toString(depth, options, false) + ((isFirst)?'':")"); + case s if (twoParamOpRegFull.match(s)): + if (symbol == "log" && options&forceLog > 0) "(log(" + right._toString(depth, options) + ")/log(" + left._toString(depth, options) + "))"; + else if (symbol == "atan2" && options&forceAtan > 0) "atan(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + case s if (constantOpRegFull.match(s)): + if (symbol == "pi" && options & forceConst > 0) Std.string(Math.PI); + else if (symbol == "e" && options&forceConst > 0) Std.string(Math.exp(1)); + else symbol + "()"; + default: + if (symbol == "ln" && options&forceLog > 0) 'log' + "(" + left._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + ")"; + } + } + + inline function floatToString(value:Float, ?options:Int = 0):String { + var s:String = Std.string(value); + if (options&forceFloat > 0 && s.indexOf('.') == -1) s += ".0"; + return s; + } + + /* + * enrolls terms and subterms for debugging + * + */ + public function debug() { + //TODO + var out:String = "";// "(" + depth() + ")"; + for (i in 0 ... depth()+1) { + if (i == 0) out += ((name != null) ? name : "?") + " = "; else out += " -> "; + out += toString(i); + } + trace(out); + } + + /* + * packs a TermNode and all sub-terms into Bytes + * + */ + public function toBytes():Bytes { + var b = new BytesOutput(); + _toBytes(b); + return b.getBytes(); + } + + inline function _toBytes(b:BytesOutput) { + // optimize (later to do): needs only 3 bit per TermNode type! + if (isValue) { + b.writeByte(0); + b.writeFloat(value); + } + else if (isName) { + b.writeByte((left!=null) ? 1:2); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); + } + else if (isParam) { + b.writeByte((left!=null) ? 3:4); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); + } + else if (isOperation) { + b.writeByte(5); + var i:Int = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp))).indexOf(symbol); + if (i > -1) { + b.writeByte(i); + if (oneParamOpRegFull.match(symbol)) left._toBytes(b); + else if (twoSideOpRegFull.match(symbol) || twoParamOpRegFull.match(symbol) ) { + left._toBytes(b); + right._toBytes(b); + } + } + else throw("Error in _toBytes"); + } + else throw("Error in _toBytes"); + } + + inline function _writeString(s:String, b:BytesOutput):Void { + b.writeByte((s.length<255) ? s.length: 255); + for (i in 0...((s.length<255) ? s.length: 255)) b.writeByte(symbol.charCodeAt(i)); + } + /* + * unserialize packed Bytes-Term to create a TermNode structure + * + */ + public static function fromBytes(b:Bytes):TermNode { + return _fromBytes(new BytesInput(b)); + } + + static inline function _fromBytes(b:BytesInput):TermNode { + return switch (b.readByte()) { + case 0: TermNode.newValue(b.readFloat()); + case 1: TermNode.newName( _readString(b), _fromBytes(b) ); + case 2: TermNode.newName( _readString(b) ); + case 3: TermNode.newParam( _readString(b), _fromBytes(b) ); + case 4: TermNode.newParam( _readString(b) ); + case 5: + var op:String = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp)))[b.readByte()]; + if (oneParamOpRegFull.match(op)) TermNode.newOperation( op, _fromBytes(b) ); + else if (twoSideOpRegFull.match(op) || twoParamOpRegFull.match(op) ) TermNode.newOperation( op, _fromBytes(b), _fromBytes(b) ); + else TermNode.newOperation( op ); + default: throw("Error in _fromBytes"); + } + } + + static inline function _readString(b:BytesInput):String { + var len:Int = b.readByte(); + var s:String = ""; + for (i in 0...len) s += String.fromCharCode(b.readByte()); + return s; + } + + + /************************************************************************************** + * * + * various math operations transformation and more. * + * * + * * + **************************************************************************************/ + + /* + * creates a new term that is derivate of a given term + * + */ + public function derivate(paramName:String):TermNode return TermDerivate.derivate(this, paramName); + + /* + * Simplify: trims the length of a math expression + * + */ + public function simplify():TermNode return TermTransform.simplify(this); + + /* + * expands a mathmatical expression recursivly into a polynomial + * + */ + public function expand():TermNode return TermTransform.expand(this); + + /* + * factorizes a mathmatical expression + * + */ + public function factorize():TermNode return TermTransform.factorize(this); +} + +/** + * abstract wrapper around TermNode + * + * by Sylvio Sell, Rostock 2017 + */ + + +@:forward( name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) +abstract Formula(TermNode) from TermNode to TermNode +{ + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula + @param formulaString the String that representing the math expression + **/ + inline public function new(formulaString:String) { + this = TermNode.fromString(formulaString); + } + + /** + Copy all from another Formula to this (keeps the own name if it is defined) + Keeps the bindings where this formula is linked into by a parameter. + @param formula the source formula from where the value is copyed + **/ + public inline function set(formula:Formula):Formula return this.set(formula); + + /** + Link a variable inside of this formula to another formula + @param formula formula that will be linked into + @param paramName (optional) name of the variable to link with (e.g. if formula have no or different name) + **/ + public function bind(formula:Formula, ?paramName:String):Formula { + if (paramName != null) { + TermNode.checkValidName(paramName); + return this.bind( [paramName => formula] ); + } + else { + if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; + return this.bind( [formula.name => formula] ); + } + } + + /** + Link variables inside of this formula to another formulas + @param formulas array of formulas to link to variables + @param paramNames (optional) names of the variables to link with (e.g. if formulas have no or different names) + **/ + public function bindArray(formulas:Array, ?paramNames:Array):Formula { + var map = new Map(); + if (paramNames != null) { + if (paramNames.length != formulas.length) throw 'paramNames need to have the same length as formulas for bindArray().'; + for (i in 0...formulas.length) { + TermNode.checkValidName(paramNames[i]); + map.set(paramNames[i], formulas[i]); + } + } + else { + for (formula in formulas) { + if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; + map.set(formula.name, formula); + } + } + return this.bind(map); + } + + /** + Link variables inside of this formula to another formulas + @param formulaMap map of formulas where the keys have same names as the variables to link with + **/ + public inline function bindMap(formulaMap:Map):Formula { + return this.bind(formulaMap); + } + + // ------------ unbind ------------- + + /** + Delete all connections of the linked formula + @param formula formula that has to be unlinked + **/ + public inline function unbind(formula:Formula):Formula { + return this.unbindTerm( [formula] ); + } + + /** + Delete all connections of the linked formulas + @param formulas array of formulas that has to be unlinked + **/ + public function unbindArray(formulas:Array):Formula { + return this.unbindTerm(formulas); + } + + /** + Delete all connections to linked formulas for a given variable name + @param paramName name of the variable where the connected formula has to unlink from + **/ + public inline function unbindParam(paramName:String):Formula { + TermNode.checkValidName(paramName); + return this.unbind( [paramName] ); + } + + /** + Delete all connections to linked formulas for the given variable names + @param paramNames array of variablenames where the connected formula has to unlink from + **/ + public inline function unbindParamArray(paramNames:Array):Formula { + return this.unbind(paramNames); + } + + // ----------------------------------- + + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula + @param depth (optional) how deep the variable-bindings should be resolved + @param plOut (optional) creates formula for a special language (only "glsl" at now) + **/ + inline public function toString(?depth:Null = null, ?plOut:String = null):String return this.toString(depth, plOut); + + /** + Creates a formula from a packet Bytes representation + **/ + inline public static function fromBytes(b:Bytes):Formula return TermNode.fromBytes(b); + + @:to inline public function toStr():String return this.toString(0); + @:to inline public function toFloat():Float return this.result; + + @:from static public function fromString(a:String):Formula return TermNode.fromString(a); + @:from static public function fromFloat(a:Float):Formula return TermNode.newValue(a); + + static inline function twoSideOp(op:String, a:Formula, b:Formula ):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a, + (b.name != null ) ? TermNode.newParam(b.name, b) : b + ); + } + @:op(A + B) static public function add (a:Formula, b:Formula):Formula return twoSideOp('+', a, b); + @:op(A - B) static public function subtract(a:Formula, b:Formula):Formula return twoSideOp('-', a, b); + @:op(A * B) static public function multiply(a:Formula, b:Formula):Formula return twoSideOp('*', a, b); + @:op(A / B) static public function divide (a:Formula, b:Formula):Formula return twoSideOp('/', a, b); + @:op(A ^ B) static public function potenz (a:Formula, b:Formula):Formula return twoSideOp('^', a, b); + @:op(A % B) static public function modulo (a:Formula, b:Formula):Formula return twoSideOp('%', a, b); + + public static inline function atan2(a:Formula, b:Formula):Formula return twoSideOp('atan2', a, b); + public static inline function log (a:Formula, b:Formula):Formula return twoSideOp('log', a, b); + public static inline function max (a:Formula, b:Formula):Formula return twoSideOp('max', a, b); + public static inline function min (a:Formula, b:Formula):Formula return twoSideOp('min', a, b); + + static inline function oneParamOp(op:String, a:Formula):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a + ); + } + public static inline function abs (a:Formula):Formula return oneParamOp('abs', a); + public static inline function ln (a:Formula):Formula return oneParamOp('ln', a); + public static inline function sin (a:Formula):Formula return oneParamOp('sin', a); + public static inline function cos (a:Formula):Formula return oneParamOp('cos', a); + public static inline function tan (a:Formula):Formula return oneParamOp('tan', a); + public static inline function cot (a:Formula):Formula return oneParamOp('cot', a); + public static inline function asin(a:Formula):Formula return oneParamOp('asin', a); + public static inline function acos(a:Formula):Formula return oneParamOp('acos', a); + public static inline function atan(a:Formula):Formula return oneParamOp('atan', a); + +} + +class MathExpressionNode extends LogicNode { + + public var property0: String; // Expression + public var property1: Bool; // Clamp + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var r: Float = 0.0; + var exp: String = property0; + // Variable + var a: Float = inputs[0].get(); + var b: Float = inputs[1].get(); + exp = StringTools.replace(exp, 'a', Std.string(a)); + exp = StringTools.replace(exp, 'b', Std.string(b)); + var c: Float = 0.0; + var d: Float = 0.0; + var e: Float = 0.0; + var x: Float = 0.0; + var y: Float = 0.0; + var h: Float = 0.0; + var i: Float = 0.0; + var k: Float = 0.0; + var i = 2; + while (i < inputs.length) { + switch (i) { + case 2: + c = inputs[i].get(); + exp = StringTools.replace(exp, 'c', Std.string(c)); + case 3: + d = inputs[i].get(); + exp = StringTools.replace(exp, 'd', Std.string(d)); + case 4: + e = inputs[i].get(); + exp = StringTools.replace(exp, 'e', Std.string(e)); + case 5: + x = inputs[i].get(); + exp = StringTools.replace(exp, 'x', Std.string(x)); + case 6: + y = inputs[i].get(); + exp = StringTools.replace(exp, 'y', Std.string(y)); + case 7: + h = inputs[i].get(); + exp = StringTools.replace(exp, 'h', Std.string(h)); + case 8: + i = inputs[i].get(); + exp = StringTools.replace(exp, 'i', Std.string(i)); + case 9: + k = inputs[i].get(); + exp = StringTools.replace(exp, 'k', Std.string(k)); + } + i++; + } + // Expression + try { + var f: Formula = new Formula(exp); + r = f.result; + } catch(msg: String) { + #if arm_debug + trace(msg); + #end + } + // Clamp + if (property1) r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r); + + return r; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/MathNode.hx b/Sources/armory/logicnode/MathNode.hx new file mode 100644 index 0000000000..d30db790aa --- /dev/null +++ b/Sources/armory/logicnode/MathNode.hx @@ -0,0 +1,107 @@ +package armory.logicnode; + +class MathNode extends LogicNode { + + public var property0: String; // Operation + public var property1: Bool; // Clamp + + public function new(tree: LogicTree) { + super(tree); + } + + public static function round(number:Float, ?precision=2): Float{ + precision = Math.round(Math.abs(precision)); + number *= Math.pow(10, precision); + return Math.round(number) / Math.pow(10, precision); + } + + public function fract(a: Float): Float { + return a - Math.floor(a); + } + + public function pingpong(a: Float, b: Float): Float { + if (b == 0.0) { + return 0.0; + } else { + return Math.abs(fract((a - b) / (b * 2.0)) * b * 2.0 - b); + } + } + + override function get(from: Int): Dynamic { + var r = 0.0; + switch (property0) { + case "Sine": + r = Math.sin(inputs[0].get()); + case "Cosine": + r = Math.cos(inputs[0].get()); + case "Abs": + r = Math.abs(inputs[0].get()); + case "Tangent": + r = Math.tan(inputs[0].get()); + case "Arcsine": + r = Math.asin(inputs[0].get()); + case "Arccosine": + r = Math.acos(inputs[0].get()); + case "Arctangent": + r = Math.atan(inputs[0].get()); + case "Logarithm": + r = Math.log(inputs[0].get()); + case "Round": + r = round(inputs[0].get(), inputs[1].get()); + case "Floor": + r = Math.floor(inputs[0].get()); + case "Ceil": + r = Math.ceil(inputs[0].get()); + case "Fract": + var v = inputs[0].get(); + r = v - Std.int(v); + case "Square Root": + r = Math.sqrt(inputs[0].get()); + case "Exp": + r = Math.exp(inputs[0].get()); + case "Max": + r = Math.max(inputs[0].get(), inputs[1].get()); + case "Min": + r = Math.min(inputs[0].get(), inputs[1].get()); + case "Power": + r = Math.pow(inputs[0].get(), inputs[1].get()); + case "Less Than": + r = inputs[0].get() < inputs[1].get() ? 1.0 : 0.0; + case "Greater Than": + r = inputs[0].get() > inputs[1].get() ? 1.0 : 0.0; + case "Modulo": + r = inputs[0].get() % inputs[1].get(); + case "Arctan2": + r = Math.atan2(inputs[0].get(), inputs[1].get()); + case "Add": + for (inp in inputs) r += inp.get(); + case "Multiply": + r = inputs[0].get(); + var i = 1; + while (i < inputs.length) { + r *= inputs[i].get(); + i++; + } + case "Subtract": + r = inputs[0].get(); + var i = 1; + while (i < inputs.length) { + r -= inputs[i].get(); + i++; + } + case "Divide": + r = inputs[0].get(); + var i = 1; + while (i < inputs.length) { + r /= inputs[i].get(); + i++; + } + case "Ping-Pong": + r = pingpong(inputs[0].get(), inputs[1].get()); + } + // Clamp + if (property1) r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r); + + return r; + } +} diff --git a/Sources/armory/logicnode/MatrixMathNode.hx b/Sources/armory/logicnode/MatrixMathNode.hx new file mode 100644 index 0000000000..d7f504f8a3 --- /dev/null +++ b/Sources/armory/logicnode/MatrixMathNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.math.Mat4; + +class MatrixMathNode extends LogicNode { + + public var property0: String; + var m = Mat4.identity(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var m1: Mat4 = inputs[0].get(); + var m2: Mat4 = inputs[1].get(); + + if (m1 == null || m2 == null) return null; + + m.setFrom(m1); + switch (property0) { + case "Multiply": + m.multmat(m2); + } + + return m; + } +} diff --git a/Sources/armory/logicnode/MergeNode.hx b/Sources/armory/logicnode/MergeNode.hx new file mode 100644 index 0000000000..c2a22e6a55 --- /dev/null +++ b/Sources/armory/logicnode/MergeNode.hx @@ -0,0 +1,32 @@ +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/MergedGamepadNode.hx b/Sources/armory/logicnode/MergedGamepadNode.hx new file mode 100644 index 0000000000..dcee3c5db5 --- /dev/null +++ b/Sources/armory/logicnode/MergedGamepadNode.hx @@ -0,0 +1,51 @@ +package armory.logicnode; + +class MergedGamepadNode extends LogicNode { + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + if (gamepad == null) return; + var b = false; + switch (property0) { + case "started": + b = gamepad.started(property1); + case "down": + b = gamepad.down(property1) > 0.0; + case "released": + b = gamepad.released(property1); + // case "Moved Left": + // b = gamepad.leftStick.movementX != 0 || gamepad.leftStick.movementY != 0; + // case "Moved Right": + // b = gamepad.rightStick.movementX != 0 || gamepad.rightStick.movementY != 0; + } + if (b) runOutput(0); + } + + override function get(from: Int): Dynamic { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + switch (property0) { + case "started": + return gamepad.started(property1); + case "down": + return gamepad.down(property1) > 0.0; + case "released": + return gamepad.released(property1); + // case "Moved Left": + // return (gamepad.leftStick.movementX != 0 || gamepad.leftStick.movementY != 0) ? 1.0 : 0.0; + // case "Moved Right": + // return (gamepad.rightStick.movementX != 0 || gamepad.rightStick.movementY != 0) ? 1.0 : 0.0; + } + return 0.0; + } +} diff --git a/Sources/armory/logicnode/MergedKeyboardNode.hx b/Sources/armory/logicnode/MergedKeyboardNode.hx new file mode 100644 index 0000000000..5a0f65a5a2 --- /dev/null +++ b/Sources/armory/logicnode/MergedKeyboardNode.hx @@ -0,0 +1,40 @@ +package armory.logicnode; + +class MergedKeyboardNode extends LogicNode { + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var keyboard = iron.system.Input.getKeyboard(); + var b = false; + switch (property0) { + case "started": + b = keyboard.started(property1); + case "down": + b = keyboard.down(property1); + case "released": + b = keyboard.released(property1); + } + if (b) runOutput(0); + } + + override function get(from: Int): Dynamic { + var keyboard = iron.system.Input.getKeyboard(); + switch (property0) { + case "started": + return keyboard.started(property1); + case "down": + return keyboard.down(property1); + case "released": + return keyboard.released(property1); + } + return false; + } +} diff --git a/Sources/armory/logicnode/MergedMouseNode.hx b/Sources/armory/logicnode/MergedMouseNode.hx new file mode 100644 index 0000000000..40ed2fec34 --- /dev/null +++ b/Sources/armory/logicnode/MergedMouseNode.hx @@ -0,0 +1,44 @@ +package armory.logicnode; + +class MergedMouseNode extends LogicNode { + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var mouse = iron.system.Input.getMouse(); + var b = false; + switch (property0) { + case "started": + b = mouse.started(property1); + case "down": + b = mouse.down(property1); + case "released": + b = mouse.released(property1); + case "moved": + b = mouse.moved; + } + if (b) runOutput(0); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + switch (property0) { + case "started": + return mouse.started(property1); + case "down": + return mouse.down(property1); + case "released": + return mouse.released(property1); + case "moved": + return mouse.moved; + } + return false; + } +} diff --git a/Sources/armory/logicnode/MergedSurfaceNode.hx b/Sources/armory/logicnode/MergedSurfaceNode.hx new file mode 100644 index 0000000000..9f00071a26 --- /dev/null +++ b/Sources/armory/logicnode/MergedSurfaceNode.hx @@ -0,0 +1,43 @@ +package armory.logicnode; + +class MergedSurfaceNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var surface = iron.system.Input.getSurface(); + var b = false; + switch (property0) { + case "started": + b = surface.started(); + case "down": + b = surface.down(); + case "released": + b = surface.released(); + case "moved": + b = surface.moved; + } + if (b) runOutput(0); + } + + override function get(from: Int): Dynamic { + var surface = iron.system.Input.getSurface(); + switch (property0) { + case "started": + return surface.started(); + case "down": + return surface.down(); + case "released": + return surface.released(); + case "moved": + return surface.moved; + } + return false; + } +} diff --git a/Sources/armory/logicnode/MergedVirtualButtonNode.hx b/Sources/armory/logicnode/MergedVirtualButtonNode.hx new file mode 100644 index 0000000000..d9e290e497 --- /dev/null +++ b/Sources/armory/logicnode/MergedVirtualButtonNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +class MergedVirtualButtonNode extends LogicNode { + + public var property0: String; + public var property1: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var vb = iron.system.Input.getVirtualButton(property1); + if (vb == null) return; + var b = false; + switch (property0) { + case "started": + b = vb.started; + case "down": + b = vb.down; + case "released": + b = vb.released; + } + if (b) runOutput(0); + } + + override function get(from: Int): Dynamic { + var vb = iron.system.Input.getVirtualButton(property1); + if (vb == null) return false; + switch (property0) { + case "started": + return vb.started; + case "down": + return vb.down; + case "released": + return vb.released; + } + return false; + } +} diff --git a/Sources/armory/logicnode/MeshNode.hx b/Sources/armory/logicnode/MeshNode.hx new file mode 100644 index 0000000000..4011204903 --- /dev/null +++ b/Sources/armory/logicnode/MeshNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.data.MeshData; + +class MeshNode extends LogicNode { + + public var property0: String; + public var value: MeshData = null; + + public function new(tree: LogicTree) { + super(tree); + + iron.Scene.active.notifyOnInit(function() { + get(0); + }); + } + + override function get(from: Int): Dynamic { + iron.data.Data.getMesh("mesh_" + property0, property0, function(mesh: MeshData) { + value = mesh; + }); + + return value; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/MixNode.hx b/Sources/armory/logicnode/MixNode.hx new file mode 100644 index 0000000000..4f75884dff --- /dev/null +++ b/Sources/armory/logicnode/MixNode.hx @@ -0,0 +1,58 @@ +package armory.logicnode; + +import iron.system.Tween; + +class MixNode extends LogicNode { + + public var property0: String; // Type + public var property1: String; // Ease + public var property2: Bool; // Clamp + + var ease: Float->Float = null; + + public function new(tree: LogicTree) { + super(tree); + } + + function init() { + switch (property0) { + case "Linear": + ease = Tween.easeLinear; + case "Sine": + ease = property1 == "In" ? Tween.easeSineIn : (property1 == "Out" ? Tween.easeSineOut : Tween.easeSineInOut); + case "Quad": + ease = property1 == "In" ? Tween.easeQuadIn : (property1 == "Out" ? Tween.easeQuadOut : Tween.easeQuadInOut); + case "Cubic": + ease = property1 == "In" ? Tween.easeCubicIn : (property1 == "Out" ? Tween.easeCubicOut : Tween.easeCubicInOut); + case "Quart": + ease = property1 == "In" ? Tween.easeQuartIn : (property1 == "Out" ? Tween.easeQuartOut : Tween.easeQuartInOut); + case "Quint": + ease = property1 == "In" ? Tween.easeQuintIn : (property1 == "Out" ? Tween.easeQuintOut : Tween.easeQuintInOut); + case "Expo": + ease = property1 == "In" ? Tween.easeExpoIn : (property1 == "Out" ? Tween.easeExpoOut : Tween.easeExpoInOut); + case "Circ": + ease = property1 == "In" ? Tween.easeCircIn : (property1 == "Out" ? Tween.easeCircOut : Tween.easeCircInOut); + case "Back": + ease = property1 == "In" ? Tween.easeBackIn : (property1 == "Out" ? Tween.easeBackOut : Tween.easeBackInOut); + case "Bounce": + ease = property1 == "In" ? Tween.easeBounceIn : (property1 == "Out" ? Tween.easeBounceOut : Tween.easeBounceInOut); + case "Elastic": + ease = property1 == "In" ? Tween.easeElasticIn : (property1 == "Out" ? Tween.easeElasticOut : Tween.easeElasticInOut); + default: + ease = Tween.easeLinear; + } + } + + override function get(from: Int): Dynamic { + if (ease == null) init(); + var k: Float = inputs[0].get(); //Factor + var v1: Float = inputs[1].get(); + var v2: Float = inputs[2].get(); + var f = v1 + (v2 - v1) * ease(k); + + // Clamp + if (property2) f = f < 0 ? 0 : f > 1 ? 1 : f; + + return f; + } +} diff --git a/Sources/armory/logicnode/MouseCoordsNode.hx b/Sources/armory/logicnode/MouseCoordsNode.hx new file mode 100644 index 0000000000..39df55d1c1 --- /dev/null +++ b/Sources/armory/logicnode/MouseCoordsNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class MouseCoordsNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var mouse = iron.system.Input.getMouse(); + if (from == 0) { + coords.x = mouse.x; + coords.y = mouse.y; + return coords; + } + else if (from == 1) { + coords.x = mouse.movementX; + coords.y = mouse.movementY; + return coords; + } + else { + return mouse.wheelDelta; + } + } +} diff --git a/Sources/armory/logicnode/NavigableLocationNode.hx b/Sources/armory/logicnode/NavigableLocationNode.hx new file mode 100644 index 0000000000..400796e274 --- /dev/null +++ b/Sources/armory/logicnode/NavigableLocationNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.math.Vec4; +import armory.trait.navigation.Navigation; + +class NavigableLocationNode extends LogicNode { + + var loc: Vec4; + + public function new(tree: LogicTree) { + super(tree); + + } + + override function get(from: Int): Dynamic { +#if arm_navigation + + assert(Error, Navigation.active.navMeshes.length > 0, "No Navigation Mesh Present"); + Navigation.active.navMeshes[0].getRandomPoint(function(p: Vec4) { + loc = p; + }); +#end + return loc; + } +} diff --git a/Sources/armory/logicnode/NetworkClientNode.hx b/Sources/armory/logicnode/NetworkClientNode.hx new file mode 100644 index 0000000000..e51b071eb9 --- /dev/null +++ b/Sources/armory/logicnode/NetworkClientNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; +import iron.object.Object; +import armory.system.Event; +import armory.network.Types; +import armory.network.Util; +import armory.network.Connect; + + +class NetworkClientNode extends LogicNode { + public var net_Url: String; + public var data: Dynamic; + public var client: armory.network.WebSocket; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + net_Url = inputs[1].get(); + if (net_Url == null) return; + if(Client.connections[net_Url] == null){ + var object = tree.object; + var client = new armory.network.Connect.Client(net_Url, object); + } else { + return; + } + runOutput(0); + } + + override function get(from: Int): Dynamic { + return Client.connections[net_Url]; + } + +} diff --git a/Sources/armory/logicnode/NetworkCloseConnectionNode.hx b/Sources/armory/logicnode/NetworkCloseConnectionNode.hx new file mode 100644 index 0000000000..8ec4015b71 --- /dev/null +++ b/Sources/armory/logicnode/NetworkCloseConnectionNode.hx @@ -0,0 +1,74 @@ +package armory.logicnode; +import armory.system.Event; +import armory.network.Connect; +import iron.object.Object; + + +class NetworkCloseConnectionNode extends LogicNode { + public var property0: Bool; + public var property1: String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + if(property1 == "client") { + var connection = cast(inputs[1].get(), armory.network.WebSocket); + if (connection == null) return; + #if sys + try{ + var net_Url = connection._protocol + "://" + connection._host + ":" + connection._port; + connection.close(); + if(property0 == true){ + Client.connections[net_Url] = null; + } else { + Client.connections[net_Url] = connection; + } + } catch(error) { + trace("Error: " + error); + return; + } + #else + try{ + var net_Url = @:privateAccess connection._url; + connection.close(); + if(property0 == true){ + Client.connections[net_Url] = null; + } else { + Client.connections[net_Url] = connection; + } + } catch(error) { + trace("Error: " + error); + return; + } + #end + } else if(property1 == "securehost"){ + #if sys + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + if (connection == null) return; + var net_Url = "wss://" + @:privateAccess connection._host + ":" + @:privateAccess connection._port; + if(SecureHost.connections[net_Url] != null){ + connection.stop(); + if(property0 == true){ + SecureHost.connections[net_Url] = null; + } + } + #end + } else { + #if sys + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + if (connection == null) return; + var net_Url = "ws://" + @:privateAccess connection._host + ":" + @:privateAccess connection._port; + if(Host.connections[net_Url] != null){ + connection.stop(); + if(property0 == true){ + Host.connections[net_Url] = null; + } + } + #end + } + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/NetworkEventNode.hx b/Sources/armory/logicnode/NetworkEventNode.hx new file mode 100644 index 0000000000..2b1006f6bd --- /dev/null +++ b/Sources/armory/logicnode/NetworkEventNode.hx @@ -0,0 +1,88 @@ +package armory.logicnode; +import armory.system.Event; +import armory.network.Connect; +import iron.object.Object; + + +class NetworkEventNode extends LogicNode { + public var property0: String; + public var property1: String; + + public var value: String; + public var listener: TEvent = null; + public var net_Url: String; + + public function new(tree:LogicTree) { + super(tree); + tree.notifyOnInit(init); + } + + function init() { + if (property0 == "client") { + net_Url = inputs[0].get(); + switch (property1) { + case "onopen": value = Client.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onmessage": value = Client.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onerror": value = Client.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onclose": value = Client.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); + default: throw "Failed to set client event type."; + } + } else if (property0 == "host") { + #if sys + var net_Domain = inputs[0].get(); + var net_Port = inputs[1].get(); + net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port); + switch (property1) { + case "onopen": value = Host.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onmessage": value = Host.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onerror": value = Host.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onclose": value = Host.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); + default: throw "Failed to set host event type."; + } + #end + } else if (property0 == "securehost"){ + #if sys + var net_Domain = inputs[0].get(); + var net_Port = inputs[1].get(); + net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port); + switch (property1) { + case "onopen": value = SecureHost.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onmessage": value = SecureHost.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onerror": value = SecureHost.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); + case "onclose": value = SecureHost.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); + default: throw "Failed to set host event type."; + } + #end + } + + } + + function onEvent() { + runOutput(0); + } + + override function get(from: Int): Dynamic { + if (property0 == "host") { + return switch (from) { + case 1: Host.id[net_Url]; + case 2: Host.data[net_Url]; + default: throw "Unreachable"; + } + } + else if (property0 == "securehost") { + return switch (from) { + case 1: SecureHost.id[net_Url]; + case 2: SecureHost.data[net_Url]; + default: throw "Unreachable"; + } + } + else { + return switch (from) { + case 1: Client.id[net_Url]; + case 2: Client.data[net_Url]; + default: throw "Unreachable"; + } + } + } + +} diff --git a/Sources/armory/logicnode/NetworkHostCloseClientNode.hx b/Sources/armory/logicnode/NetworkHostCloseClientNode.hx new file mode 100644 index 0000000000..4dee8ddef6 --- /dev/null +++ b/Sources/armory/logicnode/NetworkHostCloseClientNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; +import armory.network.Connect; +import iron.object.Object; + + +class NetworkHostCloseClientNode extends LogicNode { + public var property0: Bool; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if sys + if(property0 == false){ + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + if (connection == null) return; + var id = inputs[2].get(); + for(h in connection.handlers){ + if(h.id == id){ + h.close(); + } + } + runOutput(0); + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + if (connection == null) return; + var id = inputs[2].get(); + for(h in connection.handlers){ + if(h.id == id){ + h.close(); + } + } + runOutput(0); + } + #end + } + +} + + diff --git a/Sources/armory/logicnode/NetworkHostGetIpNode.hx b/Sources/armory/logicnode/NetworkHostGetIpNode.hx new file mode 100644 index 0000000000..e47108c531 --- /dev/null +++ b/Sources/armory/logicnode/NetworkHostGetIpNode.hx @@ -0,0 +1,49 @@ +package armory.logicnode; +import armory.network.Connect; +import iron.object.Object; + + +class NetworkHostGetIpNode extends LogicNode { + public var property0: Bool; + public var ip: String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if sys + if(property0 == false){ + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + if (connection == null) return; + var id = inputs[2].get(); + if (id == null) return; + for(h in connection.handlers){ + if(h.id == id){ + var p_ip = @:privateAccess h._socket.peer(); + ip = p_ip.host.toString(); + } + } + runOutput(0); + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + if (connection == null) return; + var id = inputs[2].get(); + if (id == null) return; + for(h in connection.handlers){ + if(h.id == id){ + var p_ip = @:privateAccess h._socket.peer(); + ip = p_ip.host.toString(); + } + } + runOutput(0); + } + #end + } + + override function get(from: Int): String { + return ip; + } +} + + diff --git a/Sources/armory/logicnode/NetworkHostNode.hx b/Sources/armory/logicnode/NetworkHostNode.hx new file mode 100644 index 0000000000..5723eb9099 --- /dev/null +++ b/Sources/armory/logicnode/NetworkHostNode.hx @@ -0,0 +1,70 @@ +package armory.logicnode; +import armory.network.Connect; +import iron.object.Object; + + +class NetworkHostNode extends LogicNode { + public var property0: Bool; + public var net_Url: String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if sys + if(property0 == false) { + final net_Object: Object = tree.object; + var net_Domain: String = inputs[1].get(); + var net_Port: Int = inputs[2].get(); + var max_Conn: Int = inputs[3].get(); + net_Url = "ws://" + net_Domain + ":" + net_Port; + if (Host.connections[net_Url] != null) return; + try{ + var server = new Host(net_Domain, net_Port, max_Conn, net_Object); + if (Host.connections[net_Url] == null) return; + Host.connections[net_Url].start(); + runOutput(0); + } catch(err){ + trace("Failed to start server with the following error: " + err + ". Check if there is a system process occupying the port."); + } + } else { + final net_Object: Object = tree.object; + var net_Domain: String = inputs[1].get(); + var net_Port: Int = inputs[2].get(); + var max_Conn: Int = inputs[3].get(); + var net_Cert: String = inputs[4].get(); + var net_Key: String = inputs[5].get(); + net_Url = "wss://" + net_Domain + ":" + net_Port; + if (SecureHost.connections[net_Url] != null) return; + try{ + var server = new SecureHost(net_Domain, net_Port, max_Conn, net_Object, net_Cert, net_Key); + if (SecureHost.connections[net_Url] == null) return; + SecureHost.connections[net_Url].start(); + runOutput(0); + } catch(err){ + trace("Failed to start server with the following error: " + err + ". Check if there is a system process occupying the port."); + } + } + #end + } + + #if sys + override function get(from: Int): Dynamic { + if(property0 == false) { + return switch (from) { + case 1: Host.connections[net_Url]; + case 2: Host.data[net_Url]; + default: throw "Unreachable"; + } + } else { + return switch (from) { + case 1: SecureHost.connections[net_Url]; + case 2: SecureHost.data[net_Url]; + default: throw "Unreachable"; + } + } + } + #end +} + diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx new file mode 100644 index 0000000000..695285acab --- /dev/null +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -0,0 +1,94 @@ +package armory.logicnode; +import iron.object.Object; + + +class NetworkHttpRequestNode extends LogicNode { + public var property0: String; + public var statusInt: Int; + public var response: Dynamic; + public var headers: Map; + public var parameters: Map; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + var url = inputs[1].get(); + + if(url == null){return;} + if(property0 == "post") { + headers = inputs[4].get(); + parameters = inputs[5].get(); + }else{ + headers = inputs[2].get(); + parameters = inputs[3].get(); + } + + var request = new haxe.Http(url); + + request.async = true; + + if(headers != null){ + for (k in headers.keys()) { + request.addHeader( k, headers[k]); + } + } + if(parameters != null){ + for (k in parameters.keys()) { + request.addParameter( k, parameters[k]); + } + } + + request.onStatus = function(status:Int) { + statusInt = status; + runOutput(0); + } + + request.onBytes = function(data:haxe.io.Bytes) { + response = data; + runOutput(0); + } + + request.onData = function(data:String) { + response = data; + runOutput(0); + } + + request.onError = function(error:String){ + trace ("Error: " + error ); + runOutput(0); + } + + try { + if(property0 == "post") { + var bytes = inputs[2].get(); + if(bytes == true){ + var data:haxe.io.Bytes = inputs[3].get(); + request.setPostBytes(data); + request.request(true); + }else{ + var data:Dynamic = inputs[3].get(); + request.setPostData(data.toString()); + request.request(true); + } + } else { + request.request(false); + } + } catch( e : Dynamic ) { + trace("Could not complete request: " + e); + } + + } + + override function get(from: Int): Dynamic { + + return switch (from) { + case 1: statusInt; + case 2: response; + default: throw "Unreachable"; + } + + } +} diff --git a/Sources/armory/logicnode/NetworkMessageParserNode.hx b/Sources/armory/logicnode/NetworkMessageParserNode.hx new file mode 100644 index 0000000000..0810be1d62 --- /dev/null +++ b/Sources/armory/logicnode/NetworkMessageParserNode.hx @@ -0,0 +1,100 @@ +package armory.logicnode; +import armory.network.Connect; +import armory.network.Buffer; +import haxe.io.Bytes; +import iron.object.Object; +import iron.math.Vec4; +import iron.math.Quat; +import iron.math.Mat4; + + +class NetworkMessageParserNode extends LogicNode { + public var property0: String; + public var api: String; + public var parsed: Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + var data: Dynamic = inputs[2].get(); + if (data == null) return; + + var buffer = cast(data, Buffer); + api = inputs[1].get(); + if(buffer.endsWith(api) == false){ + return; + } + + switch (property0) { + case "string": + var bytes = buffer.readAllAvailableBytes(); + parsed = bytes.toString().substr(0, bytes.length - api.length); + runOutput(0); + case "vector": + var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var vec = new Vec4(); + for (f in Reflect.fields(vec)) { + Reflect.setField(vec, f, bytes.readFloat()); + } + parsed = vec; + runOutput(0); + case "float": + var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var float: Float = bytes.readFloat(); + parsed = float; + runOutput(0); + case "integer": + var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var integer: Int = bytes.readInt32(); + parsed = integer; + runOutput(0); + case "boolean": + var bytes = buffer.readAllAvailableBytes(); + var boolean = bytes.toString().substr(0, bytes.length - api.length); + if(boolean == "true"){ + parsed = true; + } else { + parsed = false; + } + runOutput(0); + case "transform": + var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var loc: Vec4 = new Vec4(); + var rot: Quat = new Quat(); + var scl: Vec4 = new Vec4(); + for (f in Reflect.fields(loc)) { + Reflect.setField(loc, f, bytes.readFloat()); + } + for (f in Reflect.fields(rot)) { + Reflect.setField(rot, f, bytes.readFloat()); + } + for (f in Reflect.fields(scl)) { + Reflect.setField(scl, f, bytes.readFloat()); + } + var transform = Mat4.identity(); + parsed = transform.compose(loc, rot, scl); + runOutput(0); + case "rotation": + var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var rot = new Quat(); + for (f in Reflect.fields(rot)) { + Reflect.setField(rot, f, bytes.readFloat()); + } + parsed = rot; + runOutput(0); + default: throw "Failed to parse data."; + } + + } + + override function get(from: Int): Dynamic { + return switch (from) { + case 1: api; + case 2: parsed; + default: throw "Unreachable"; + } + } + +} diff --git a/Sources/armory/logicnode/NetworkOpenConnectionNode.hx b/Sources/armory/logicnode/NetworkOpenConnectionNode.hx new file mode 100644 index 0000000000..134e686196 --- /dev/null +++ b/Sources/armory/logicnode/NetworkOpenConnectionNode.hx @@ -0,0 +1,137 @@ +package armory.logicnode; +import armory.system.Event; +import armory.network.Connect; +import armory.network.Types; +import armory.network.Util; +import iron.object.Object; + + +class NetworkOpenConnectionNode extends LogicNode { + public var property0: String; + public var net_Url: String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + if(property0 == "client") { + var connection = cast(inputs[1].get(), armory.network.WebSocket); + if (connection == null) return; + var object = tree.object; + #if sys + net_Url = connection._protocol + "://" + connection._host + ":" + connection._port; + Client.connections[net_Url] = null; + var client = new armory.network.Connect.Client(net_Url, object); + #else + net_Url = @:privateAccess connection._url; + var client = Client.connections[net_Url]; + if (client == null) return; + try { + client.open(); + } catch (error) { + trace("Error: " + error); + return; + } + final openEvent = Event.get(Client.onOpenEvent); + final messageEvent = Event.get(Client.onMessageEvent); + final errorEvent = Event.get(Client.onErrorEvent); + final closeEvent = Event.get(Client.onCloseEvent); + client.onopen = function() { + Client.data[net_Url] = "Connection open."; + Client.id[net_Url] = Util.generateUUID(); + if (openEvent != null) { + for (e in openEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + client.onmessage = function(message: MessageType) { + switch (message) { + case BytesMessage(content): + Client.data[net_Url] = content; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + case StrMessage(content): + Client.data[net_Url] = content; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + } + client.onerror = function(err){ + Client.data[net_Url] = err; + if (errorEvent != null) { + for (e in errorEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + client.onclose =function(){ + Client.data[net_Url] = "Connection closed."; + if (closeEvent != null) { + for (e in closeEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + Client.connections[net_Url] = client; + #end + runOutput(0); + } else if (property0 == "securehost"){ + #if sys + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + if (connection == null) return; + net_Url = "wss://" + @:privateAccess connection._host + ":" + @:privateAccess connection._port; + if(SecureHost.connections[net_Url] != null){ + try{ + connection.start(); + }catch(error){ + trace("Error: " + error); + } + } + runOutput(0); + #end + } else { + #if sys + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + if (connection == null) return; + net_Url = "ws://" + @:privateAccess connection._host + ":" + @:privateAccess connection._port; + if(Host.connections[net_Url] != null){ + try{ + connection.start(); + }catch(error){ + trace("Error: " + error); + } + runOutput(0); + } + #end + } + } + + override function get(from: Int): Dynamic { + return switch (property0) { + #if sys + case "host": Host.connections[net_Url]; + case "securehost": SecureHost.connections[net_Url]; + #end + case "client": Client.connections[net_Url]; + default: throw "Unreachable"; + } + } +} diff --git a/Sources/armory/logicnode/NetworkSendMessageNode.hx b/Sources/armory/logicnode/NetworkSendMessageNode.hx new file mode 100644 index 0000000000..4aa0d98673 --- /dev/null +++ b/Sources/armory/logicnode/NetworkSendMessageNode.hx @@ -0,0 +1,802 @@ +package armory.logicnode; +import armory.network.Connect; +import armory.network.Buffer; +import haxe.io.Bytes; +import iron.object.Object; +import iron.math.Vec4; +import iron.math.Quat; +import iron.math.Mat4; + + +class NetworkSendMessageNode extends LogicNode { + public var property0: String; + public var property1: String; + public var id: String; + public var all: Bool; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + var api: String = inputs[2].get(); + var message: Dynamic = inputs[3].get(); + if (message == null) return; + + switch (property1) { + case "string": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofString(message)); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofString(message)); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofString(message)); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofString(message)); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofString(message)); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "vector": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "float": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "integer": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "boolean": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var buffer = new Buffer(); + if(message == true){ + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var buffer = new Buffer(); + if(message == true){ + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var buffer = new Buffer(); + if(message == true){ + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var buffer = new Buffer(); + if(message == true){ + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var buffer = new Buffer(); + if(message == true){ + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "transform": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + var l = Reflect.fields(loc); + for (b in l) { + bytesOut.writeFloat(Reflect.field(loc, b)); + } + var r = Reflect.fields(rot); + for (b in r) { + bytesOut.writeFloat(Reflect.field(rot, b)); + } + var s = Reflect.fields(scale); + for (b in s) { + bytesOut.writeFloat(Reflect.field(scale, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + var l = Reflect.fields(loc); + for (b in l) { + bytesOut.writeFloat(Reflect.field(loc, b)); + } + var r = Reflect.fields(rot); + for (b in r) { + bytesOut.writeFloat(Reflect.field(rot, b)); + } + var s = Reflect.fields(scale); + for (b in s) { + bytesOut.writeFloat(Reflect.field(scale, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + var l = Reflect.fields(loc); + for (b in l) { + bytesOut.writeFloat(Reflect.field(loc, b)); + } + var r = Reflect.fields(rot); + for (b in r) { + bytesOut.writeFloat(Reflect.field(rot, b)); + } + var s = Reflect.fields(scale); + for (b in s) { + bytesOut.writeFloat(Reflect.field(scale, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + var l = Reflect.fields(loc); + for (b in l) { + bytesOut.writeFloat(Reflect.field(loc, b)); + } + var r = Reflect.fields(rot); + for (b in r) { + bytesOut.writeFloat(Reflect.field(rot, b)); + } + var s = Reflect.fields(scale); + for (b in s) { + bytesOut.writeFloat(Reflect.field(scale, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + var l = Reflect.fields(loc); + for (b in l) { + bytesOut.writeFloat(Reflect.field(loc, b)); + } + var r = Reflect.fields(rot); + for (b in r) { + bytesOut.writeFloat(Reflect.field(rot, b)); + } + var s = Reflect.fields(scale); + for (b in s) { + bytesOut.writeFloat(Reflect.field(scale, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + case "rotation": + if(property0 == "client"){ + var connection = cast(inputs[1].get(), armory.network.WebSocket); + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + connection.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + #if sys + else if(inputs[5].get() == true){ + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var id = inputs[4].get(); + if (id == null) return; + if(property0 == "securehost"){ + var connection = cast(inputs[1].get(), armory.network.WebSocketSecureServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } else { + var connection = cast(inputs[1].get(), armory.network.WebSocketServer); + for(h in connection.handlers){ + if(h.id == id){ + var bytesOut = new haxe.io.BytesOutput(); + var i = Reflect.fields(message); + for (b in i) { + bytesOut.writeFloat(Reflect.field(message, b)); + } + var buffer = new Buffer(); + buffer.writeBytes(bytesOut.getBytes()); + if(api != "" || api != null){ + buffer.writeBytes(Bytes.ofString(api)); + } + try { + h.send(buffer); + } catch(error) { + trace("Error: " + error); + } + } + } + } + } + #end + runOutput(0); + default: throw "Failed to send data."; + } + + } + +} + + diff --git a/Sources/armory/logicnode/NoneNode.hx b/Sources/armory/logicnode/NoneNode.hx new file mode 100644 index 0000000000..0dd28ce534 --- /dev/null +++ b/Sources/armory/logicnode/NoneNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class NoneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return null; + } +} diff --git a/Sources/armory/logicnode/NotNode.hx b/Sources/armory/logicnode/NotNode.hx new file mode 100644 index 0000000000..d771521cfb --- /dev/null +++ b/Sources/armory/logicnode/NotNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class NotNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var v1: Bool = inputs[0].get(); + return !v1; + } +} diff --git a/Sources/armory/logicnode/NullNode.hx b/Sources/armory/logicnode/NullNode.hx new file mode 100644 index 0000000000..88d1f13f78 --- /dev/null +++ b/Sources/armory/logicnode/NullNode.hx @@ -0,0 +1,10 @@ +package armory.logicnode; + +class NullNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { return null; } +} diff --git a/Sources/armory/logicnode/ObjectNode.hx b/Sources/armory/logicnode/ObjectNode.hx new file mode 100644 index 0000000000..bd3e849434 --- /dev/null +++ b/Sources/armory/logicnode/ObjectNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; + +class ObjectNode extends LogicNode { + + public var objectName: String; + public var value: Object = null; + + public function new(tree: LogicTree, objectName: String = "") { + this.objectName = objectName; + super(tree); + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + if (value == null) value = objectName != "" ? iron.Scene.active.getChild(objectName) : tree.object; + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else { + objectName = value != null ? value.name : ""; + this.value = value; + } + } +} diff --git a/Sources/armory/logicnode/OnActionMarkerNode.hx b/Sources/armory/logicnode/OnActionMarkerNode.hx new file mode 100644 index 0000000000..80c2b95fec --- /dev/null +++ b/Sources/armory/logicnode/OnActionMarkerNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.Object; + +class OnActionMarkerNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnInit(init); + } + + function init() { + var object: Object = inputs[0].get(); + var marker: String = inputs[1].get(); + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.notifyOnMarker(marker, function() { runOutput(0); }); + } +} diff --git a/Sources/armory/logicnode/OnApplicationStateNode.hx b/Sources/armory/logicnode/OnApplicationStateNode.hx new file mode 100644 index 0000000000..ae084fcf83 --- /dev/null +++ b/Sources/armory/logicnode/OnApplicationStateNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +@:access(iron.Trait) +class OnApplicationStateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnInit(init); + } + + function init() { + kha.System.notifyOnApplicationState( + () -> runOutput(0), // On foreground + null, // On resume + null, // On pause + () -> runOutput(1), // On background + () -> runOutput(2) // On shutdown + ); + } +} diff --git a/Sources/armory/logicnode/OnCanvasElementNode.hx b/Sources/armory/logicnode/OnCanvasElementNode.hx new file mode 100644 index 0000000000..3967538e28 --- /dev/null +++ b/Sources/armory/logicnode/OnCanvasElementNode.hx @@ -0,0 +1,116 @@ +package armory.logicnode; + +import armory.trait.internal.CanvasScript; +import iron.Scene; + +#if arm_ui +import armory.ui.Canvas.Anchor; +#end + +class OnCanvasElementNode extends LogicNode { + + var canvas: CanvasScript; + var element: String; + + /** + * The event type this node should react to, can be "click" or "hover". + */ + public var property0: String; + /** + * If the event type is click, this property states whether to check for + * "down", "started" or "released" events. + */ + public var property1: String; + /** + * The mouse button that this node should react to. Only used when listening + * for mouse clicks. + */ + public var property2: String; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + #if arm_ui + function update() { + element = inputs[0].get(); + + // Ensure canvas is ready + if(!Scene.active.ready) return; + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + if(canvas == null) return; + if (!canvas.ready) return; + if(canvas.getElement(element) == null) return; + if(canvas.getElement(element).visible == false) return; + var mouse = iron.system.Input.getMouse(); + var isEvent = false; + + if (property0 == "click") { + switch (property1) { + case "started": + isEvent = mouse.started(property2); + case "down": + isEvent = mouse.down(property2); + case "released": + isEvent = mouse.released(property2); + } + } + // Hovered + else { + isEvent = true; + } + + if (isEvent) + { + var canvasElem = canvas.getElement(element); + var left = canvasElem.x*canvas.getUiScale(); + var top = canvasElem.y*canvas.getUiScale(); + var right = left + canvasElem.width*canvas.getUiScale(); + var bottom = top + canvasElem.height*canvas.getUiScale(); + + var anchor = canvasElem.anchor; + var cx = canvas.getCanvas().width; + var cy = canvas.getCanvas().height; + var mouseX = mouse.x; + var mouseY = mouse.y; + + switch(anchor) + { + case Top: + mouseX -= cx/2 - canvasElem.width/2; + case TopRight: + mouseX -= cx - canvasElem.width; + case CenterLeft: + mouseY -= cy/2 - canvasElem.height/2; + case Anchor.Center: + mouseX -= cx/2 - canvasElem.width/2; + mouseY -= cy/2 - canvasElem.height/2; + case CenterRight: + mouseX -= cx - canvasElem.width; + mouseY -= cy/2 - canvasElem.height/2; + case BottomLeft: + mouseY -= cy - canvasElem.height; + case Bottom: + mouseX -= cx/2 - canvasElem.width/2; + mouseY -= cy - canvasElem.height; + case BottomRight: + mouseX -= cx - canvasElem.width; + mouseY -= cy - canvasElem.height; + } + + if((mouseX >= left) && (mouseX <= right)) + { + if((mouseY >= top) && (mouseY <= bottom)) + { + runOutput(0); + } + } + } + } + #else + function update() {} + #end +} diff --git a/Sources/armory/logicnode/OnContactArrayNode.hx b/Sources/armory/logicnode/OnContactArrayNode.hx new file mode 100644 index 0000000000..8c3f3af5c1 --- /dev/null +++ b/Sources/armory/logicnode/OnContactArrayNode.hx @@ -0,0 +1,58 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class OnContactArrayNode extends LogicNode { + + public var property0: String; + var lastContact = false; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var object1: Object = inputs[0].get(); + var objects: Array = inputs[1].get(); + + if (object1 == null) object1 = tree.object; + if (objects == null)return; + + var contact = false; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb1 = object1.getTrait(RigidBody); + var rbs = physics.getContacts(rb1); + if (rb1 != null && rbs != null) { + for (object2 in objects) { + var rb2 = object2.getTrait(RigidBody); + for (rb in rbs) { + if (rb == rb2) { + contact = true; + break; + } + } + if (contact) break; + } + } +#end + + var b = false; + switch (property0) { + case "begin": + b = contact && !lastContact; + case "overlap": + b = contact; + case "end": + b = !contact && lastContact; + } + + lastContact = contact; + + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnContactNode.hx b/Sources/armory/logicnode/OnContactNode.hx new file mode 100644 index 0000000000..aedc74d954 --- /dev/null +++ b/Sources/armory/logicnode/OnContactNode.hx @@ -0,0 +1,57 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class OnContactNode extends LogicNode { + + public var property0: String; + var lastContact = false; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var object1: Object = inputs[0].get(); + var object2: Object = inputs[1].get(); + + if (object1 == null) object1 = tree.object; + if (object2 == null) object2 = tree.object; + + var contact = false; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb1 = object1.getTrait(RigidBody); + if (rb1 != null) { + var rbs = physics.getContacts(rb1); + if (rbs != null) { + var rb2 = object2.getTrait(RigidBody); + for (rb in rbs) { + if (rb == rb2) { + contact = true; + break; + } + } + } + } +#end + + var b = false; + switch (property0) { + case "begin": + b = contact && !lastContact; + case "overlap": + b = contact; + case "end": + b = !contact && lastContact; + } + + lastContact = contact; + + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnEventNode.hx b/Sources/armory/logicnode/OnEventNode.hx new file mode 100644 index 0000000000..42658898cf --- /dev/null +++ b/Sources/armory/logicnode/OnEventNode.hx @@ -0,0 +1,55 @@ +package armory.logicnode; + +import armory.system.Event; + +class OnEventNode extends LogicNode { + + public var property1: String; // Init, Update, Custom + var value: String; + var listener: TEvent = null; + var oldValue: String; + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnInit(init_main); + tree.notifyOnRemove(onRemove); + } + + function init_main() { + switch (property1) { + case "init": tree.notifyOnInit(init); + case "update": tree.notifyOnUpdate(update); + } + } + + function init() { + value = inputs[0].get(); + listener = Event.add(value, onEvent, tree.object.uid); + } + + function update() { + value = inputs[0].get(); + if (value != oldValue) { + onRemove(); + listener = Event.add(value, onEvent, tree.object.uid); + oldValue = value; + } + } + + override function run(from: Int) { + value = inputs[1].get(); + if (value != oldValue) { + onRemove(); + listener = Event.add(value, onEvent, tree.object.uid); + oldValue = value; + } + } + + function onEvent() { + runOutput(0); + } + + function onRemove() { + if (listener != null) Event.removeListener(listener); + } +} diff --git a/Sources/armory/logicnode/OnGamepadNode.hx b/Sources/armory/logicnode/OnGamepadNode.hx new file mode 100644 index 0000000000..5f376bd270 --- /dev/null +++ b/Sources/armory/logicnode/OnGamepadNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +class OnGamepadNode extends LogicNode { + + public var property0: String; + public var property1: String; + + @:deprecated("The 'On Gamepad' node is deprecated and will be removed in future SDK versions. Please use 'Gamepad' instead.") + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var num: Int = inputs[0].get(); + var gamepad = iron.system.Input.getGamepad(num); + if (gamepad == null) return; + var b = false; + switch (property0) { + case "Down": + b = gamepad.down(property1) > 0.0; + case "Started": + b = gamepad.started(property1); + case "Released": + b = gamepad.released(property1); + // case "Moved Left": + // b = gamepad.leftStick.movementX != 0 || gamepad.leftStick.movementY != 0; + // case "Moved Right": + // b = gamepad.rightStick.movementX != 0 || gamepad.rightStick.movementY != 0; + } + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnInitNode.hx b/Sources/armory/logicnode/OnInitNode.hx new file mode 100644 index 0000000000..ea825cd102 --- /dev/null +++ b/Sources/armory/logicnode/OnInitNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import armory.trait.physics.PhysicsWorld; + +@:access(iron.Trait) +class OnInitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnInit(init); + } + + function init() { + #if arm_physics + var noPhysics = PhysicsWorld.active == null || PhysicsWorld.active._lateUpdate == null; + noPhysics ? runOutput(0) : PhysicsWorld.active.notifyOnPreUpdate(physics_init); + #else + runOutput(0); + #end + } + + #if arm_physics + function physics_init() { + PhysicsWorld.active.removePreUpdate(physics_init); + runOutput(0); + } + #end +} diff --git a/Sources/armory/logicnode/OnInputMapNode.hx b/Sources/armory/logicnode/OnInputMapNode.hx new file mode 100644 index 0000000000..3e354f29f5 --- /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/OnKeyboardNode.hx b/Sources/armory/logicnode/OnKeyboardNode.hx new file mode 100644 index 0000000000..1e1bbec950 --- /dev/null +++ b/Sources/armory/logicnode/OnKeyboardNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +class OnKeyboardNode extends LogicNode { + + public var property0: String; + public var property1: String; + + @:deprecated("The 'On Keyboard' node is deprecated and will be removed in future SDK versions. Please use 'Keyboard' instead.") + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var keyboard = iron.system.Input.getKeyboard(); + var b = false; + switch (property0) { + case "Down": + b = keyboard.down(property1); + case "Started": + b = keyboard.started(property1); + case "Released": + b = keyboard.released(property1); + } + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnMouseNode.hx b/Sources/armory/logicnode/OnMouseNode.hx new file mode 100644 index 0000000000..32e7f76ccc --- /dev/null +++ b/Sources/armory/logicnode/OnMouseNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +class OnMouseNode extends LogicNode { + + public var property0: String; + public var property1: String; + + @:deprecated("The 'On Mouse' node is deprecated and will be removed in future SDK versions. Please use 'Mouse' instead.") + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var mouse = iron.system.Input.getMouse(); + var b = false; + switch (property0) { + case "Down": + b = mouse.down(property1); + case "Started": + b = mouse.started(property1); + case "Released": + b = mouse.released(property1); + case "Moved": + b = mouse.moved; + } + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnRender2DNode.hx b/Sources/armory/logicnode/OnRender2DNode.hx new file mode 100644 index 0000000000..fe965efeac --- /dev/null +++ b/Sources/armory/logicnode/OnRender2DNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +import armory.renderpath.RenderToTexture; + +class OnRender2DNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnRender2D(onRender2D); + } + + function onRender2D(g: kha.graphics2.Graphics) { + RenderToTexture.ensureEmptyRenderTarget("OnRender2DNode"); + RenderToTexture.g = g; + runOutput(0); + RenderToTexture.g = null; + } +} diff --git a/Sources/armory/logicnode/OnSurfaceNode.hx b/Sources/armory/logicnode/OnSurfaceNode.hx new file mode 100644 index 0000000000..859e61b24a --- /dev/null +++ b/Sources/armory/logicnode/OnSurfaceNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +class OnSurfaceNode extends LogicNode { + + public var property0: String; + + @:deprecated("The 'On Surface' node is deprecated and will be removed in future SDK versions. Please use 'Surface' instead.") + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var surface = iron.system.Input.getSurface(); + var b = false; + switch (property0) { + case "Touched": + b = surface.down(); + case "Started": + b = surface.started(); + case "Released": + b = surface.released(); + case "Moved": + b = surface.moved; + } + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnSwipeNode.hx b/Sources/armory/logicnode/OnSwipeNode.hx new file mode 100644 index 0000000000..ff3f6ecaf1 --- /dev/null +++ b/Sources/armory/logicnode/OnSwipeNode.hx @@ -0,0 +1,201 @@ +package armory.logicnode; +import iron.math.Vec4; +import iron.math.Vec2; + +class OnSwipeNode extends LogicNode { + + var point_start = new Vec2(); + var point_end = new Vec2(); + var direction = new Vec2(); + var length = 0; + var swipe = false; + public var time_delta = 0.0; + public var minimal_length = 0; + var timer = 0.0; + + // New (constructor) + public function new(tree: LogicTree) { + super(tree); + // Set update + tree.notifyOnUpdate(update); + } + + // Update + function update() { + var surface = iron.system.Input.getSurface(); + // Check swipe end + if (swipe == true) { + timer += iron.system.Time.delta; + if ((surface.released() == true) || (timer >= time_delta)) { + swipe = false; + point_end.x = surface.x; + point_end.y = point_start.y; + point_start.y = surface.y; + // Calc result direction + direction.x = point_end.x - point_start.x; + direction.y = point_end.y - point_start.y; + // Calc length + length = Math.round(Math.sqrt(direction.x * direction.x + direction.y * direction.y)); + // Check minimal length + if (length >= minimal_length) { + // Execute next action linked to this node + runOutput(0); + } + } + } + // Check swipe start + else if ((surface.started() == true)) { + // In parameter + if (inputs.length > 1) + { + time_delta = inputs[0].get(); + minimal_length = inputs[1].get(); + } + point_start.x = surface.x; + point_start.y = surface.y; + swipe = true; + timer = 0; + direction.x = 0; + direction.y = 0; + length = 0; + } + } + + // Calc angle (0-360) + function calc_angle(vector: Vec2): Int { + var ax = vector.x; + var ay = vector.y; + var bx = vector.x; + var by = 0.0; + var angle = Math.atan2(ax * by - bx * ay, ax * bx + ay * by) * 180 / Math.PI; + // Determine the quarter + if ((ax > 0) && (ay > 0)) { + // I + angle = -angle; + } else if ((ax < 0) && (ay > 0)) { + // II + angle = 180.0 - angle; + } else if ((ax < 0) && (ay < 0)) { + // III + angle = 180.0 - angle; + } else if ((ax > 0) && (ay < 0)) { + // IV + angle = 360.0 - angle; + } + return Math.round(angle); + } + + // State determination according to 4 directions + function getStateFor4Direction(from: Int, dir: Vec2): Dynamic { + var angle = calc_angle(dir); + switch (from) { + // Up + case 1: { + // 45 - 135 + if ((angle >= 45) && (angle < 135)) return true; + return false; + } + // Down + case 2: + // 225 - 315 + if ((angle >= 225) && (angle < 315)) return true; + return false; + // Left + case 3: + // 135 - 225 + if ((angle >= 135) && (angle < 225)) return true; + return false; + // Right + case 4: + // 315 - 45 + if ((angle >= 315) || (angle < 45)) return true; + return false; + } + return null; + } + + // State determination according to 8 directions + function getStateFor8Direction(from: Int, dir: Vec2): Dynamic { + var angle = calc_angle(dir); + switch (from) { + // UP + case 1: { + // 68 - 112 + if ((angle >= 68) && (angle < 112)) return true; + return false; + } + // DOWN + case 2: { + // 248 - 292 + if ((angle >= 248) && (angle < 292)) return true; + return false; + } + // LEFT + case 3: { + // 158 - 202 + if ((angle >= 158) && (angle < 202)) return true; + return false; + } + // RIGHT + case 4: { + // 338 - 22 + if ((angle >= 338) || (angle < 22)) return true; + return false; + } + // UP-LEFT + case 5: { + // 112 - 158 + if ((angle >= 112) && (angle < 158)) return true; + return false; + } + // UP-RIGHT + case 6: { + // 22 - 68 + if ((angle >= 22) && (angle < 68)) return true; + return false; + } + // DOWN-LEFT + case 7: { + // 202 - 248 + if ((angle >= 202) && (angle < 248)) return true; + return false; + } + // DOWN-RIGHT + case 8: { + // 292 - 338 + if ((angle >= 292) && (angle < 338)) return true; + return false; + } + } + return null; + } + + // State determination + function getState(from: Int, dir: Vec2): Dynamic { + // Check count outputs parameter + if (outputs.length == 8) { + return getStateFor4Direction(from, dir); + } else if (outputs.length == 12) { + return getStateFor8Direction(from, dir); + } + return null; + } + + // Get - out + override function get(from: Int): Dynamic { + switch (from) { + // Out value - Direction (Vector) + case 1: { + direction = direction.normalize(); + return new Vec4(direction.x, direction.y, 0, 0); + } + // Out value - Length (px) + case 2: return length; + // Out value - Angle (0-360) + case 3: return calc_angle(direction); + } + // Direction State + if (from > 3) return getState(from - 3, direction); + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/OnTapScreen.hx b/Sources/armory/logicnode/OnTapScreen.hx new file mode 100644 index 0000000000..d5b0c2bc39 --- /dev/null +++ b/Sources/armory/logicnode/OnTapScreen.hx @@ -0,0 +1,108 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class OnTapScreen extends LogicNode { + + var duration = 0.3; + var interval = 0.0; + var repeat = 2; + + var timer_run = false; + var timer_duration = 0.0; + var timer_interval = 0.0; + var count_taps = 0; + var coords_last_tap = new Vec4(); + + // New (constructor) + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnUpdate(update); + } + + // Clear var + function clear_var() { + timer_run = false; + count_taps = 0; + timer_duration = 0.0; + timer_interval = 0.0; + } + + // Save vector coords + function save_vector_coord(x: Float, y: Float) { + coords_last_tap.x = x; + coords_last_tap.y = y; + } + + // Update + function update() { + var surface = iron.system.Input.getSurface(); + // In parameters + if (surface.started() == true) { + duration = inputs[0].get(); + interval = inputs[1].get(); + repeat = inputs[2].get(); + } + // Check + if ((repeat <= 0) || (duration <= 0)) { + clear_var(); + return; + } + // timer_duration check + if (timer_run == true) { + timer_duration += iron.system.Time.delta; + if (interval > 0) timer_interval += iron.system.Time.delta; + } + // First Tap, start timer + if ((surface.started() == true) && (count_taps == 0)) { + clear_var(); + count_taps += 1; + save_vector_coord(surface.x, surface.y); + runOutput(2); // action Tap + timer_run = true; + } else { + // Next Taps + if ((surface.started() == true) && (timer_duration < duration)) { + if (interval > 0) { + if (timer_interval >= interval) { + count_taps += 1; + save_vector_coord(surface.x, surface.y); + runOutput(2); // action Tap + timer_interval = 0; + } + } else { + count_taps += 1; + save_vector_coord(surface.x, surface.y); + runOutput(2); // action Tap + } + } + // Time passed + if (timer_duration >= duration) { + // Taps completed + if (count_taps >= repeat) { + save_vector_coord(surface.x, surface.y); + runOutput(0); // action Done + return; + } + clear_var(); + runOutput(1); // action Fail + } else if (count_taps >= repeat) { + // Taps completed + save_vector_coord(surface.x, surface.y); + runOutput(0); // action Done + clear_var(); + } + } + } + + // Get - out + override function get(from: Int): Dynamic { + switch (from) { + // Out value - Tap Number + case 3: return count_taps; + // Out value - Coords last tap + case 4: return coords_last_tap; + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/OnTimerNode.hx b/Sources/armory/logicnode/OnTimerNode.hx new file mode 100644 index 0000000000..5328bddbe1 --- /dev/null +++ b/Sources/armory/logicnode/OnTimerNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class OnTimerNode extends LogicNode { + + var duration = 0.0; + var repeat = false; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + + if (duration <= 0.0) { + duration = inputs[0].get(); + repeat = inputs[1].get(); + } + + duration -= iron.system.Time.delta; + if (duration <= 0.0) { + if (!repeat) tree.removeUpdate(update); + runOutput(0); + } + } +} diff --git a/Sources/armory/logicnode/OnUpdateNode.hx b/Sources/armory/logicnode/OnUpdateNode.hx new file mode 100644 index 0000000000..a445c3e327 --- /dev/null +++ b/Sources/armory/logicnode/OnUpdateNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import armory.trait.physics.PhysicsWorld; + +class OnUpdateNode extends LogicNode { + + public var property0: String; // Update, Late Update, Physics Pre-Update + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnInit(init); + } + + function init() { + switch (property0) { + case "Late Update": tree.notifyOnLateUpdate(update); + #if arm_physics + case "Physics Pre-Update": PhysicsWorld.active.notifyOnPreUpdate(update); + #end + default: tree.notifyOnUpdate(update); /* Update */ + } + } + + function update() { + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnVirtualButtonNode.hx b/Sources/armory/logicnode/OnVirtualButtonNode.hx new file mode 100644 index 0000000000..e7a3da89ab --- /dev/null +++ b/Sources/armory/logicnode/OnVirtualButtonNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +class OnVirtualButtonNode extends LogicNode { + + public var property0: String; + public var property1: String; + + @:deprecated("The 'On Virtual Button' node is deprecated and will be removed in future SDK versions. Please use 'Virtual Button' instead.") + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var vb = iron.system.Input.getVirtualButton(property1); + if (vb == null) return; + var b = false; + switch (property0) { + case "Down": + b = vb.down; + case "Started": + b = vb.started; + case "Released": + b = vb.released; + } + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OnVolumeTriggerNode.hx b/Sources/armory/logicnode/OnVolumeTriggerNode.hx new file mode 100644 index 0000000000..20f4026c4a --- /dev/null +++ b/Sources/armory/logicnode/OnVolumeTriggerNode.hx @@ -0,0 +1,52 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; + +class OnVolumeTriggerNode extends LogicNode { + + public var property0: String; + var lastOverlap = false; + + var l1 = new Vec4(); + var l2 = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var object: Object = inputs[0].get(); + var volume: Object = inputs[1].get(); + + if (object == null) return; + if (volume == null) volume = tree.object; + + var t1 = object.transform; + var t2 = volume.transform; + l1.set(t1.worldx(), t1.worldy(), t1.worldz()); + l2.set(t2.worldx(), t2.worldy(), t2.worldz()); + var d1 = t1.dim; + var d2 = t2.dim; + + var overlap = l1.x + d1.x / 2 > l2.x - d2.x / 2 && l1.x - d1.x / 2 < l2.x + d2.x / 2 && + l1.y + d1.y / 2 > l2.y - d2.y / 2 && l1.y - d1.y / 2 < l2.y + d2.y / 2 && + l1.z + d1.z / 2 > l2.z - d2.z / 2 && l1.z - d1.z / 2 < l2.z + d2.z / 2; + + var b = false; + switch (property0) { + case "begin": + b = overlap && !lastOverlap; + case "overlap": + b = overlap; + case "end": + b = !overlap && lastOverlap; + } + + lastOverlap = overlap; + + if (b) runOutput(0); + } +} diff --git a/Sources/armory/logicnode/OncePerFrameNode.hx b/Sources/armory/logicnode/OncePerFrameNode.hx new file mode 100644 index 0000000000..0e2aadd584 --- /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/ParseFloatNode.hx b/Sources/armory/logicnode/ParseFloatNode.hx new file mode 100644 index 0000000000..6fe0bb4a19 --- /dev/null +++ b/Sources/armory/logicnode/ParseFloatNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class ParseFloatNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s: String = inputs[0].get(); + if (s == null) return null; + + return Std.parseFloat(s); + } +} diff --git a/Sources/armory/logicnode/ParseIntNode.hx b/Sources/armory/logicnode/ParseIntNode.hx new file mode 100644 index 0000000000..3aad5cf205 --- /dev/null +++ b/Sources/armory/logicnode/ParseIntNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class ParseIntNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s: String = inputs[0].get(); + if (s == null) return null; + + return Std.parseInt(s); + } +} diff --git a/Sources/armory/logicnode/PauseActionNode.hx b/Sources/armory/logicnode/PauseActionNode.hx new file mode 100644 index 0000000000..5bd76f384c --- /dev/null +++ b/Sources/armory/logicnode/PauseActionNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.Object; + +class PauseActionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.pause(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PauseActiveCameraRenderNode.hx b/Sources/armory/logicnode/PauseActiveCameraRenderNode.hx new file mode 100644 index 0000000000..670a172cd3 --- /dev/null +++ b/Sources/armory/logicnode/PauseActiveCameraRenderNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +import iron.RenderPath; + +class PauseActiveCameraRenderNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + final isPaused: Bool = inputs[1].get(); + + RenderPath.active.paused = isPaused; + runOutput(0); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/PauseSoundNode.hx b/Sources/armory/logicnode/PauseSoundNode.hx new file mode 100644 index 0000000000..9c712df92e --- /dev/null +++ b/Sources/armory/logicnode/PauseSoundNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +import iron.object.SpeakerObject; + +class PauseSoundNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: SpeakerObject = cast(inputs[1].get(), SpeakerObject); + if (object == null) return; + object.pause(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PauseTilesheetNode.hx b/Sources/armory/logicnode/PauseTilesheetNode.hx new file mode 100644 index 0000000000..55566edb82 --- /dev/null +++ b/Sources/armory/logicnode/PauseTilesheetNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class PauseTilesheetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + + if (object == null) return; + + object.tilesheet.pause(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PauseTraitNode.hx b/Sources/armory/logicnode/PauseTraitNode.hx new file mode 100644 index 0000000000..7dc3c084fc --- /dev/null +++ b/Sources/armory/logicnode/PauseTraitNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class PauseTraitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var trait: Dynamic = inputs[1].get(); + if (trait == null || !Std.isOfType(trait, LogicTree)) return; + + cast(trait, LogicTree).pause(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PhysicsConstraintNode.hx b/Sources/armory/logicnode/PhysicsConstraintNode.hx new file mode 100644 index 0000000000..a06eaa5181 --- /dev/null +++ b/Sources/armory/logicnode/PhysicsConstraintNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; + +#if arm_physics +import armory.trait.physics.bullet.PhysicsConstraint.ConstraintAxis; +#end + +class PhysicsConstraintNode extends LogicNode { + + public var property0: String; //Linear or Angular + public var property1: String; //Axis + public var property2: Bool; //Is a spring + +#if arm_physics + public var value1: Float; //Lower limit or Spring Stiffness + public var value2: Float; //Upper limit or Spring Damping + public var isAngular: Bool; + public var axis: ConstraintAxis; + public var isSpring: Bool; +#end + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): PhysicsConstraintNode { +#if arm_physics + value1 = inputs[0].get(); + value2 = inputs[1].get(); + + isAngular = property0 != "Linear"; + isSpring = property2; + + switch (property1) { + case "X": axis = X; + case "Y": axis = Y; + case "Z": axis = Z; + } +#end + return this; + } +} diff --git a/Sources/armory/logicnode/PhysicsConvexCastNode.hx b/Sources/armory/logicnode/PhysicsConvexCastNode.hx new file mode 100644 index 0000000000..f91da74bc2 --- /dev/null +++ b/Sources/armory/logicnode/PhysicsConvexCastNode.hx @@ -0,0 +1,50 @@ +package armory.logicnode; + +#if arm_physics +import armory.trait.physics.RigidBody; +#end +import iron.object.Object; +import iron.math.Vec4; +import iron.math.Quat; + +class PhysicsConvexCastNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var convex: Object = inputs[0].get(); + var vfrom: Vec4 = inputs[1].get(); + var vto: Vec4 = inputs[2].get(); + var rot: Quat = inputs[3].get(); + var mask: Int = inputs[4].get(); + + if (vfrom == null || vto == null) return null; + +#if arm_physics + var rb = convex.getTrait(RigidBody); + if(rb == null) return null; + var physics = armory.trait.physics.PhysicsWorld.active; + var hit = physics.convexSweepTest(rb, vfrom, vto, rot, mask); + + if (from == 0) { // Hit Position + if (hit != null) return hit.pos; + } + else if (from == 1) { // RB Position + if (hit != null) { + var d = Vec4.distance(vfrom, vto); + var v = new Vec4(); + v.subvecs(vto, vfrom).normalize(); + v.mult(d * hit.hitFraction); + v.add(vfrom); + return v; + } + } + else if (from == 2) { // Hit + if (hit != null) return hit.normal; + } +#end + return null; + } +} diff --git a/Sources/armory/logicnode/PhysicsConvexCastOnNode.hx b/Sources/armory/logicnode/PhysicsConvexCastOnNode.hx new file mode 100644 index 0000000000..62b69a3610 --- /dev/null +++ b/Sources/armory/logicnode/PhysicsConvexCastOnNode.hx @@ -0,0 +1,68 @@ +package armory.logicnode; + +#if arm_physics +import armory.trait.physics.RigidBody; +#end +import iron.object.Object; +import iron.math.Vec4; +import iron.math.Quat; + +class PhysicsConvexCastOnNode extends LogicNode { + + var hitPos: Vec4 = null; + var convexPos: Vec4 = null; + var hitNormal: Vec4 = null; + + public function new(tree: LogicTree) { + super(tree); + } + + function reset() { + hitPos = null; + convexPos = null; + hitNormal = null; + } + + override function run(from:Int) { + reset(); + + var convex: Object = inputs[1].get(); + var vfrom: Vec4 = inputs[2].get(); + var vto: Vec4 = inputs[3].get(); + var rot: Quat = inputs[4].get(); + var mask: Int = inputs[5].get(); + +#if arm_physics + if (vfrom != null && vto != null) { + var rb = convex.getTrait(RigidBody); + var physics = armory.trait.physics.PhysicsWorld.active; + var hit = physics.convexSweepTest(rb, vfrom, vto, rot, mask); + if(hit != null) { + hitPos = new Vec4().setFrom(hit.pos); + var d = Vec4.distance(vfrom, vto); + var v = new Vec4(); + v.subvecs(vto, vfrom).normalize(); + v.mult(d * hit.hitFraction); + v.add(vfrom); + convexPos = new Vec4().setFrom(v); + + hitNormal = new Vec4().setFrom(physics.hitNormalWorld); + } + } +#end + runOutput(0); + } + + override function get(from: Int): Dynamic { + if (from == 1) { // Hit Position + return hitPos; + } + else if (from == 2) { // RB Position + return convexPos; + } + else { // Normal + return hitNormal; + } + return null; + } +} diff --git a/Sources/armory/logicnode/PickLocationNode.hx b/Sources/armory/logicnode/PickLocationNode.hx new file mode 100644 index 0000000000..5cb9eeb530 --- /dev/null +++ b/Sources/armory/logicnode/PickLocationNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; + +class PickLocationNode extends LogicNode { + + var loc = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var coords: Vec4 = inputs[1].get(); + + if (object == null || coords == null) return null; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var b = physics.pickClosest(coords.x, coords.y); + var rb = object.getTrait(armory.trait.physics.RigidBody); + + if (rb != null && b == rb) { + var p = physics.hitPointWorld; + loc.set(p.x, p.y, p.z); + return loc; + } +#end + return null; + } +} diff --git a/Sources/armory/logicnode/PickObjectNode.hx b/Sources/armory/logicnode/PickObjectNode.hx new file mode 100644 index 0000000000..f1903cfd6c --- /dev/null +++ b/Sources/armory/logicnode/PickObjectNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class PickObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var coords: Vec4 = inputs[0].get(); + var mask: Int = inputs[1].get(); + + if (coords == null) return null; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + var rb = physics.pickClosest(coords.x, coords.y, mask); + if (rb == null) return null; + + if (from == 0) { // Object + return rb.object; + } + else if(from == 1){ // Hit + var v = new Vec4(); + return v.set(physics.hitPointWorld.x, physics.hitPointWorld.y, physics.hitPointWorld.z); + } + else { // Normal + var v = new Vec4(); + return v.set(physics.hitNormalWorld.x, physics.hitNormalWorld.y, physics.hitNormalWorld.z, 0); + } +#end + return null; + } +} diff --git a/Sources/armory/logicnode/PlayActionFromNode.hx b/Sources/armory/logicnode/PlayActionFromNode.hx new file mode 100644 index 0000000000..10d9623bcd --- /dev/null +++ b/Sources/armory/logicnode/PlayActionFromNode.hx @@ -0,0 +1,136 @@ +package armory.logicnode; + +import iron.object.Animation; +import iron.object.Object; +import iron.Scene; +import kha.arrays.Float32Array; +import iron.object.ObjectAnimation; + +class PlayActionFromNode extends LogicNode { + + var animation: Animation; + var startFrame: Int; + var endFrame: Int = -1; + var loop: Bool; + var reverse: Bool; + var actionR: String; + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnUpdate(update); + } + + function update() { + if (animation != null) { + if (animation.currentFrame() == endFrame) { + if (loop) animation.setFrame(startFrame); + else { + if (!animation.paused) { + animation.pause(); + runOutput(1); + } + } + } + } + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var action: String = inputs[2].get(); + startFrame = inputs[3].get(); + endFrame = inputs[4].get(); + var blendTime: Float = inputs[5].get(); + var speed: Float = inputs[6].get(); + loop = inputs[7].get(); + reverse = inputs[8].get(); + + if (object == null) return; + animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + if (reverse){ + var isnew = true; + actionR = action+'Reverse'; + + if (animation.isSkinned){ + for(a in animation.armature.actions) + if (a.name == actionR) isnew = false; + + if (isnew){ + for(a in animation.armature.actions) + if(a.name == action) + animation.armature.actions.push({ + name: actionR, + bones: a.bones, + mats: null}); + + for(a in animation.armature.actions) + if(a.name == actionR){ + for(bone in a.bones){ + var val: Array = []; + var v = bone.anim.tracks[0]; + var len: Int = v.values.length; + var l = Std.int(len/16); + for(i in 0...l) + for(j in 0...16) + val.push(v.values[(l-i)*16+j-16]); + for(i in 0...v.values.length) + v.values[i] = val[i]; + } + + var castBoneAnim = cast(animation, iron.object.BoneAnimation); + castBoneAnim.data.geom.actions.set(actionR, a.bones); + castBoneAnim.data.geom.mats.set(actionR, castBoneAnim.data.geom.mats.get(action)); + + for(o in iron.Scene.active.raw.objects) + if (o.name == object.name) o.bone_actions.push('action_'+o.bone_actions[0].split('_')[1]+'_'+actionR); + } + } + } + else { + + var oaction = null; + var tracks: Array = []; + + var oactions = cast(animation, ObjectAnimation).oactions; + + for (a in oactions) + if (a.objects[0].name == actionR) isnew = false; + + if (isnew){ + for (a in oactions){ + if (a.objects[0].name == action){ + oaction = a.objects[0]; + for(b in a.objects[0].anim.tracks){ + var val: Array = []; + for(c in b.values) val.push(c); + val.reverse(); + var vali = new Float32Array(val.length); + for(i in 0...val.length) vali[i] = val[i]; + tracks.push({target: b.target, frames: b.frames, values: vali}); + } + + oactions.push({ + objects: [{name: actionR, + anim: {begin: oaction.anim.begin, end: oaction.anim.end, tracks: tracks}, + type: 'object', + data_ref: '', + transform: null}]}); + + for(o in iron.Scene.active.raw.objects) + if (o.name == object.name) o.object_actions.push('action_'+actionR); + } + } + } + } + } + + animation.play(reverse ? actionR : action, function() { + runOutput(1); + }, blendTime, speed, loop); + animation.update(startFrame * Scene.active.raw.frame_time); + + runOutput(0); + + } +} diff --git a/Sources/armory/logicnode/PlayActionNode.hx b/Sources/armory/logicnode/PlayActionNode.hx new file mode 100644 index 0000000000..61afa79485 --- /dev/null +++ b/Sources/armory/logicnode/PlayActionNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.object.Object; + +class PlayActionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var action: String = inputs[2].get(); + var blendTime: Float = inputs[3].get(); + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.play(action, function() { + runOutput(1); + }, blendTime); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PlaySoundNode.hx b/Sources/armory/logicnode/PlaySoundNode.hx new file mode 100644 index 0000000000..9b32174299 --- /dev/null +++ b/Sources/armory/logicnode/PlaySoundNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +import iron.object.SpeakerObject; + +class PlaySoundNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: SpeakerObject = cast(inputs[1].get(), SpeakerObject); + if (object == null) return; + object.play(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx new file mode 100644 index 0000000000..2f5325db0f --- /dev/null +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -0,0 +1,82 @@ +package armory.logicnode; + +class PlaySoundRawNode extends LogicNode { + + /** The name of the sound */ + public var property0: String; + /** Whether to loop the playback */ + public var property1: Bool; + /** Retrigger */ + public var property2: Bool; + /** Override sample rate */ + public var property3: Bool; + /** Playback sample rate */ + public var property4: Int; + /** Whether to stream the sound from disk **/ + public var property5: Bool; + + var sound: kha.Sound = null; + var channel: kha.audio1.AudioChannel = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + switch (from) { + case Play: + if (sound == null) { + iron.data.Data.getSound(property0, function(s: kha.Sound) { + this.sound = s; + }); + } + + // Resume + if (channel != null) { + if (property2) channel.stop(); + channel.play(); + channel.volume = inputs[4].get(); + } + // Start + else if (sound != null) { + if (property3) sound.sampleRate = property4; + channel = iron.system.Audio.play(sound, property1, property5); + channel.volume = inputs[4].get(); + } + + tree.notifyOnUpdate(this.onUpdate); + runOutput(0); + + case Pause: + if (channel != null) channel.pause(); + tree.removeUpdate(this.onUpdate); + + case Stop: + if (channel != null) channel.stop(); + tree.removeUpdate(this.onUpdate); + runOutput(2); + + case UpdateVolume: + if (channel != null) channel.volume = inputs[4].get(); + } + } + + function onUpdate() { + if (channel != null) { + // Done + if (channel.finished) { + channel = null; + runOutput(2); + } + // Running + else runOutput(1); + } + } +} + +private enum abstract PlayState(Int) from Int to Int { + var Play = 0; + var Pause = 1; + var Stop = 2; + var UpdateVolume = 3; +} diff --git a/Sources/armory/logicnode/PlayTilesheetNode.hx b/Sources/armory/logicnode/PlayTilesheetNode.hx new file mode 100644 index 0000000000..6c06b94c28 --- /dev/null +++ b/Sources/armory/logicnode/PlayTilesheetNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class PlayTilesheetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var action: String = inputs[2].get(); + + if (object == null) return; + + object.tilesheet.play(action, function() { + runOutput(1); + }); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PrintNode.hx b/Sources/armory/logicnode/PrintNode.hx new file mode 100644 index 0000000000..f4e89f6215 --- /dev/null +++ b/Sources/armory/logicnode/PrintNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class PrintNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var value: Dynamic = inputs[1].get(); + + #if (arm_debug) + trace(tree.name + ": " + value); + #else + trace(value); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/PulseNode.hx b/Sources/armory/logicnode/PulseNode.hx new file mode 100644 index 0000000000..6497868d0a --- /dev/null +++ b/Sources/armory/logicnode/PulseNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.system.Time; + +class PulseNode extends LogicNode { + + var running = false; + var interval: Float; + var lastTick: Null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + if (from == 0) { + // Start + interval = inputs[2].get(); + + if (!running) { + tree.notifyOnUpdate(update); + running = true; + } + } + else if (from == 1) { + // Stop + if (running) { + tree.removeUpdate(update); + running = false; + } + } + } + + function update() { + var tick = Time.time(); + + if (lastTick == null || tick - lastTick > interval) { + runOutput(0); + lastTick = tick; + } + } +} diff --git a/Sources/armory/logicnode/QuaternionMathNode.hx b/Sources/armory/logicnode/QuaternionMathNode.hx new file mode 100644 index 0000000000..a2f210185f --- /dev/null +++ b/Sources/armory/logicnode/QuaternionMathNode.hx @@ -0,0 +1,137 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Vec4; +import iron.math.Mat4; +import kha.FastFloat; + +class QuaternionMathNode extends LogicNode { + + public var property0: String; // Operation + public var property1: Bool; // Separator Out + var res_q = new Quat(); + var res_v = new Vec4(); + var res_f: FastFloat = 0.0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + switch (property0) { + // 1 argument: Module, Normalize + case "Module": { + var q: Quat = inputs[0].get(); + if (q == null) return null; + res_q.setFrom(q); + res_f = res_q.module(); + } + case "Normalize": { + var q: Quat = inputs[0].get(); + if (q == null) return null; + res_q.setFrom(q); + res_q = res_q.normalize(); + } + // Many arguments: Add, Subtract, DotProduct, Multiply + case "Add": { + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); + var i = 1; + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + res_q.x += res_v.x; + res_q.y += res_v.y; + res_q.z += res_v.z; + res_q.w += res_f; + i++; + } + } + case "Subtract": { + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); + var i = 1; + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + res_q.x -= res_v.x; + res_q.y -= res_v.y; + res_q.z -= res_v.z; + res_q.w -= res_f; + i++; + } + } + case "Multiply": { + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); + var i = 1; + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + res_f = inputs[2*i+1].get(); + if (res_v == null || res_f == null) return null; + var temp_q = new Quat(res_v.x, res_v.y, res_v.z, res_f); + res_q.mult(temp_q); + i++; + } + } + case "MultiplyFloats": { + res_v = inputs[0].get(); + res_f = inputs[1].get(); + if (res_v == null || res_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, res_f); + var f: Float = 1.0; + var i = 2; + while (i < inputs.length) { + f *= inputs[i].get(); + if (f == null) return null; + i++; + } + res_q.scale(f); + } + case "DotProduct": { // what this does with more than 2 terms is not *remotely* intuitive. Heck, you could consider it a footgun! + + res_v = inputs[0].get(); + var temp_f = inputs[1].get(); + if (res_v == null || temp_f == null) return null; + + res_q.set(res_v.x, res_v.y, res_v.z, temp_f); + var i = 1; + while (2*i+1 < inputs.length) { + res_v = inputs[2*i].get(); + temp_f = inputs[2*i+1].get(); + if (res_v == null || temp_f == null) return null; + var temp_q = new Quat(res_v.x, res_v.y, res_v.z, temp_f); + res_f = res_q.dot(temp_q); + res_q.set(res_f, res_f, res_f, res_f); + i++; + } + } + } + switch (from) { + case 0: { + return res_q; + } + case 1: + if (property0 == "DotProduct" || property0 == "Module") { + return res_f; + } else { + return null; + } + default: { + return null; + } + } + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/QuaternionNode.hx b/Sources/armory/logicnode/QuaternionNode.hx new file mode 100644 index 0000000000..bf83f4de4e --- /dev/null +++ b/Sources/armory/logicnode/QuaternionNode.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Vec4; + +class QuaternionNode extends LogicNode { + + var value = new Quat(); + + public function new(tree: LogicTree, x: Null = null, y: Null = null, z: Null = null, w: Null = null) { + super(tree); + + if (x != null) { + LogicNode.addLink(new FloatNode(tree, x), this, 0, 0); + LogicNode.addLink(new FloatNode(tree, y), this, 0, 1); + LogicNode.addLink(new FloatNode(tree, z), this, 0, 2); + LogicNode.addLink(new FloatNode(tree, w), this, 0, 3); + } + } + + override function get(from: Int): Dynamic { + value.x = inputs[0].get(); + value.y = inputs[1].get(); + value.z = inputs[2].get(); + value.w = inputs[3].get(); + value.normalize(); + switch (from){ + case 0: + return value; + case 1: + var value1 = new Vec4(); + value1.x = value.x; + value1.y = value.y; + value1.z = value.z; + value1.w = 0; // use 0 to avoid this vector being translated. + return value1; + case 2: + return value.w; + default: + return null; + } + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/RadToDegNode.hx b/Sources/armory/logicnode/RadToDegNode.hx new file mode 100644 index 0000000000..7e746e6936 --- /dev/null +++ b/Sources/armory/logicnode/RadToDegNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class RadToDegNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var rad: Float = inputs[0].get(); + return rad * 57.29578; + } +} diff --git a/Sources/armory/logicnode/RandomBooleanNode.hx b/Sources/armory/logicnode/RandomBooleanNode.hx new file mode 100644 index 0000000000..0c2927a99a --- /dev/null +++ b/Sources/armory/logicnode/RandomBooleanNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class RandomBooleanNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return Std.random(2) == 0; + } +} diff --git a/Sources/armory/logicnode/RandomChoiceNode.hx b/Sources/armory/logicnode/RandomChoiceNode.hx new file mode 100644 index 0000000000..dc07ac2936 --- /dev/null +++ b/Sources/armory/logicnode/RandomChoiceNode.hx @@ -0,0 +1,14 @@ +package armory.logicnode; + +class RandomChoiceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var array: Array = inputs[0].get(); + + return array[Std.random(array.length)]; + } +} diff --git a/Sources/armory/logicnode/RandomColorNode.hx b/Sources/armory/logicnode/RandomColorNode.hx new file mode 100644 index 0000000000..230c787dc7 --- /dev/null +++ b/Sources/armory/logicnode/RandomColorNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class RandomColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var r = Math.random(); + var g = Math.random(); + var b = Math.random(); + // var a = Math.random(); + var v = new Vec4(r, g, b); + return v; + } +} diff --git a/Sources/armory/logicnode/RandomFloatNode.hx b/Sources/armory/logicnode/RandomFloatNode.hx new file mode 100644 index 0000000000..f76699556c --- /dev/null +++ b/Sources/armory/logicnode/RandomFloatNode.hx @@ -0,0 +1,14 @@ +package armory.logicnode; + +class RandomFloatNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var min: Float = inputs[0].get(); + var max: Float = inputs[1].get(); + return min + (Math.random() * (max - min)); + } +} diff --git a/Sources/armory/logicnode/RandomIntegerNode.hx b/Sources/armory/logicnode/RandomIntegerNode.hx new file mode 100644 index 0000000000..6bf81e4a25 --- /dev/null +++ b/Sources/armory/logicnode/RandomIntegerNode.hx @@ -0,0 +1,14 @@ +package armory.logicnode; + +class RandomIntegerNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var min: Int = inputs[0].get(); + var max: Int = inputs[1].get(); + return min + Std.random(max - min); + } +} diff --git a/Sources/armory/logicnode/RandomOutputNode.hx b/Sources/armory/logicnode/RandomOutputNode.hx new file mode 100644 index 0000000000..cfa7c34445 --- /dev/null +++ b/Sources/armory/logicnode/RandomOutputNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class RandomOutputNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + runOutput(Std.random(outputs.length)); + } +} diff --git a/Sources/armory/logicnode/RandomStringNode.hx b/Sources/armory/logicnode/RandomStringNode.hx new file mode 100644 index 0000000000..da918b6c8c --- /dev/null +++ b/Sources/armory/logicnode/RandomStringNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +using StringTools; + +class RandomStringNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var length: Int = inputs[0].get(); + var characters: String = inputs[1].get(); + + var buf = new StringBuf(); + + while(buf.length < length) { + buf.addChar(characters.fastCodeAt(Std.random(characters.length))); + } + + return buf.toString(); + } +} diff --git a/Sources/armory/logicnode/RandomVectorNode.hx b/Sources/armory/logicnode/RandomVectorNode.hx new file mode 100644 index 0000000000..90381563d9 --- /dev/null +++ b/Sources/armory/logicnode/RandomVectorNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class RandomVectorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var v = new Vec4(); + + var min: Vec4 = inputs[0].get(); + var max: Vec4 = inputs[1].get(); + var x = min.x + (Math.random() * (max.x - min.x)); + var y = min.y + (Math.random() * (max.y - min.y)); + var z = min.z + (Math.random() * (max.z - min.z)); + v.set(x, y, z); + return v; + } +} diff --git a/Sources/armory/logicnode/RaycastClosestObjectNode.hx b/Sources/armory/logicnode/RaycastClosestObjectNode.hx new file mode 100644 index 0000000000..03f2acea2c --- /dev/null +++ b/Sources/armory/logicnode/RaycastClosestObjectNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.RayCaster; +import iron.object.Object; +import iron.object.CameraObject; + +class RaycastClosestObjectNode extends LogicNode { + + var o: Object; + var v = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var objects: Array = inputs[1].get(); + var inputX: Float = inputs[2].get(); + var inputY: Float = inputs[3].get(); + var camera: CameraObject = inputs[4].get(); + + o = RayCaster.closestBoxIntersectObject(objects, inputX, inputY, camera); + if (o != null) + v = RayCaster.boxIntersectObject(o, inputX, inputY, camera); + + if (o == null) runOutput(2); else runOutput(1); + + runOutput(0); + } + + override function get(from: Int): Dynamic { + switch (from) { + case 3: return o; + case 4: if(o == null) return null; else return v; + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/RaycastObjectNode.hx b/Sources/armory/logicnode/RaycastObjectNode.hx new file mode 100644 index 0000000000..9afe0b390b --- /dev/null +++ b/Sources/armory/logicnode/RaycastObjectNode.hx @@ -0,0 +1,32 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.RayCaster; +import iron.object.Object; +import iron.object.CameraObject; + +class RaycastObjectNode extends LogicNode { + + var v = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var o: Object = inputs[1].get(); + var inputX: Float = inputs[2].get(); + var inputY: Float = inputs[3].get(); + var camera: CameraObject = inputs[4].get(); + + v = RayCaster.boxIntersectObject(o, inputX, inputY, camera); + + if (v == null) runOutput(2); else runOutput(1); + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return v; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/ReadFileNode.hx b/Sources/armory/logicnode/ReadFileNode.hx new file mode 100644 index 0000000000..e8d4f9639b --- /dev/null +++ b/Sources/armory/logicnode/ReadFileNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +class ReadFileNode extends LogicNode { + + var data: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + // Relative or absolute path to file + var file: String = inputs[1].get(); + + // Whether to use the cache + var useCache: Bool = inputs[2].get(); + + // Load the file asynchronously + if (!useCache && iron.data.Data.cachedBlobs.get(file) != null) iron.data.Data.cachedBlobs.remove(file); + iron.data.Data.getBlob(file, function(b: kha.Blob) { + data = b.toString(); + if (!useCache) iron.data.Data.cachedBlobs.remove(file); + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + return data; + } +} diff --git a/Sources/armory/logicnode/ReadJsonNode.hx b/Sources/armory/logicnode/ReadJsonNode.hx new file mode 100644 index 0000000000..1c7c06dea5 --- /dev/null +++ b/Sources/armory/logicnode/ReadJsonNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +class ReadJsonNode extends LogicNode { + + var data: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + // Relative or absolute path to file + var file: String = inputs[1].get(); + + // Whether to use the cache + var useCache: Bool = inputs[2].get(); + + // Load the file asynchronously + if (!useCache && iron.data.Data.cachedBlobs.get(file) != null) iron.data.Data.cachedBlobs.remove(file); + iron.data.Data.getBlob(file, function(b: kha.Blob) { + data = haxe.Json.parse(b.toString()); + if (!useCache) iron.data.Data.cachedBlobs.remove(file); + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + return data; + } +} diff --git a/Sources/armory/logicnode/ReadStorageNode.hx b/Sources/armory/logicnode/ReadStorageNode.hx new file mode 100644 index 0000000000..40f60a49db --- /dev/null +++ b/Sources/armory/logicnode/ReadStorageNode.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +class ReadStorageNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var key: String = inputs[0].get(); + + var data: Dynamic = iron.system.Storage.data; + if (data == null) return null; + + var value: Dynamic = Reflect.field(data, key); + + if (value == null) { + value = parseArg(inputs[1].get()); + } + + return value; + } + + static function parseArg(str: String): Dynamic { + if (str == "true") return true; + else if (str == "false") return false; + else if (str.charAt(0) == "'") return StringTools.replace(str, "'", ""); + else if (str.charAt(0) == "[") { // Array + // Remove [] and recursively parse into array, + // then append into parent + str = StringTools.replace(str, "[", ""); + str = StringTools.replace(str, "]", ""); + str = StringTools.replace(str, " ", ""); + var ar: Dynamic = []; + var s = str.split(","); + for (childStr in s) { + ar.push(parseArg(childStr)); + } + return ar; + } + else { + var f = Std.parseFloat(str); + var i = Std.parseInt(str); + return f == i ? i : f; + } + } +} diff --git a/Sources/armory/logicnode/RemoveActiveSceneNode.hx b/Sources/armory/logicnode/RemoveActiveSceneNode.hx new file mode 100644 index 0000000000..2a24e01ca7 --- /dev/null +++ b/Sources/armory/logicnode/RemoveActiveSceneNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class RemoveActiveSceneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + iron.Scene.active.remove(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RemoveGroupNode.hx b/Sources/armory/logicnode/RemoveGroupNode.hx new file mode 100644 index 0000000000..78d2b00a47 --- /dev/null +++ b/Sources/armory/logicnode/RemoveGroupNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +class RemoveGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var groupName: String = inputs[1].get(); + var raw = iron.Scene.active.raw; + + for (g in raw.groups) { + if (g.name == groupName) { + raw.groups.remove(g); + @:privateAccess iron.Scene.active.groups.remove(groupName); + break; + } + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RemoveInputMapKeyNode.hx b/Sources/armory/logicnode/RemoveInputMapKeyNode.hx new file mode 100644 index 0000000000..44708c6d20 --- /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/RemoveMapKeyNode.hx b/Sources/armory/logicnode/RemoveMapKeyNode.hx new file mode 100644 index 0000000000..bcb1251eca --- /dev/null +++ b/Sources/armory/logicnode/RemoveMapKeyNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + + +class RemoveMapKeyNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from: Int) { + var map: Map = inputs[1].get(); + if (map == null) return; + var key: Dynamic = inputs[2].get(); + map.remove(key); + runOutput(0); + } + +} + diff --git a/Sources/armory/logicnode/RemoveObjectFromGroupNode.hx b/Sources/armory/logicnode/RemoveObjectFromGroupNode.hx new file mode 100644 index 0000000000..563249e1bc --- /dev/null +++ b/Sources/armory/logicnode/RemoveObjectFromGroupNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.object.Object; + +class RemoveObjectFromGroupNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var groupName: String = inputs[1].get(); + var object: Object = inputs[2].get(); + + iron.Scene.active.getGroup(groupName).remove(object); + + runOutput(0); + + } +} diff --git a/Sources/armory/logicnode/RemoveObjectNode.hx b/Sources/armory/logicnode/RemoveObjectNode.hx new file mode 100644 index 0000000000..72746cc161 --- /dev/null +++ b/Sources/armory/logicnode/RemoveObjectNode.hx @@ -0,0 +1,37 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class RemoveObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var removeChildren: Bool = inputs[2].get(); + var keepChildrenTransforms: Bool = inputs[3].get(); + + if (object == null) return; + + if (removeChildren == false) { + for (c in object.children.copy()) { + c.setParent(iron.Scene.active.root, false, keepChildrenTransforms); + + #if arm_physics + var rigidBody = c.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + } + } + + var raw = iron.Scene.active.raw; + for (g in raw.groups) + iron.Scene.active.getGroup(g.name).remove(object); + + object.remove(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RemoveParentBoneNode.hx b/Sources/armory/logicnode/RemoveParentBoneNode.hx new file mode 100644 index 0000000000..c7d1601956 --- /dev/null +++ b/Sources/armory/logicnode/RemoveParentBoneNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.object.Object; + +class RemoveParentBoneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + #if arm_skin + + var object: Object = inputs[1].get(); + var parent: Object = inputs[2].get(); + var bone: String = inputs[3].get(); + + if (object == null || parent == null) return; + + var banim = object.getParentArmature(object.parent.name); + banim.removeBoneChild(bone, object); + + runOutput(0); + + #end + } +} diff --git a/Sources/armory/logicnode/RemovePhysicsNode.hx b/Sources/armory/logicnode/RemovePhysicsNode.hx new file mode 100644 index 0000000000..d71acd644f --- /dev/null +++ b/Sources/armory/logicnode/RemovePhysicsNode.hx @@ -0,0 +1,25 @@ + package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class RemovePhysicsNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + + if (object == null) return; + +#if arm_physics + var rigidBody = object.getTrait(RigidBody); + + rigidBody.remove(); +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RemoveTraitNode.hx b/Sources/armory/logicnode/RemoveTraitNode.hx new file mode 100644 index 0000000000..b70ee56e8d --- /dev/null +++ b/Sources/armory/logicnode/RemoveTraitNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class RemoveTraitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var trait: Dynamic = inputs[1].get(); + if (trait == null) return; + trait.remove(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RemoveTraitObjectNode.hx b/Sources/armory/logicnode/RemoveTraitObjectNode.hx new file mode 100644 index 0000000000..32e1885b90 --- /dev/null +++ b/Sources/armory/logicnode/RemoveTraitObjectNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.object.Object; + +class RemoveTraitObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var traitName: String = inputs[2].get(); + + assert(Error, object != null, "Object should not be null"); + assert(Error, traitName != null, "Trait name should not be null"); + + var cname = Type.resolveClass(Main.projectPackage + "." + traitName); + if (cname == null) cname = Type.resolveClass(Main.projectPackage + ".node." + traitName); + assert(Error, cname != null, 'No trait with the name "$traitName" found, make sure that the trait is exported!'); + + var trait = object.getTrait(cname); + + if(trait != null) + object.removeTrait(trait); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ResumeActionNode.hx b/Sources/armory/logicnode/ResumeActionNode.hx new file mode 100644 index 0000000000..3cc291ac9c --- /dev/null +++ b/Sources/armory/logicnode/ResumeActionNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.Object; + +class ResumeActionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ResumeTilesheetNode.hx b/Sources/armory/logicnode/ResumeTilesheetNode.hx new file mode 100644 index 0000000000..fbb7b8cb5a --- /dev/null +++ b/Sources/armory/logicnode/ResumeTilesheetNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class ResumeTilesheetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + + if (object == null) return; + + object.tilesheet.resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ResumeTraitNode.hx b/Sources/armory/logicnode/ResumeTraitNode.hx new file mode 100644 index 0000000000..c8a6201185 --- /dev/null +++ b/Sources/armory/logicnode/ResumeTraitNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class ResumeTraitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var trait: Dynamic = inputs[1].get(); + if (trait == null || !Std.isOfType(trait, LogicTree)) return; + + cast(trait, LogicTree).resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RetainValueNode.hx b/Sources/armory/logicnode/RetainValueNode.hx new file mode 100644 index 0000000000..82e94ea49c --- /dev/null +++ b/Sources/armory/logicnode/RetainValueNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class RetainValueNode extends LogicNode { + + var value: Dynamic = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + value = inputs[1].get(); + + runOutput(0); + } + + override function get(from:Int):Dynamic { + return value; + } +} diff --git a/Sources/armory/logicnode/RotateObjectAroundAxisNode.hx b/Sources/armory/logicnode/RotateObjectAroundAxisNode.hx new file mode 100644 index 0000000000..dd44d32ced --- /dev/null +++ b/Sources/armory/logicnode/RotateObjectAroundAxisNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +class RotateObjectAroundAxisNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var axis: Vec4 = inputs[2].get(); + var angle: Float = inputs[3].get(); + + if (object == null || axis == null) return; + + // the rotate function already calls buildMatrix + object.transform.rotate(axis.normalize(), angle); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RotateObjectNode.hx b/Sources/armory/logicnode/RotateObjectNode.hx new file mode 100644 index 0000000000..b880cf06cf --- /dev/null +++ b/Sources/armory/logicnode/RotateObjectNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Quat; +import armory.trait.physics.RigidBody; + +class RotateObjectNode extends LogicNode { + + public var property0 = "Local"; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var q: Quat = inputs[2].get(); + + if (object == null || q == null) return; + + q.normalize(); + switch (property0){ + case "Local": + object.transform.rot.mult(q); + case "Global": + object.transform.rot.multquats(q, object.transform.rot); + // that function call (Quat.multquats) is weird: it both modifies the object, and returns `this` + } + + object.transform.buildMatrix(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RotateRenderTargetNode.hx b/Sources/armory/logicnode/RotateRenderTargetNode.hx new file mode 100644 index 0000000000..5de11b8a8c --- /dev/null +++ b/Sources/armory/logicnode/RotateRenderTargetNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import armory.renderpath.RenderToTexture; + +class RotateRenderTargetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("RotateRenderTargetNode"); + + final angle: Float = inputs[1].get(); + final centerX: Float = inputs[2].get(); + final centerY: Float = inputs[3].get(); + final revert: Bool = inputs[4].get(); + + //Rotate render target + RenderToTexture.g.rotate(angle, centerX, centerY); + + //Execute all outputs + runOutput(0); + + //Revert rotation if enabled + if(revert) RenderToTexture.g.rotate(-angle, centerX, centerY); + } +} diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx new file mode 100644 index 0000000000..2af24da5cd --- /dev/null +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -0,0 +1,96 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Vec4; +import iron.math.Mat4; +import kha.FastFloat; + +class RotationMathNode extends LogicNode { + + public var property0: String; // Operation + var res_q = new Quat(); + var res_v = new Vec4(); + var res_f: FastFloat = 0.0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + //var q: Quat = inputs[0].get(); + //if (q==null) return null; + + //var res_q: Quat = new Quat(); + switch (property0) { + // 1 argument: Normalize, Inverse + case "Normalize": { + var q: Quat = inputs[0].get(); + if (q==null) return null; + res_q.setFrom(q); + res_q = res_q.normalize(); + } + case "Inverse": { + var q: Quat = inputs[0].get(); + if (q==null) return null; + var modl = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w; + modl = -1/modl; + res_q.w = -q.w*modl; + res_q.x = q.x*modl; + res_q.y = q.y*modl; + res_q.z = q.z*modl; + } + // 2 arguments: Compose, Amplify, FromTo, FromRotationMat, + case "FromTo": { + var v1: Vec4 = inputs[0].get(); + var v2: Vec4 = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.fromTo(v1, v2); + } + case "Compose": { + var v1: Quat = inputs[0].get(); + var v2: Quat = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.multquats(v1,v2); + } + case "Amplify": { + var v1: Quat = inputs[0].get(); + var v2: Float = inputs[1].get(); + if ((v1 == null) || (v2 == null)) return null; + res_q.setFrom(v1); + var fac2 = Math.sqrt(1- res_q.w*res_q.w); + if (fac2 > 0.001) { + var fac1 = v2*Math.acos(res_q.w); + res_q.w = Math.cos(fac1); + fac1 = Math.sin(fac1)/fac2; + res_q.x *= fac1; + res_q.y *= fac1; + res_q.z *= fac1; + } + } + //case "FromRotationMat": { + // var m: Mat4 = inputs[1].get(); + // if (m == null) return null; + + // res_q = res_q.fromMat(m); + //} + // # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler + case "Lerp": { + //var from = q; + var from: Quat = inputs[0].get(); + var to: Quat = inputs[1].get(); + var f: Float = inputs[2].get(); + if ((from == null) || (f == null) || (to == null)) return null; + res_q = res_q.lerp(from, to, f); + } + case "Slerp": { + //var from = q; + var from:Quat = inputs[0].get(); + var to: Quat = inputs[1].get(); + var f: Float = inputs[2].get(); + if ((from == null) || (f == null) || (to == null)) return null; + res_q = res_q.slerp(from, to, f); + } + } + return res_q; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/RotationNode.hx b/Sources/armory/logicnode/RotationNode.hx new file mode 100644 index 0000000000..717cd009e8 --- /dev/null +++ b/Sources/armory/logicnode/RotationNode.hx @@ -0,0 +1,101 @@ +package armory.logicnode; + +import armory.math.Helper; +import iron.math.Vec4; +import iron.math.Quat; +import kha.FastFloat; + +class RotationNode extends LogicNode { + + public var property0: String; // type of input (EulerAngles, AxisAngle, Quaternion) + public var property1: String; // angle unit (Deg, Rad) + public var property2: String; // euler order (XYZ, XZY, etc…) + + public var value: Quat; + + public function new(tree: LogicTree, + x: Null = null, y: Null = null, + z: Null = null, w: Null = null + ) { + super(tree); + + this.value = new Quat(); + if (x != null) this.value.set(x, y, z, w); + } + + override function get(from: Int): Dynamic { + if (inputs.length == 0) { + // This node has no inputs if it is an implicitely added node + // for a socket's default value + return this.value; + } + + switch (property0) { + case "Quaternion": + var vect: Vec4 = inputs[0].get(); + value.x = vect.x; + value.y = vect.y; + value.z = vect.z; + value.w = inputs[1].get(); + + case "AxisAngle": + var vec: Vec4 = inputs[0].get(); + var angle: FastFloat = inputs[1].get(); + if (property1 == "Deg") { + angle = Helper.degToRad(angle); + } + value.fromAxisAngle(vec, angle); + + case "EulerAngles": + var vec: Vec4 = new Vec4().setFrom(inputs[0].get()); + if (property1 == "Deg") { + vec.x = Helper.degToRad(vec.x); + vec.y = Helper.degToRad(vec.y); + vec.z = Helper.degToRad(vec.z); + } + this.value.fromEulerOrdered(vec, property2); + + default: + throw 'Unsupported rotation type ${property0}'; + } + return this.value; + } + + override function set(value: Dynamic) { + if (inputs.length == 0) { + this.value.setFrom(value); + return; + } + + switch (property0) { + case "Quaternion": + var vect = new Vec4(); + vect.x = value.x; + vect.y = value.y; + vect.z = value.z; + inputs[0].set(vect); + inputs[1].set(value.w); + + case "AxisAngle": + var vec = new Vec4(); + var angle = this.value.toAxisAngle(vec); + if (property1 == "Deg") { + angle = Helper.radToDeg(angle); + } + inputs[0].set(vec); + inputs[1].set(angle); + + case "EulerAngles": + var vec: Vec4 = value.toEulerOrdered(property2); + if (property1 == "Deg") { + vec.x = Helper.radToDeg(vec.x); + vec.y = Helper.radToDeg(vec.y); + vec.z = Helper.radToDeg(vec.z); + } + inputs[0].set(vec); + + default: + throw 'Unsupported rotation type ${property0}'; + } + } +} diff --git a/Sources/armory/logicnode/RpConfigNode.hx b/Sources/armory/logicnode/RpConfigNode.hx new file mode 100644 index 0000000000..534b50c18f --- /dev/null +++ b/Sources/armory/logicnode/RpConfigNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +class RpConfigNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int){ + var on: Bool = inputs[1].get(); + + switch (property0) { + case "SSGI": + on ? armory.data.Config.raw.rp_ssgi = true : armory.data.Config.raw.rp_ssgi = false; + case "SSR": + on ? armory.data.Config.raw.rp_ssr = true : armory.data.Config.raw.rp_ssr = false; + case "Bloom": + on ? armory.data.Config.raw.rp_bloom = true : armory.data.Config.raw.rp_bloom = false; + case "GI": + on ? armory.data.Config.raw.rp_voxels = true : armory.data.Config.raw.rp_voxels = false; + case "Motion Blur": + on ? armory.data.Config.raw.rp_motionblur = true : armory.data.Config.raw.rp_motionblur = false; + } + armory.renderpath.RenderPathCreator.applyConfig(); + armory.data.Config.save(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RpMSAANode.hx b/Sources/armory/logicnode/RpMSAANode.hx new file mode 100644 index 0000000000..827e936a2b --- /dev/null +++ b/Sources/armory/logicnode/RpMSAANode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +class RpMSAANode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int){ + + switch (property0) { + case "1": + armory.data.Config.raw.window_msaa = 1; + case "2": + armory.data.Config.raw.window_msaa = 2; + case "4": + armory.data.Config.raw.window_msaa = 4; + case "8": + armory.data.Config.raw.window_msaa = 8; + case "16": + armory.data.Config.raw.window_msaa = 16; + } + armory.renderpath.RenderPathCreator.applyConfig(); + armory.data.Config.save(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RpShadowQualityNode.hx b/Sources/armory/logicnode/RpShadowQualityNode.hx new file mode 100644 index 0000000000..f31f84f338 --- /dev/null +++ b/Sources/armory/logicnode/RpShadowQualityNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +class RpShadowQualityNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int){ + + switch (property0) { + case "High": + armory.data.Config.raw.rp_shadowmap_cascade = 4096; + case "Medium": + armory.data.Config.raw.rp_shadowmap_cascade = 2048; + case "Low": + armory.data.Config.raw.rp_shadowmap_cascade = 1024; + } + + armory.renderpath.RenderPathCreator.applyConfig(); + armory.data.Config.save(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/RpSuperSampleNode.hx b/Sources/armory/logicnode/RpSuperSampleNode.hx new file mode 100644 index 0000000000..26f5d60ec2 --- /dev/null +++ b/Sources/armory/logicnode/RpSuperSampleNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +class RpSuperSampleNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int){ + + switch (property0) { + case "1": + armory.data.Config.raw.rp_supersample = 1; + case "1.5": + armory.data.Config.raw.rp_supersample = 1.5; + case "2": + armory.data.Config.raw.rp_supersample = 2; + case "4": + armory.data.Config.raw.rp_supersample = 4; + } + armory.renderpath.RenderPathCreator.applyConfig(); + armory.data.Config.save(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SSAOGetNode.hx b/Sources/armory/logicnode/SSAOGetNode.hx new file mode 100644 index 0000000000..021f010d94 --- /dev/null +++ b/Sources/armory/logicnode/SSAOGetNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class SSAOGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + + return switch (from) { + case 0: armory.renderpath.Postprocess.ssao_uniforms[0]; + case 1: armory.renderpath.Postprocess.ssao_uniforms[1]; + case 2: armory.renderpath.Postprocess.ssao_uniforms[2]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/SSAOSetNode.hx b/Sources/armory/logicnode/SSAOSetNode.hx new file mode 100644 index 0000000000..373e2e65d7 --- /dev/null +++ b/Sources/armory/logicnode/SSAOSetNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class SSAOSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + armory.renderpath.Postprocess.ssao_uniforms[0] = inputs[1].get(); + armory.renderpath.Postprocess.ssao_uniforms[1] = inputs[2].get(); + armory.renderpath.Postprocess.ssao_uniforms[2] = inputs[3].get(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SSRGetNode.hx b/Sources/armory/logicnode/SSRGetNode.hx new file mode 100644 index 0000000000..7fd2b32fc9 --- /dev/null +++ b/Sources/armory/logicnode/SSRGetNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class SSRGetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from:Int):Dynamic { + + return switch (from) { + case 0: armory.renderpath.Postprocess.ssr_uniforms[0]; + case 1: armory.renderpath.Postprocess.ssr_uniforms[1]; + case 2: armory.renderpath.Postprocess.ssr_uniforms[2]; + case 3: armory.renderpath.Postprocess.ssr_uniforms[3]; + case 4: armory.renderpath.Postprocess.ssr_uniforms[4]; + default: 0.0; + } + } +} diff --git a/Sources/armory/logicnode/SSRSetNode.hx b/Sources/armory/logicnode/SSRSetNode.hx new file mode 100644 index 0000000000..800942a265 --- /dev/null +++ b/Sources/armory/logicnode/SSRSetNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class SSRSetNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + armory.renderpath.Postprocess.ssr_uniforms[0] = inputs[1].get(); + armory.renderpath.Postprocess.ssr_uniforms[1] = inputs[2].get(); + armory.renderpath.Postprocess.ssr_uniforms[2] = inputs[3].get(); + armory.renderpath.Postprocess.ssr_uniforms[3] = inputs[4].get(); + armory.renderpath.Postprocess.ssr_uniforms[4] = inputs[5].get(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ScaleObjectNode.hx b/Sources/armory/logicnode/ScaleObjectNode.hx new file mode 100644 index 0000000000..16c54ac86e --- /dev/null +++ b/Sources/armory/logicnode/ScaleObjectNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +class ScaleObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var vec: Vec4 = inputs[2].get(); + + if (object == null || vec == null) return; + + object.transform.scale.add(vec); + object.transform.buildMatrix(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SceneNode.hx b/Sources/armory/logicnode/SceneNode.hx new file mode 100644 index 0000000000..5dc7d9bd77 --- /dev/null +++ b/Sources/armory/logicnode/SceneNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class SceneNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return property0; + } + + override function set(value: Dynamic) { + this.property0 = value; + } +} diff --git a/Sources/armory/logicnode/SceneRootNode.hx b/Sources/armory/logicnode/SceneRootNode.hx new file mode 100644 index 0000000000..263e759736 --- /dev/null +++ b/Sources/armory/logicnode/SceneRootNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class SceneRootNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return iron.Scene.active.root; + } +} diff --git a/Sources/armory/logicnode/ScreenToWorldSpaceNode.hx b/Sources/armory/logicnode/ScreenToWorldSpaceNode.hx new file mode 100644 index 0000000000..956a44099a --- /dev/null +++ b/Sources/armory/logicnode/ScreenToWorldSpaceNode.hx @@ -0,0 +1,74 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.RayCaster; + +class ScreenToWorldSpaceNode extends LogicNode { + + public var property0: Bool; // Separator Out + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vInput: Vec4 = new Vec4(); + vInput.x = inputs[0].get(); + vInput.y = inputs[1].get(); + + var cam = iron.Scene.active.camera; + if (cam == null) return null; + + // Separator Out + if (property0) { + switch (from) { + // World + case 0: { + return RayCaster.getRay(vInput.x, vInput.y, cam).origin; + } + // World X + case 1: { + return RayCaster.getRay(vInput.x, vInput.y, cam).origin.x; + } + // World Y + case 2: { + return RayCaster.getRay(vInput.x, vInput.y, cam).origin.y; + } + // World Z + case 3: { + return RayCaster.getRay(vInput.x, vInput.y, cam).origin.z; + } + // Direction + case 4: { + return RayCaster.getRay(vInput.x, vInput.y, cam).direction.normalize(); + } + // Direction X + case 5: { + return RayCaster.getRay(vInput.x, vInput.y, cam).direction.normalize().x; + } + // Direction Y + case 6: { + return RayCaster.getRay(vInput.x, vInput.y, cam).direction.normalize().y; + } + // Direction Z + case 7: { + return RayCaster.getRay(vInput.x, vInput.y, cam).direction.normalize().z; + } + } + } + else + { + switch (from) { + // World + case 0: { + return RayCaster.getRay(vInput.x, vInput.y, cam).origin; + } + // Direction + case 1: { + return RayCaster.getRay(vInput.x, vInput.y, cam).direction.normalize(); + } + } + } + return null; + } +} diff --git a/Sources/armory/logicnode/ScriptNode.hx b/Sources/armory/logicnode/ScriptNode.hx new file mode 100644 index 0000000000..1a50f4b3e9 --- /dev/null +++ b/Sources/armory/logicnode/ScriptNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +class ScriptNode extends LogicNode { + + public var property0: String; + var result: Dynamic; + + #if hscript + var parser: hscript.Parser = null; + var interp: hscript.Interp = null; + var ast: hscript.Expr = null; + #end + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var v: Dynamic = inputs[1].get(); + + #if hscript + if (parser == null) { + parser = new hscript.Parser(); + parser.allowJSON = true; + parser.allowTypes = true; + ast = parser.parseString(property0); + interp = new hscript.Interp(); + interp.variables.set("Math", Math); + interp.variables.set("Std", Std); + } + interp.variables.set("input", v); + result = interp.execute(ast); + #end + + runOutput(0); + } + + override function get(from: Int): Dynamic { + return result; + } +} diff --git a/Sources/armory/logicnode/SelectNode.hx b/Sources/armory/logicnode/SelectNode.hx new file mode 100644 index 0000000000..6d78f51ea7 --- /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() == null ? -1 : 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/SelfNode.hx b/Sources/armory/logicnode/SelfNode.hx new file mode 100644 index 0000000000..f8db60a4a8 --- /dev/null +++ b/Sources/armory/logicnode/SelfNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class SelfNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return tree.object; + } +} diff --git a/Sources/armory/logicnode/SelfTraitNode.hx b/Sources/armory/logicnode/SelfTraitNode.hx new file mode 100644 index 0000000000..f6de93cbec --- /dev/null +++ b/Sources/armory/logicnode/SelfTraitNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class SelfTraitNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return tree; + } +} diff --git a/Sources/armory/logicnode/SendEventNode.hx b/Sources/armory/logicnode/SendEventNode.hx new file mode 100644 index 0000000000..0353173d4c --- /dev/null +++ b/Sources/armory/logicnode/SendEventNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.system.Event; + +class SendEventNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + final name: String = inputs[1].get(); + final object: Object = inputs.length > 2 ? inputs[2].get() : tree.object; + + if (object != null) { + final entries = Event.get(name); + + if (entries != null) { + for (e in entries) if (e.mask == object.uid) e.onEvent(); + } + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SendGlobalEventNode.hx b/Sources/armory/logicnode/SendGlobalEventNode.hx new file mode 100644 index 0000000000..61bb17b097 --- /dev/null +++ b/Sources/armory/logicnode/SendGlobalEventNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import armory.system.Event; + +class SendGlobalEventNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + final name: String = inputs[1].get(); + final entries = Event.get(name); + + if (entries != null) { + for (e in entries) e.onEvent(); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SensorCoordsNode.hx b/Sources/armory/logicnode/SensorCoordsNode.hx new file mode 100644 index 0000000000..618b6ce1b1 --- /dev/null +++ b/Sources/armory/logicnode/SensorCoordsNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SensorCoordsNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var sensor = iron.system.Input.getSensor(); + coords.x = sensor.x; + coords.y = sensor.y; + coords.z = sensor.z; + return coords; + } +} diff --git a/Sources/armory/logicnode/SeparateColorHSVNode.hx b/Sources/armory/logicnode/SeparateColorHSVNode.hx new file mode 100644 index 0000000000..b7e36b126e --- /dev/null +++ b/Sources/armory/logicnode/SeparateColorHSVNode.hx @@ -0,0 +1,47 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SeparateColorHSVNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vector: Vec4 = inputs[0].get(); + if (vector == null) return 0.0; + + var r = vector.x; + var g = vector.y; + var b = vector.z; + var a = vector.w; + + + var max = Math.max(Math.max(r, g), b); + var min = Math.min(Math.min(r, g), b); + var h: Float = max; + var s: Float = max; + var v: Float = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if(max == min){ + h = 0; // achromatic + }else{ + if(max == r) h = (g - b) / d + (g < b ? 6 : 0); + else if(max == g) h = (b - r) / d + 2; + else h = (r - g) / d + 4; + h /= 6; + } + + return switch (from) { + case 0: h; + case 1: s; + case 2: v; + case 3: a; + default: throw "Unreachable"; + } + } +} diff --git a/Sources/armory/logicnode/SeparateColorNode.hx b/Sources/armory/logicnode/SeparateColorNode.hx new file mode 100644 index 0000000000..e363a9037a --- /dev/null +++ b/Sources/armory/logicnode/SeparateColorNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SeparateColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vector: Vec4 = inputs[0].get(); + if (vector == null) return 0.0; + + return switch (from) { + case 0: vector.x; + case 1: vector.y; + case 2: vector.z; + case 3: vector.w; + default: throw "Unreachable"; + } + } +} diff --git a/Sources/armory/logicnode/SeparateQuaternionNode.hx b/Sources/armory/logicnode/SeparateQuaternionNode.hx new file mode 100644 index 0000000000..fac7b69ebe --- /dev/null +++ b/Sources/armory/logicnode/SeparateQuaternionNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.math.Quat; +import kha.FastFloat; + +class SeparateQuaternionNode extends LogicNode { + var q:Quat = null; + + public function new(tree:LogicTree) { super(tree); } + + override function get(from:Int):Dynamic{ + q = inputs[0].get(); + if (from==0) + return q.x; + else if (from==1) + return q.y; + else if (from==2) + return q.z; + else + return q.w; + + } +} diff --git a/Sources/armory/logicnode/SeparateRotationNode.hx b/Sources/armory/logicnode/SeparateRotationNode.hx new file mode 100644 index 0000000000..0d366d79c9 --- /dev/null +++ b/Sources/armory/logicnode/SeparateRotationNode.hx @@ -0,0 +1,60 @@ +package armory.logicnode; + +import kha.FastFloat; +import iron.math.Quat; +import iron.math.Vec4; + +class SeparateRotationNode extends LogicNode { + + public var property0 = "EulerAngles"; // EulerAngles, AxisAngle, or Quat + public var property1 = "Rad"; // Rad or Deg + public var property2 = "XYZ"; + + static inline var toDEG:FastFloat = 57.29577951308232; // 180/pi + + var input_cache = new Quat(); + var euler_cache = new Vec4(); + var aa_axis_cache = new Vec4(); + var aa_angle_cache: Float = 0; + + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var q: Quat = inputs[0].get(); + if (q == null) return null; + q.normalize(); + + switch (property0) { + case "EulerAngles": + if (q!=this.input_cache) + euler_cache = q.toEulerOrdered(property2); + if (from>0) + return null; + + switch (property1){ + case "Rad": return euler_cache; + case "Deg": return new Vec4(euler_cache.x*toDEG, euler_cache.y*toDEG, euler_cache.z*toDEG); + } + + case "AxisAngle": + if (q!=this.input_cache) + aa_angle_cache = q.toAxisAngle(aa_axis_cache); + switch (from){ + case 0: return aa_axis_cache; + case 1: switch(property1){ + case "Rad": return aa_angle_cache; + case "Deg": return toDEG*aa_angle_cache; + } + } + case "Quaternion": + switch(from){ + case 0: return new Vec4(q.x,q.y,q.z); + case 1: return q.w; + } + } + return null; + } +} diff --git a/Sources/armory/logicnode/SeparateTransformNode.hx b/Sources/armory/logicnode/SeparateTransformNode.hx new file mode 100644 index 0000000000..ac4ea14496 --- /dev/null +++ b/Sources/armory/logicnode/SeparateTransformNode.hx @@ -0,0 +1,26 @@ +package armory.logicnode; + +import iron.math.Mat4; +import iron.math.Vec4; +import iron.math.Quat; + +class SeparateTransformNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var matrix: Mat4 = inputs[0].get(); + if (matrix == null) return null; + + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + matrix.decompose(loc, rot, scale); + + if (from == 0) return loc; + else if (from == 1) return rot; + else return scale; + } +} diff --git a/Sources/armory/logicnode/SeparateVectorNode.hx b/Sources/armory/logicnode/SeparateVectorNode.hx new file mode 100644 index 0000000000..72276fc66e --- /dev/null +++ b/Sources/armory/logicnode/SeparateVectorNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SeparateVectorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var vector: Vec4 = inputs[0].get(); + if (vector == null) return 0.0; + + if (from == 0) return vector.x; + else if (from == 1) return vector.y; + else return vector.z; + } +} diff --git a/Sources/armory/logicnode/SequenceNode.hx b/Sources/armory/logicnode/SequenceNode.hx new file mode 100644 index 0000000000..3513f2ffca --- /dev/null +++ b/Sources/armory/logicnode/SequenceNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class SequenceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + for (i in 0...outputs.length) runOutput(i); + } +} diff --git a/Sources/armory/logicnode/SetActionPausedNode.hx b/Sources/armory/logicnode/SetActionPausedNode.hx new file mode 100644 index 0000000000..dae303d289 --- /dev/null +++ b/Sources/armory/logicnode/SetActionPausedNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetActionPausedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var paused: Bool = inputs [2].get(); + + if (object == null) return; + + var animation = object.animation; + + if (animation == null) animation = object.getParentArmature(object.name); + + paused ? animation.pause() : animation.resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetActionSpeedNode.hx b/Sources/armory/logicnode/SetActionSpeedNode.hx new file mode 100644 index 0000000000..af34a66154 --- /dev/null +++ b/Sources/armory/logicnode/SetActionSpeedNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetActionSpeedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var speed: Float = inputs[2].get(); + + if (object == null) return; + var animation = object.animation; + if (animation == null) animation = object.getParentArmature(object.name); + + animation.speed = speed; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetActivationStateNode.hx b/Sources/armory/logicnode/SetActivationStateNode.hx new file mode 100644 index 0000000000..25dd2e871f --- /dev/null +++ b/Sources/armory/logicnode/SetActivationStateNode.hx @@ -0,0 +1,43 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +/** +define ISLAND_SLEEPING 2 +define WANTS_DEACTIVATION 3 +**/ + +class SetActivationStateNode extends LogicNode { + + public var property0: String; + public var state: Int; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + if (object == null) return; + +#if arm_physics + var rigidBody = object.getTrait(RigidBody); + + switch (property0) { + case "inactive": + state = 0; // Inactive Tag + case "active": + state = 1; // Active Tag + case "always active": + state = 4 ; // Disable Deactivation + case "always inactive": + state = 5; // Disable Simulation + } + rigidBody.setActivationState(state); + +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetAreaLightSizeNode.hx b/Sources/armory/logicnode/SetAreaLightSizeNode.hx new file mode 100644 index 0000000000..1dd9e9efcd --- /dev/null +++ b/Sources/armory/logicnode/SetAreaLightSizeNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.LightObject; + +class SetAreaLightSizeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var light: LightObject = inputs[1].get(); + var size: Float = inputs[2].get(); + var size_y: Float = inputs[3].get(); + + if (light == null) return; + + light.data.raw.size = size; + light.data.raw.size_y = size_y; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx b/Sources/armory/logicnode/SetBoneFkIkOnlyNode.hx new file mode 100644 index 0000000000..4d9b59fbc6 --- /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/SetCameraAspectNode.hx b/Sources/armory/logicnode/SetCameraAspectNode.hx new file mode 100644 index 0000000000..1b41b44562 --- /dev/null +++ b/Sources/armory/logicnode/SetCameraAspectNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class SetCameraAspectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + var aspect: Float = inputs[2].get(); + + if (camera == null) return; + + camera.data.raw.aspect = aspect; + camera.buildProjection(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetCameraFovNode.hx b/Sources/armory/logicnode/SetCameraFovNode.hx new file mode 100644 index 0000000000..0e416865af --- /dev/null +++ b/Sources/armory/logicnode/SetCameraFovNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class SetCameraFovNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + var fov: Float = inputs[2].get(); + + if (camera == null) return; + + camera.data.raw.fov = fov; + camera.buildProjection(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetCameraNode.hx b/Sources/armory/logicnode/SetCameraNode.hx new file mode 100644 index 0000000000..f2a183f49d --- /dev/null +++ b/Sources/armory/logicnode/SetCameraNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class SetCameraNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + if (camera == null) return; + camera.buildProjection(); + + iron.Scene.active.camera = camera; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetCameraScaleNode.hx b/Sources/armory/logicnode/SetCameraScaleNode.hx new file mode 100644 index 0000000000..2c28da778f --- /dev/null +++ b/Sources/armory/logicnode/SetCameraScaleNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.object.CameraObject; +import kha.arrays.Float32Array; + +class SetCameraScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + var scale: Float = inputs[2].get(); + + if (camera == null) return; + + var aspect = camera.data.raw.aspect != null ? camera.data.raw.aspect : iron.App.w() / iron.App.h(); + var ortho = new Float32Array(4); + ortho[0] = - scale / 2; + ortho[1] = scale / 2; + ortho[2] = - scale / (2 * aspect); + ortho[3] = scale / (2 * aspect); + camera.data.raw.ortho = ortho; + + camera.buildProjection(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetCameraStartEndNode.hx b/Sources/armory/logicnode/SetCameraStartEndNode.hx new file mode 100644 index 0000000000..630df4764c --- /dev/null +++ b/Sources/armory/logicnode/SetCameraStartEndNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import iron.object.CameraObject; + +class SetCameraStartEndNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + + assert(Error, Std.isOfType(camera, CameraObject), "Camera must be a camera object!"); + + if (camera == null) return; + + if (property0 == 'Start'){ + var start: Float = inputs[2].get(); + camera.data.raw.near_plane = start; + } + else if (property0 == 'End' ){ + var end: Float = inputs[2].get(); + camera.data.raw.far_plane = end; + } + else { + var start: Float = inputs[2].get(); + camera.data.raw.near_plane = start; + var end: Float = inputs[3].get(); + camera.data.raw.far_plane = end; + } + + camera.buildProjection(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetCameraTypeNode.hx b/Sources/armory/logicnode/SetCameraTypeNode.hx new file mode 100644 index 0000000000..98f99709ce --- /dev/null +++ b/Sources/armory/logicnode/SetCameraTypeNode.hx @@ -0,0 +1,42 @@ +package armory.logicnode; + +import iron.object.CameraObject; +import kha.arrays.Float32Array; + +class SetCameraTypeNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var camera: CameraObject = inputs[1].get(); + var prop: Float = inputs[2].get(); + + if (camera == null) return; + + if (property0 == 'Perspective'){ + camera.data.raw.ortho = null; + camera.data.raw.fov = prop; + camera.data.raw.near_plane = inputs[3].get(); + camera.data.raw.far_plane = inputs[4].get(); + } + else { + var aspect = camera.data.raw.aspect != null ? camera.data.raw.aspect : iron.App.w() / iron.App.h(); + camera.data.raw.fov = null; + var ortho = new Float32Array(4); + ortho[0] = - prop / 2; + ortho[1] = prop / 2; + ortho[2] = - prop / (2 * aspect); + ortho[3] = prop / (2 * aspect); + camera.data.raw.ortho = ortho; + camera.data.raw.near_plane = - camera.data.raw.far_plane; + } + + camera.buildProjection(); + + runOutput(0); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/SetCursorStateNode.hx b/Sources/armory/logicnode/SetCursorStateNode.hx new file mode 100644 index 0000000000..22a0d36368 --- /dev/null +++ b/Sources/armory/logicnode/SetCursorStateNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +class SetCursorStateNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var state: Bool = inputs[1].get(); + var mouse = iron.system.Input.getMouse(); + + switch (property0) { + case "hide locked": + state ? mouse.hide() : mouse.show(); + mouse.hidden ? mouse.lock() : mouse.unlock(); + + case "hide": + state ? mouse.hide() : mouse.show(); + + case "lock": + state ? mouse.lock() : mouse.unlock(); + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetDebugConsoleSettings.hx b/Sources/armory/logicnode/SetDebugConsoleSettings.hx new file mode 100644 index 0000000000..cf64b558f9 --- /dev/null +++ b/Sources/armory/logicnode/SetDebugConsoleSettings.hx @@ -0,0 +1,32 @@ +package armory.logicnode; +import armory.trait.internal.DebugConsole; + +class SetDebugConsoleSettings extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + #if arm_debug + var visible: Dynamic = inputs[1].get(); + armory.trait.internal.DebugConsole.setVisible(visible); + + var scale: Dynamic = inputs[2].get(); + if ((scale >= 0.3) && (scale <= 10.0)) + armory.trait.internal.DebugConsole.setScale(scale); + + switch (property0) { + case "left": + return armory.trait.internal.DebugConsole.setPosition(PositionStateEnum.Left); + case "center": + return armory.trait.internal.DebugConsole.setPosition(PositionStateEnum.Center); + case "right": + return armory.trait.internal.DebugConsole.setPosition(PositionStateEnum.Right); + } + #end + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetFrictionNode.hx b/Sources/armory/logicnode/SetFrictionNode.hx new file mode 100644 index 0000000000..31a697b635 --- /dev/null +++ b/Sources/armory/logicnode/SetFrictionNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class SetFrictionNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + + if (object == null) return; + + var friction = inputs[2].get(); + +#if arm_physics + var rigidBody = object.getTrait(RigidBody); + + rigidBody.setFriction(friction); +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetGlobalCanvasFontSizeNode.hx b/Sources/armory/logicnode/SetGlobalCanvasFontSizeNode.hx new file mode 100644 index 0000000000..4a61f6e8e2 --- /dev/null +++ b/Sources/armory/logicnode/SetGlobalCanvasFontSizeNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class SetGlobalCanvasFontSizeNode extends LogicNode { + + var canvas: CanvasScript; + var factor: Int; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + factor = inputs[1].get(); + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + canvas.setCanvasFontSize(factor); + runOutput(0); + } +#end +} diff --git a/Sources/armory/logicnode/SetGlobalCanvasScaleNode.hx b/Sources/armory/logicnode/SetGlobalCanvasScaleNode.hx new file mode 100644 index 0000000000..c7be7af6ff --- /dev/null +++ b/Sources/armory/logicnode/SetGlobalCanvasScaleNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.Scene; +import armory.trait.internal.CanvasScript; + +class SetGlobalCanvasScaleNode extends LogicNode { + + var canvas: CanvasScript; + var factor: Float; + + public function new(tree: LogicTree) { + super(tree); + } + +#if arm_ui + override function run(from: Int) { + factor = inputs[1].get(); + canvas = Scene.active.getTrait(CanvasScript); + if (canvas == null) canvas = Scene.active.camera.getTrait(CanvasScript); + + canvas.setUiScale(factor); + runOutput(0); + } +#end +} diff --git a/Sources/armory/logicnode/SetGravityEnabledNode.hx b/Sources/armory/logicnode/SetGravityEnabledNode.hx new file mode 100644 index 0000000000..49a09be577 --- /dev/null +++ b/Sources/armory/logicnode/SetGravityEnabledNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.object.Object; +#if arm_physics +import armory.trait.physics.RigidBody; +#end + +/** + Enable or disable the gravity for a specific object. +**/ +class SetGravityEnabledNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var gravityEnabled: Bool = inputs[2].get(); + if (object == null) return; + + #if arm_physics + var body = object.getTrait(RigidBody); + if (body != null) { + if (gravityEnabled) { + body.enableGravity(); + } + else { + body.disableGravity(); + } + } + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetGravityNode.hx b/Sources/armory/logicnode/SetGravityNode.hx new file mode 100644 index 0000000000..0b390cd8e3 --- /dev/null +++ b/Sources/armory/logicnode/SetGravityNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SetGravityNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var gravity: Vec4 = inputs[1].get(); + + if (gravity == null) return; + +#if arm_physics + var physics = armory.trait.physics.PhysicsWorld.active; + physics.setGravity(gravity); +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetHaxePropertyNode.hx b/Sources/armory/logicnode/SetHaxePropertyNode.hx new file mode 100644 index 0000000000..d874992492 --- /dev/null +++ b/Sources/armory/logicnode/SetHaxePropertyNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class SetHaxePropertyNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Dynamic = inputs[1].get(); + var property: String = inputs[2].get(); + var value: Dynamic = inputs[3].get(); + + if (object == null) return; + + Reflect.setProperty(object, property, value); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetInputMapKeyNode.hx b/Sources/armory/logicnode/SetInputMapKeyNode.hx new file mode 100644 index 0000000000..e657456d9b --- /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/SetLightColorNode.hx b/Sources/armory/logicnode/SetLightColorNode.hx new file mode 100644 index 0000000000..220ca5f66f --- /dev/null +++ b/Sources/armory/logicnode/SetLightColorNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.LightObject; + +class SetLightColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var light: LightObject = inputs[1].get(); + var color: iron.math.Vec4 = inputs[2].get(); + + if (light == null) return; + + light.data.raw.color[0] = color.x; + light.data.raw.color[1] = color.y; + light.data.raw.color[2] = color.z; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetLightStrengthNode.hx b/Sources/armory/logicnode/SetLightStrengthNode.hx new file mode 100644 index 0000000000..dceebf9668 --- /dev/null +++ b/Sources/armory/logicnode/SetLightStrengthNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.LightObject; + +class SetLightStrengthNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var light: LightObject = inputs[1].get(); + var strength: Float = inputs[2].get(); + + if (light == null) return; + + light.data.raw.strength = light.data.raw.type == "sun" ? strength * 0.325 : strength * 0.026; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetLocationNode.hx b/Sources/armory/logicnode/SetLocationNode.hx new file mode 100644 index 0000000000..4f879586d3 --- /dev/null +++ b/Sources/armory/logicnode/SetLocationNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +class SetLocationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + 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(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/SetMapValueNode.hx b/Sources/armory/logicnode/SetMapValueNode.hx new file mode 100644 index 0000000000..51ce02a182 --- /dev/null +++ b/Sources/armory/logicnode/SetMapValueNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + + +class SetMapValueNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + var map: Map = inputs[1].get(); + if (map == null) return null; + var key: Dynamic = inputs[2].get(); + var value: Dynamic = inputs[3].get(); + map[key] = value; + runOutput(0); + return; + } + +} diff --git a/Sources/armory/logicnode/SetMaterialImageParamNode.hx b/Sources/armory/logicnode/SetMaterialImageParamNode.hx new file mode 100644 index 0000000000..7d9ad9bbc3 --- /dev/null +++ b/Sources/armory/logicnode/SetMaterialImageParamNode.hx @@ -0,0 +1,48 @@ +package armory.logicnode; + +import iron.Scene; +import iron.data.MaterialData; +import iron.object.Object; +import armory.trait.internal.UniformsManager; + +class SetMaterialImageParamNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + + } + + override function run(from: Int) { + var object: Object; + var perObject: Null; + var mat: MaterialData; + var link: String; + var img: String; + + object = inputs[1].get(); + if(object == null) return; + + perObject = inputs[2].get(); + if(perObject == null) perObject = false; + + mat = inputs[3].get(); + if(mat == null) return; + + link = inputs[4].get(); + if(link == null) return; + + img = inputs[5].get(); + if(img == null) return; + + if(! perObject){ + UniformsManager.removeTextureValue(object, mat, link); + object = Scene.active.root; + } + + iron.data.Data.getImage(img, function(image: kha.Image) { + UniformsManager.setTextureValue(mat, object, inputs[4].get(), image); + }); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetMaterialNode.hx b/Sources/armory/logicnode/SetMaterialNode.hx new file mode 100644 index 0000000000..09f89960ae --- /dev/null +++ b/Sources/armory/logicnode/SetMaterialNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.object.MeshObject; +import iron.data.MaterialData; + +class SetMaterialNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var mat: MaterialData = inputs[2].get(); + + if (object == null) return; + + for (i in 0...object.materials.length) { + object.materials[i] = mat; + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetMaterialRgbParamNode.hx b/Sources/armory/logicnode/SetMaterialRgbParamNode.hx new file mode 100644 index 0000000000..ac3fcdac39 --- /dev/null +++ b/Sources/armory/logicnode/SetMaterialRgbParamNode.hx @@ -0,0 +1,45 @@ +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 { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object; + var perObject: Null; + var mat: MaterialData; + var link: String; + var vec: Vec4; + + object = inputs[1].get(); + if(object == null) return; + + perObject = inputs[2].get(); + if(perObject == null) perObject = false; + + mat = inputs[3].get(); + if(mat == null) return; + + link = inputs[4].get(); + if(link == null) return; + + vec = inputs[5].get(); + if(vec == null) return; + + if(! perObject){ + UniformsManager.removeVectorValue(object, mat, link); + object = Scene.active.root; + } + + UniformsManager.setVec3Value(mat, object, inputs[4].get(), vec); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetMaterialSlotNode.hx b/Sources/armory/logicnode/SetMaterialSlotNode.hx new file mode 100644 index 0000000000..336389d5dc --- /dev/null +++ b/Sources/armory/logicnode/SetMaterialSlotNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + +import iron.object.MeshObject; +import iron.data.MaterialData; + +class SetMaterialSlotNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var mat: MaterialData = inputs[2].get(); + var slot: Int = inputs[3].get(); + + if (object == null) return; + if (slot >= object.materials.length) return; + + object.materials[slot] = mat; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetMaterialValueParamNode.hx b/Sources/armory/logicnode/SetMaterialValueParamNode.hx new file mode 100644 index 0000000000..42e043a37f --- /dev/null +++ b/Sources/armory/logicnode/SetMaterialValueParamNode.hx @@ -0,0 +1,45 @@ +package armory.logicnode; + +import iron.Scene; +import iron.data.MaterialData; +import iron.object.Object; +import armory.trait.internal.UniformsManager; + +class SetMaterialValueParamNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object; + var perObject: Null; + var mat: MaterialData; + var link: String; + var value: Null; + + object = inputs[1].get(); + if(object == null) return; + + perObject = inputs[2].get(); + if(perObject == null) perObject = false; + + mat = inputs[3].get(); + if(mat == null) return; + + link = inputs[4].get(); + if(link == null) return; + + value = inputs[5].get(); + if(value == null) return; + + if(! perObject){ + UniformsManager.removeFloatValue(object, mat, link); + object = Scene.active.root; + } + + UniformsManager.setFloatValue(mat, object, inputs[4].get(), value); + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/SetMeshNode.hx b/Sources/armory/logicnode/SetMeshNode.hx new file mode 100644 index 0000000000..71a70d6e5a --- /dev/null +++ b/Sources/armory/logicnode/SetMeshNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.MeshObject; +import iron.data.MeshData; + +class SetMeshNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var mesh: MeshData = inputs[2].get(); + + if (object == null) return; + + object.data = mesh; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetMouseLockNode.hx b/Sources/armory/logicnode/SetMouseLockNode.hx new file mode 100644 index 0000000000..cc999a2b9c --- /dev/null +++ b/Sources/armory/logicnode/SetMouseLockNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class SetMouseLockNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var lock: Bool = inputs[1].get(); + var mouse = iron.system.Input.getMouse(); + lock ? mouse.lock() : mouse.unlock(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetNameNode.hx b/Sources/armory/logicnode/SetNameNode.hx new file mode 100644 index 0000000000..630baa7a67 --- /dev/null +++ b/Sources/armory/logicnode/SetNameNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetNameNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var name: String = inputs[2].get(); + + if (object == null) return; + + object.name = name; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetObjectShapeKeyNode.hx b/Sources/armory/logicnode/SetObjectShapeKeyNode.hx new file mode 100644 index 0000000000..f0a22603eb --- /dev/null +++ b/Sources/armory/logicnode/SetObjectShapeKeyNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class SetObjectShapeKeyNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + #if arm_morph_target + var object: Dynamic = inputs[1].get(); + var shapeKey: String = inputs[2].get(); + var value: Dynamic = inputs[3].get(); + + assert(Error, object != null, "Object should not be null"); + var morph = cast(object, MeshObject).morphTarget; + + assert(Error, morph != null, "Object does not have shape keys"); + morph.setMorphValue(shapeKey, value); + #end + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetParentBoneNode.hx b/Sources/armory/logicnode/SetParentBoneNode.hx new file mode 100644 index 0000000000..7e0cfdeddb --- /dev/null +++ b/Sources/armory/logicnode/SetParentBoneNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetParentBoneNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + #if arm_skin + + var object: Object = inputs[1].get(); + var parent: Object = inputs[2].get(); + var bone: String = inputs[3].get(); + + if (object == null || parent == null) return; + + object.setParent(parent, false, false); + + var banim = object.getParentArmature(object.parent.name); + banim.addBoneChild(bone, object); + + runOutput(0); + + #end + } +} diff --git a/Sources/armory/logicnode/SetParentNode.hx b/Sources/armory/logicnode/SetParentNode.hx new file mode 100644 index 0000000000..71bccae564 --- /dev/null +++ b/Sources/armory/logicnode/SetParentNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class SetParentNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var parentObject: Object = inputs[2].get(); + var keepTransform: Bool = inputs[3].get(); + var parentInverse: Bool = inputs[4].get(); + + if (object == null || parentObject == null || object.parent == parentObject) return; + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.setActivationState(0); + #end + + object.setParent(parentObject, parentInverse, keepTransform); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetParticleSpeedNode.hx b/Sources/armory/logicnode/SetParticleSpeedNode.hx new file mode 100644 index 0000000000..7a9238b560 --- /dev/null +++ b/Sources/armory/logicnode/SetParticleSpeedNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetParticleSpeedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + #if arm_particles + var object: Object = inputs[1].get(); + var speed: Float = inputs[2].get(); + + if (object == null) return; + + var mo = cast(object, iron.object.MeshObject); + var psys = mo.particleSystems.length > 0 ? mo.particleSystems[0] : null; + if (psys == null) mo.particleOwner.particleSystems[0]; + + psys.speed = speed; + + runOutput(0); + #end + } +} diff --git a/Sources/armory/logicnode/SetPropertyNode.hx b/Sources/armory/logicnode/SetPropertyNode.hx new file mode 100644 index 0000000000..0ab25e19a2 --- /dev/null +++ b/Sources/armory/logicnode/SetPropertyNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetPropertyNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var property: String = inputs[2].get(); + var value: Dynamic = inputs[3].get(); + + if (object == null) return; + if (object.properties == null) object.properties = new Map(); + object.properties.set(property, value); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetRotationNode.hx b/Sources/armory/logicnode/SetRotationNode.hx new file mode 100644 index 0000000000..79f3a84a4e --- /dev/null +++ b/Sources/armory/logicnode/SetRotationNode.hx @@ -0,0 +1,34 @@ + +package armory.logicnode; + +import iron.object.Object; +import iron.math.Quat; +import armory.trait.physics.RigidBody; + +class SetRotationNode extends LogicNode { + + public var property0: String; // UNUSED + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + if (object == null) return; + var _q: Quat = inputs[2].get(); + if (_q == null) return; + + final q = new Quat(_q.x, _q.y, _q.z, _q.w).normalize(); + object.transform.rot.setFrom(q); + object.transform.buildMatrix(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) { + rigidBody.syncTransform(); + } + #end + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetScaleNode.hx b/Sources/armory/logicnode/SetScaleNode.hx new file mode 100644 index 0000000000..48f8bc6e32 --- /dev/null +++ b/Sources/armory/logicnode/SetScaleNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +class SetScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var vec: Vec4 = inputs[2].get(); + + if (object == null || vec == null) return; + + object.transform.scale.setFrom(vec); + object.transform.buildMatrix(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetSceneNode.hx b/Sources/armory/logicnode/SetSceneNode.hx new file mode 100644 index 0000000000..4114cffd56 --- /dev/null +++ b/Sources/armory/logicnode/SetSceneNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetSceneNode extends LogicNode { + + var root: Object; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var sceneName: String = inputs[1].get(); + + #if arm_json + sceneName += ".json"; + #elseif arm_compress + sceneName += ".lz4"; + #end + + iron.Scene.setActive(sceneName, function(o: iron.object.Object) { + root = o; + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + return root; + } +} diff --git a/Sources/armory/logicnode/SetShaderUniformNode.hx b/Sources/armory/logicnode/SetShaderUniformNode.hx new file mode 100644 index 0000000000..3a3f382abc --- /dev/null +++ b/Sources/armory/logicnode/SetShaderUniformNode.hx @@ -0,0 +1,65 @@ +package armory.logicnode; + +import iron.data.MaterialData; +import iron.object.Object; + +class SetShaderUniformNode extends LogicNode { + + static var registered = false; + static var intMap = new Map>(); + static var floatMap = new Map>(); + static var vec2Map = new Map(); + static var vec3Map = new Map(); + static var vec4Map = new Map(); + + /** Uniform type **/ + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + if (!registered) { + registered = true; + iron.object.Uniforms.externalIntLinks.push(intLink); + iron.object.Uniforms.externalFloatLinks.push(floatLink); + iron.object.Uniforms.externalVec2Links.push(vec2Link); + iron.object.Uniforms.externalVec3Links.push(vec3Link); + iron.object.Uniforms.externalVec4Links.push(vec4Link); + } + } + + override function run(from: Int) { + var uniformName: String = inputs[1].get(); + if (uniformName == null) return; + + switch (property0) { + case "int": intMap.set(uniformName, inputs[2].get()); + case "float": floatMap.set(uniformName, inputs[2].get()); + case "vec2": vec2Map.set(uniformName, inputs[2].get()); + case "vec3": vec3Map.set(uniformName, inputs[2].get()); + case "vec4": vec4Map.set(uniformName, inputs[2].get()); + default: + } + + runOutput(0); + } + + static function intLink(object: Object, mat: MaterialData, link: String): Null { + return intMap.get(link); + } + + static function floatLink(object: Object, mat: MaterialData, link: String): Null { + return floatMap.get(link); + } + + static function vec2Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + return vec2Map.get(link); + } + + static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + return vec3Map.get(link); + } + + static function vec4Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + return vec4Map.get(link); + } +} diff --git a/Sources/armory/logicnode/SetSpotLightBlendNode.hx b/Sources/armory/logicnode/SetSpotLightBlendNode.hx new file mode 100644 index 0000000000..93e9ef9cb1 --- /dev/null +++ b/Sources/armory/logicnode/SetSpotLightBlendNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.LightObject; + +class SetSpotLightBlendNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var light: LightObject = inputs[1].get(); + var blend: Float = inputs[2].get(); + + if (light == null) return; + + light.data.raw.spot_blend = blend; + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetSpotLightSizeNode.hx b/Sources/armory/logicnode/SetSpotLightSizeNode.hx new file mode 100644 index 0000000000..659c880713 --- /dev/null +++ b/Sources/armory/logicnode/SetSpotLightSizeNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.LightObject; + +class SetSpotLightSizeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var light: LightObject = inputs[1].get(); + var size: Float = inputs[2].get(); + + if (light == null) return; + + light.data.raw.spot_size = size; + + runOutput(0); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/SetTilesheetPausedNode.hx b/Sources/armory/logicnode/SetTilesheetPausedNode.hx new file mode 100644 index 0000000000..6b1d3cde84 --- /dev/null +++ b/Sources/armory/logicnode/SetTilesheetPausedNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class SetTilesheetPausedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var paused: Bool = inputs[2].get(); + + if (object == null) return; + + paused ? object.tilesheet.pause() : object.tilesheet.resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetTimeScaleNode.hx b/Sources/armory/logicnode/SetTimeScaleNode.hx new file mode 100644 index 0000000000..f9ef49433f --- /dev/null +++ b/Sources/armory/logicnode/SetTimeScaleNode.hx @@ -0,0 +1,14 @@ +package armory.logicnode; + +class SetTimeScaleNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var f: Float = inputs[1].get(); + iron.system.Time.scale = f; + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetTraitPausedNode.hx b/Sources/armory/logicnode/SetTraitPausedNode.hx new file mode 100644 index 0000000000..53327e1f14 --- /dev/null +++ b/Sources/armory/logicnode/SetTraitPausedNode.hx @@ -0,0 +1,19 @@ +package armory.logicnode; + +class SetTraitPausedNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var trait: Dynamic = inputs[1].get(); + var paused: Bool = inputs[2].get(); + + if (trait == null || !Std.isOfType(trait, LogicTree)) return; + + paused ? cast(trait, LogicTree).pause() : cast(trait, LogicTree).resume(); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetTransformNode.hx b/Sources/armory/logicnode/SetTransformNode.hx new file mode 100644 index 0000000000..3864669314 --- /dev/null +++ b/Sources/armory/logicnode/SetTransformNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Mat4; +import armory.trait.physics.RigidBody; + +class SetTransformNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var matrix: Mat4 = inputs[2].get(); + + if (object == null || matrix == null) return; + + object.transform.setMatrix(matrix); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetVariableNode.hx b/Sources/armory/logicnode/SetVariableNode.hx new file mode 100644 index 0000000000..8a315d5e3a --- /dev/null +++ b/Sources/armory/logicnode/SetVariableNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class SetVariableNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var variable = inputs[1]; + var value: Dynamic = inputs[2].get(); + + variable.set(value); + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetVelocityNode.hx b/Sources/armory/logicnode/SetVelocityNode.hx new file mode 100644 index 0000000000..d4ebf60a35 --- /dev/null +++ b/Sources/armory/logicnode/SetVelocityNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +class SetVelocityNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var linear: Vec4 = inputs[2].get(); + var linearFactor: Vec4 = inputs[3].get(); + var angular: Vec4 = inputs[4].get(); + var angularFactor: Vec4 = inputs[5].get(); + + if (object == null || linear == null || linearFactor == null || angular == null || angularFactor == null) return; + +#if arm_physics + var rb: RigidBody = object.getTrait(RigidBody); + rb.activate(); + rb.setLinearVelocity(linear.x, linear.y, linear.z); + rb.setLinearFactor(linearFactor.x, linearFactor.y, linearFactor.z); + rb.setAngularVelocity(angular.x, angular.y, angular.z); + rb.setAngularFactor(angularFactor.x, angularFactor.y, angularFactor.z); +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetVibrateNode.hx b/Sources/armory/logicnode/SetVibrateNode.hx new file mode 100644 index 0000000000..996be15d0f --- /dev/null +++ b/Sources/armory/logicnode/SetVibrateNode.hx @@ -0,0 +1,16 @@ +// Pulses the vibration hardware on the device for time in milliseconds, if such hardware exists. + +package armory.logicnode; +import kha.System; + +class SetVibrateNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + if (inputs[1].get() > 0) System.vibrate(inputs[1].get()); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/SetVisibleNode.hx b/Sources/armory/logicnode/SetVisibleNode.hx new file mode 100644 index 0000000000..2fe99d2493 --- /dev/null +++ b/Sources/armory/logicnode/SetVisibleNode.hx @@ -0,0 +1,44 @@ +package armory.logicnode; + +import iron.object.Object; + +class SetVisibleNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var visible: Bool = inputs[2].get(); + var children: Bool = inputs[3].get(); + + if (object == null) return; + + var objectChildren: Array = object.children; + + switch (property0) { + case "object": + object.visible = visible; + if (children == true) for (child in objectChildren) { + child.visible = visible; + } + + case "mesh": + object.visibleMesh = visible; + if (children == true) for (child in objectChildren) { + child.visibleMesh = visible; + } + + case "shadow": + object.visibleShadow = visible; + if (children == true) for (child in objectChildren) { + child.visibleShadow = visible; + } + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ShowMouseNode.hx b/Sources/armory/logicnode/ShowMouseNode.hx new file mode 100644 index 0000000000..be6727a1c1 --- /dev/null +++ b/Sources/armory/logicnode/ShowMouseNode.hx @@ -0,0 +1,15 @@ +package armory.logicnode; + +class ShowMouseNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var show: Bool = inputs[1].get(); + var mouse = iron.system.Input.getMouse(); + show ? mouse.show() : mouse.hide(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/ShutdownNode.hx b/Sources/armory/logicnode/ShutdownNode.hx new file mode 100644 index 0000000000..4f6f0e3a6c --- /dev/null +++ b/Sources/armory/logicnode/ShutdownNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class ShutdownNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + kha.System.stop(); + } +} diff --git a/Sources/armory/logicnode/SleepNode.hx b/Sources/armory/logicnode/SleepNode.hx new file mode 100644 index 0000000000..c0d046c622 --- /dev/null +++ b/Sources/armory/logicnode/SleepNode.hx @@ -0,0 +1,31 @@ +package armory.logicnode; + +import iron.system.Tween; + +class SleepNode extends LogicNode { + + var sleepArray: Array; + + public function new(tree: LogicTree) { + super(tree); + + sleepArray = new Array(); + tree.notifyOnRemove(stop); + } + + override function run(from: Int) { + var time: Float = inputs[1].get(); + var sleep = Tween.timer(time, done); + sleepArray.push(sleep); + } + + function done() { + runOutput(0); + } + + function stop() { + for(sleep in sleepArray) { + Tween.stop(sleep); + } + } +} diff --git a/Sources/armory/logicnode/SpawnCollectionNode.hx b/Sources/armory/logicnode/SpawnCollectionNode.hx new file mode 100644 index 0000000000..cc1dc53f65 --- /dev/null +++ b/Sources/armory/logicnode/SpawnCollectionNode.hx @@ -0,0 +1,90 @@ +package armory.logicnode; + +import iron.data.Data; +import iron.data.SceneFormat.TObj; +import iron.data.SceneFormat.TSceneFormat; +import iron.math.Mat4; +import iron.object.Object; + +class SpawnCollectionNode extends LogicNode { + + /** Collection name **/ + public var property0: Null; + /** scene name **/ + public var property1: Null; + + var topLevelObjects: Array; + var allObjects: Array; + var ownerObject: Null; + + public function new(tree: LogicTree) { + super(tree); + + // Return empty arrays if not executed + topLevelObjects = new Array(); + allObjects = new Array(); + } + + override function run(from: Int) { + if (property0 == null) return; + + //Raw scene not specified, using current active scene + if (property1 == "" || property1 == null) { + var raw = iron.Scene.active.raw; + spawnCollectionFromSceneRaw(raw); + return; + } + + //Raw scene specified, using the given scene + var sceneFileName = property1; + #if arm_json + sceneFileName += ".json"; + #elseif arm_compress + sceneFileName += ".lz4"; + #end + Data.getSceneRaw(sceneFileName, spawnCollectionFromSceneRaw); + return; + } + + override function get(from: Int): Dynamic { + switch (from) { + case 1: return topLevelObjects; + case 2: return allObjects; + case 3: return ownerObject; + } + return null; + } + + function spawnCollectionFromSceneRaw(raw: TSceneFormat) { + // Check if the group exists + for (g in raw.groups) { + if (g.name == property0) { + + var transform: Mat4 = inputs[1].get(); + if (transform == null) transform = Mat4.identity(); + + // Create owner object that instantiates the group + var rawOwnerObject: TObj = { + name: property0, + type: "object", + group_ref: property0, + data_ref: "", + transform: { + values: transform.toFloat32Array() + } + }; + + iron.Scene.active.createObject(rawOwnerObject, raw, null, null, + (created: Object) -> { + ownerObject = created; + topLevelObjects = created.getChildren(false); + allObjects = created.getChildren(true); + + runOutput(0); + } + ); + return; + } + } + } +} diff --git a/Sources/armory/logicnode/SpawnObjectByNameNode.hx b/Sources/armory/logicnode/SpawnObjectByNameNode.hx new file mode 100644 index 0000000000..5ecb1bc71a --- /dev/null +++ b/Sources/armory/logicnode/SpawnObjectByNameNode.hx @@ -0,0 +1,72 @@ +package armory.logicnode; + +import iron.data.SceneFormat.TSceneFormat; +import iron.data.Data; +import iron.object.Object; +import iron.math.Mat4; +import armory.trait.physics.RigidBody; + +class SpawnObjectByNameNode extends LogicNode { + + var object: Object; + var matrices: Array = []; + + /** Scene from which to take the object **/ + public var property0: Null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var objectName = inputs[1].get(); + if (objectName == null) return; + + #if arm_json + property0 += ".json"; + #elseif arm_compress + property0 += ".lz4"; + #end + + var m: Mat4 = inputs[2].get(); + matrices.push(m != null ? m.clone() : null); + var spawnChildren: Bool = inputs.length > 3 ? inputs[3].get() : true; // TODO + + Data.getSceneRaw(property0, (rawScene: TSceneFormat) -> { + + //Check if object with given name present in the specified scene + var objPresent: Bool = false; + + for (o in rawScene.objects) { + if (o.name == objectName) { + objPresent = true; + break; + } + } + if (! objPresent) return; + + //Spawn object if present + iron.Scene.active.spawnObject(objectName, null, function(o: Object) { + object = o; + var matrix = matrices.pop(); // Async spawn in a loop, order is non-stable + if (matrix != null) { + object.transform.setMatrix(matrix); + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) { + object.transform.buildMatrix(); + rigidBody.syncTransform(); + } + #end + } + object.visible = true; + runOutput(0); + }, spawnChildren, rawScene); + + }); + } + + override function get(from: Int): Dynamic { + return object; + } +} diff --git a/Sources/armory/logicnode/SpawnObjectNode.hx b/Sources/armory/logicnode/SpawnObjectNode.hx new file mode 100644 index 0000000000..e6f16aac7f --- /dev/null +++ b/Sources/armory/logicnode/SpawnObjectNode.hx @@ -0,0 +1,48 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Mat4; +import armory.trait.physics.RigidBody; + +class SpawnObjectNode extends LogicNode { + + var object: Object; + var matrices: Array = []; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var objectInput = inputs[1].get(); + if (objectInput == null) return; + + var objectName = objectInput.name; + if (objectName == "") objectName = tree.object.name; + + var m: Mat4 = inputs[2].get(); + matrices.push(m != null ? m.clone() : null); + var spawnChildren: Bool = inputs.length > 3 ? inputs[3].get() : true; // TODO + + iron.Scene.active.spawnObject(objectName, null, function(o: Object) { + object = o; + var matrix = matrices.pop(); // Async spawn in a loop, order is non-stable + if (matrix != null) { + object.transform.setMatrix(matrix); + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) { + object.transform.buildMatrix(); + rigidBody.syncTransform(); + } + #end + } + object.visible = true; + runOutput(0); + }, spawnChildren); + } + + override function get(from: Int): Dynamic { + return object; + } +} diff --git a/Sources/armory/logicnode/SpawnSceneNode.hx b/Sources/armory/logicnode/SpawnSceneNode.hx new file mode 100644 index 0000000000..3f158c925b --- /dev/null +++ b/Sources/armory/logicnode/SpawnSceneNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Mat4; + +class SpawnSceneNode extends LogicNode { + + var root: Object; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var sceneName: String = inputs[1].get(); + var matrix: Mat4 = inputs[2].get(); + + root = iron.Scene.active.addObject(); + root.name = sceneName; + if (matrix != null) { + root.transform.setMatrix(matrix); + root.transform.buildMatrix(); + } + + iron.Scene.active.addScene(sceneName, root, function(o: Object) { + runOutput(0); + }); + } + + override function get(from: Int): Dynamic { + return root; + } +} diff --git a/Sources/armory/logicnode/SplitStringNode.hx b/Sources/armory/logicnode/SplitStringNode.hx new file mode 100644 index 0000000000..5d56cf8574 --- /dev/null +++ b/Sources/armory/logicnode/SplitStringNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class SplitStringNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var s1: String = inputs[0].get(); + var s2: String = inputs[1].get(); + if (s1 == null || s2 == null) return null; + + return s1.split(s2); + } +} diff --git a/Sources/armory/logicnode/StopAgentNode.hx b/Sources/armory/logicnode/StopAgentNode.hx new file mode 100644 index 0000000000..5c319e60f5 --- /dev/null +++ b/Sources/armory/logicnode/StopAgentNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.object.Object; + +class StopAgentNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + + if (object == null) return; + +#if arm_navigation + var agent: armory.trait.NavAgent = object.getTrait(armory.trait.NavAgent); + agent.stop(); +#end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/StopSoundNode.hx b/Sources/armory/logicnode/StopSoundNode.hx new file mode 100644 index 0000000000..bfbfafdbc0 --- /dev/null +++ b/Sources/armory/logicnode/StopSoundNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +import iron.object.SpeakerObject; + +class StopSoundNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: SpeakerObject = cast(inputs[1].get(), SpeakerObject); + if (object == null) return; + object.stop(); + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/StringNode.hx b/Sources/armory/logicnode/StringNode.hx new file mode 100644 index 0000000000..eaa8493cd7 --- /dev/null +++ b/Sources/armory/logicnode/StringNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class StringNode extends LogicNode { + + public var value: String; + + public function new(tree: LogicTree, value = "") { + super(tree); + this.value = value; + } + + override function get(from: Int): Dynamic { + if (inputs.length > 0) return inputs[0].get(); + return value; + } + + override function set(value: Dynamic) { + if (inputs.length > 0) inputs[0].set(value); + else this.value = value; + } +} diff --git a/Sources/armory/logicnode/StringReplaceNode.hx b/Sources/armory/logicnode/StringReplaceNode.hx new file mode 100644 index 0000000000..bee51b6faa --- /dev/null +++ b/Sources/armory/logicnode/StringReplaceNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class StringReplaceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var string: String = inputs[0].get(); + var find: String = inputs[1].get(); + var replace: String = inputs[2].get(); + if (find == null || replace == null || string == null) return null; + + return StringTools.replace(string, find, replace); + } +} diff --git a/Sources/armory/logicnode/SubStringNode.hx b/Sources/armory/logicnode/SubStringNode.hx new file mode 100644 index 0000000000..8417242670 --- /dev/null +++ b/Sources/armory/logicnode/SubStringNode.hx @@ -0,0 +1,17 @@ +package armory.logicnode; + +class SubStringNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var string: String = inputs[0].get(); + var start: Int = inputs[1].get(); + var end: Int = inputs[2].get(); + if (start == null || end == null || string == null) return null; + + return string.substring(start, end); + } +} diff --git a/Sources/armory/logicnode/SurfaceCoordsNode.hx b/Sources/armory/logicnode/SurfaceCoordsNode.hx new file mode 100644 index 0000000000..d3c53d622f --- /dev/null +++ b/Sources/armory/logicnode/SurfaceCoordsNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class SurfaceCoordsNode extends LogicNode { + + var coords = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var surface = iron.system.Input.getSurface(); + if (from == 0) { + coords.x = surface.x; + coords.y = surface.y; + return coords; + } + else if (from == 1) { + coords.x = surface.movementX; + coords.y = surface.movementY; + return coords; + } + else { + return surface.wheelDelta; + } + } +} diff --git a/Sources/armory/logicnode/SwitchNode.hx b/Sources/armory/logicnode/SwitchNode.hx new file mode 100644 index 0000000000..5cad169e3b --- /dev/null +++ b/Sources/armory/logicnode/SwitchNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class SwitchNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var v1: Dynamic = inputs[1].get(); + if (inputs.length > 2) { + for (i in 2...inputs.length) { + if (inputs[i].get() == v1) { + runOutput(i - 1); + return; + } + } + } + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/TimeNode.hx b/Sources/armory/logicnode/TimeNode.hx new file mode 100644 index 0000000000..f5fec03d86 --- /dev/null +++ b/Sources/armory/logicnode/TimeNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class TimeNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) return iron.system.Time.time(); + else return iron.system.Time.delta; + } +} diff --git a/Sources/armory/logicnode/TimerNode.hx b/Sources/armory/logicnode/TimerNode.hx new file mode 100644 index 0000000000..779f6ecf68 --- /dev/null +++ b/Sources/armory/logicnode/TimerNode.hx @@ -0,0 +1,66 @@ +package armory.logicnode; + +class TimerNode extends LogicNode { + + var time = 0.0; + var duration = 0.0; + var repeat = 0; + var running = false; + var repetitions = 0; + + public function new(tree: LogicTree) { + super(tree); + } + + function update() { + time += iron.system.Time.delta; + if (time >= duration) { + repeat--; + runOutput(0); + if (repeat != 0) { + time = 0; + repetitions++; + } + else { + removeUpdate(); + runOutput(1); + time = 0; + repetitions = 0; + } + } + } + + override function run(from: Int) { + if (from == 0) { // Start + duration = inputs[3].get(); + repeat = inputs[4].get(); + if (!running) { + running = true; + tree.notifyOnUpdate(update); + } + } + else if (from == 1) { // Pause + removeUpdate(); + } + else { // Stop + removeUpdate(); + time = 0; + repetitions = 0; + } + } + + inline function removeUpdate() { + if (running) { + running = false; + tree.removeUpdate(update); + } + } + + override function get(from: Int): Dynamic { + if (from == 2) return running; + else if (from == 3) return time; + else if (from == 4) return duration - time; + else if (from == 5) return time / duration; + else return repetitions; + } +} diff --git a/Sources/armory/logicnode/ToBoolNode.hx b/Sources/armory/logicnode/ToBoolNode.hx new file mode 100644 index 0000000000..d156ed9579 --- /dev/null +++ b/Sources/armory/logicnode/ToBoolNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class ToBoolNode extends LogicNode { + + var value: Bool; + var b = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + b = true; + } + + override function get(from: Int): Dynamic { + value = b; + b = false; + return value; + } +} diff --git a/Sources/armory/logicnode/TouchInRegionNode.hx b/Sources/armory/logicnode/TouchInRegionNode.hx new file mode 100644 index 0000000000..025895523e --- /dev/null +++ b/Sources/armory/logicnode/TouchInRegionNode.hx @@ -0,0 +1,115 @@ +package armory.logicnode; + +import iron.math.Vec2; + +class TouchInRegionNode extends LogicNode { + + public var property0: String; + + var lastInside: Null = null; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(update); + } + + function update() { + var touch = iron.system.Input.getSurface(); + final center = new Vec2(inputs[0].get(), inputs[1].get()); + final size = new Vec2(inputs[2].get(), inputs[3].get()); + final angle: Float = inputs[4].get(); + final lastPoint = new Vec2(touch.lastX, touch.lastY); + final point = new Vec2(touch.x, touch.y); + + switch (property0) { + case "rectangle": + if(lastInside == null) { + lastInside = detectPointInRectangle(center, size, angle, lastPoint); + } + final inside = detectPointInRectangle(center, size, angle, point); + + //On Enter + if(!lastInside && inside) runOutput(0); + + //On Exit + if(lastInside && !inside) runOutput(1); + + lastInside = inside; + + case "ellipse": + if(lastInside == null) { + lastInside = detectPointInEllipse(center, size, angle, lastPoint); + } + final inside = detectPointInEllipse(center, size, angle, point); + + //On Enter + if(!lastInside && inside) runOutput(0); + + //On Exit + if(lastInside && !inside) runOutput(1); + + lastInside = inside; + } + } + + override function get(from: Int): Dynamic { + var touch = iron.system.Input.getSurface(); + + if(from == 2) { + final center = new Vec2(inputs[0].get(), inputs[1].get()); + final size = new Vec2(inputs[2].get(), inputs[3].get()); + final angle = inputs[4].get(); + final point = new Vec2(touch.x, touch.y); + + switch (property0) { + case "rectangle": + return detectPointInRectangle(center, size, angle, point); + case "ellipse": + return detectPointInEllipse(center, size, angle, point); + } + } + + return null; + } + + //Rotate touch location and calculate relative location to shape center + inline function alignRotatePoint(point: Vec2, center: Vec2, angle: Float): Vec2 { + var relativePoint = point.clone(); + relativePoint.sub(center); + + final sin = Math.sin(angle); + final cos = Math.cos(angle); + + final xnew = relativePoint.x * cos - relativePoint.y * sin; + final ynew = relativePoint.x * sin + relativePoint.y * cos; + + relativePoint.x = xnew; + relativePoint.y = ynew; + + return relativePoint; + } + + function detectPointInRectangle(center: Vec2, size: Vec2, angle: Float, point: Vec2): Bool { + final relativePoint = alignRotatePoint(point, center, -angle); + final magX = Math.abs(relativePoint.x); + final magY = Math.abs(relativePoint.y); + if(magX <= size.x/2 && magY <= size.y/2){ + return true; + } + + return false; + } + + function detectPointInEllipse(center: Vec2, size: Vec2, angle: Float, point: Vec2): Bool { + final relativePoint = alignRotatePoint(point, center, -angle); + final magX = (relativePoint.x * relativePoint.x) / (0.25 * size.x * size.x); + final magY = (relativePoint.y * relativePoint.y) / (0.25 * size.y * size.y); + if(magX + magY <= 1.0){ + return true; + } + + return false; + } +} + diff --git a/Sources/armory/logicnode/TraitNode.hx b/Sources/armory/logicnode/TraitNode.hx new file mode 100644 index 0000000000..245216ac5b --- /dev/null +++ b/Sources/armory/logicnode/TraitNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + +class TraitNode extends LogicNode { + + public var property0: String; + public var value: Dynamic = null; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (value != null) return value; + + var cname = Type.resolveClass(Main.projectPackage + "." + property0); + if (cname == null) cname = Type.resolveClass(Main.projectPackage + ".node." + property0); + if (cname == null) throw 'No trait with the name "$property0" found, make sure that the trait is exported!'; + value = Type.createInstance(cname, []); + return value; + } + + override function set(value: Dynamic) { + this.value = value; + } +} diff --git a/Sources/armory/logicnode/TransformMathNode.hx b/Sources/armory/logicnode/TransformMathNode.hx new file mode 100644 index 0000000000..4f727cf594 --- /dev/null +++ b/Sources/armory/logicnode/TransformMathNode.hx @@ -0,0 +1,55 @@ +package armory.logicnode; + +import iron.math.Mat4; + +class TransformMathNode extends LogicNode { + + public var property0: String; + var m = Mat4.identity(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var m1: Mat4 = inputs[0].get(); + var m2: Mat4 = inputs[1].get(); + + if (m1 == null || m2 == null) return null; + + m.setFrom(m1); + transformMath(m, m2); + + return m; + } + + public static function transformMath(m1: Mat4, m2: Mat4) { + var a00 = m1._00; var a01 = m1._01; var a02 = m1._02; + var a10 = m1._10; var a11 = m1._11; var a12 = m1._12; + var a20 = m1._20; var a21 = m1._21; var a22 = m1._22; + + var b0 = m2._00; var b1 = m2._10; var b2 = m2._20; + m1._00 = a00 * b0 + a01 * b1 + a02 * b2; + m1._10 = a10 * b0 + a11 * b1 + a12 * b2; + m1._20 = a20 * b0 + a21 * b1 + a22 * b2; + + b0 = m2._01; b1 = m2._11; b2 = m2._21; + m1._01 = a00 * b0 + a01 * b1 + a02 * b2; + m1._11 = a10 * b0 + a11 * b1 + a12 * b2; + m1._21 = a20 * b0 + a21 * b1 + a22 * b2; + + b0 = m2._02; b1 = m2._12; b2 = m2._22; + m1._02 = a00 * b0 + a01 * b1 + a02 * b2; + m1._12 = a10 * b0 + a11 * b1 + a12 * b2; + m1._22 = a20 * b0 + a21 * b1 + a22 * b2; + + b0 = m2._03; b1 = m2._13; b2 = m2._23; + m1._03 = a00 * b0 + a01 * b1 + a02 * b2; + m1._13 = a10 * b0 + a11 * b1 + a12 * b2; + m1._23 = a20 * b0 + a21 * b1 + a22 * b2; + + m1._30 += m2._30; + m1._31 += m2._31; + m1._32 += m2._32; + } +} diff --git a/Sources/armory/logicnode/TransformNode.hx b/Sources/armory/logicnode/TransformNode.hx new file mode 100644 index 0000000000..f00c7af975 --- /dev/null +++ b/Sources/armory/logicnode/TransformNode.hx @@ -0,0 +1,37 @@ +package armory.logicnode; + +import iron.math.Mat4; +import iron.math.Vec4; +import iron.math.Quat; + +class TransformNode extends LogicNode { + + var value: Mat4 = Mat4.identity(); + var q = new Quat(); + var v1 = new Vec4(); + var v2 = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var loc: Vec4 = inputs[0].get(); + var rot: Quat = new Quat().setFrom(inputs[1].get()); + rot.normalize(); + var scale: Vec4 = inputs[2].get(); + if (loc == null && rot == null && scale == null) return this.value; + if (loc == null || rot == null || scale == null) return null; + this.value.compose(loc, rot, scale); + return this.value; + } + + override function set(value: Dynamic) { + if (inputs.length>0){ + cast(value, Mat4).decompose(v1, q, v2); + inputs[0].set(v1); + inputs[1].set(q); + inputs[2].set(v2); + }else this.value = value; + } +} diff --git a/Sources/armory/logicnode/TranslateObjectNode.hx b/Sources/armory/logicnode/TranslateObjectNode.hx new file mode 100644 index 0000000000..f2c047d997 --- /dev/null +++ b/Sources/armory/logicnode/TranslateObjectNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; +import armory.trait.physics.RigidBody; + +using armory.object.TransformExtension; + +class TranslateObjectNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var vec: Vec4 = inputs[2].get(); + var local: Bool = inputs.length > 3 ? inputs[3].get() : false; + + if (object == null || vec == null) return; + + if (!local) { + object.transform.loc.add(vec); + object.transform.buildMatrix(); + } + + else { + object.transform.loc.add(object.transform.worldVecToOrientation(vec)); + object.transform.buildMatrix(); + } + +#if arm_physics + var rigidBody = object.getTrait(RigidBody); + + if (rigidBody != null) rigidBody.syncTransform(); +#end + + runOutput(0); + } + +} diff --git a/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx b/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx new file mode 100644 index 0000000000..4906fce1bb --- /dev/null +++ b/Sources/armory/logicnode/TranslateOnLocalAxisNode.hx @@ -0,0 +1,35 @@ +package armory.logicnode; + +import iron.object.Object; +import armory.trait.physics.RigidBody; + +class TranslateOnLocalAxisNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: Object = inputs[1].get(); + var sp: Float = inputs[2].get(); + var l: Int = inputs[3].get(); + var ini: Bool = inputs[4].get(); + + if (object == null) return; + + if (ini) sp *= -1; + + if (l == 1) object.transform.move(object.transform.local.look(),sp); + else if (l == 2) object.transform.move(object.transform.local.up(),sp); + else if (l == 3) object.transform.move(object.transform.local.right(),sp); + + object.transform.buildMatrix(); + + #if arm_physics + var rigidBody = object.getTrait(RigidBody); + if (rigidBody != null) rigidBody.syncTransform(); + #end + + runOutput(0); + } +} diff --git a/Sources/armory/logicnode/TweenFloatNode.hx b/Sources/armory/logicnode/TweenFloatNode.hx new file mode 100644 index 0000000000..8e8f251520 --- /dev/null +++ b/Sources/armory/logicnode/TweenFloatNode.hx @@ -0,0 +1,114 @@ +package armory.logicnode; + +import iron.system.Tween; + +class TweenFloatNode extends LogicNode { + + public var property0:String; + + public var anim: TAnim; + public var fromValue:Float = 0.0; + public var toValue:Float = 1.0; + public var duration:Float = 1.0; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(from == 0){ + + if(anim != null){ + Tween.stop(anim); + } + + fromValue = inputs[2].get(); + toValue = inputs[3].get(); + duration = inputs[4].get(); + var type:Dynamic = Linear; + + switch (property0) { + case "Linear": + type = Linear; + case "SineIn": + type = SineIn; + case "SineOut": + type = SineOut; + case "SineInOut": + type = SineInOut; + case "QuadIn": + type = QuadIn; + case "QuadOut": + type = QuadOut; + case "QuadInOut": + type = QuadInOut; + case "CubicIn": + type = CubicIn; + case "CubicOut": + type = CubicOut; + case "CubicInOut": + type = CubicInOut; + case "QuartIn": + type = QuartIn; + case "QuartOut": + type = QuartOut; + case "QuartInOut": + type = QuartInOut; + case "QuintIn": + type = QuintIn; + case "QuintOut": + type = QuintOut; + case "QuintInOut": + type = QuintInOut; + case "ExpoIn": + type = ExpoIn; + case "ExpoOut": + type = ExpoOut; + case "ExpoInOut": + type = ExpoInOut; + case "CircIn": + type = CircIn; + case "CircOut": + type = CircOut; + case "CircInOut": + type = CircInOut; + case "BackIn": + type = BackIn; + case "BackOut": + type = BackOut; + case "BackInOut": + type = BackInOut; + } + + anim = Tween.to({ + target: this, + props: { fromValue: toValue }, + duration: duration, + ease: type, + tick: update, + done: done + }); + } + else{ + if(anim != null){ + Tween.stop(anim); + } + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + if(from == 3) return fromValue; + return null; + } + + function update() { + runOutput(1); + } + + function done() { + runOutput(2); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/TweenRotationNode.hx b/Sources/armory/logicnode/TweenRotationNode.hx new file mode 100644 index 0000000000..31a3bb76ac --- /dev/null +++ b/Sources/armory/logicnode/TweenRotationNode.hx @@ -0,0 +1,116 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.system.Tween; + +class TweenRotationNode extends LogicNode { + + public var property0:String; + + public var anim: TAnim; + public var fromValue:Quat = new Quat(); + public var toValue:Quat = new Quat(); + public var duration:Float = 1.0; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(from == 0){ + + if(anim != null){ + Tween.stop(anim); + } + + fromValue.setFrom(inputs[2].get()); + toValue.setFrom(inputs[3].get()); + duration = inputs[4].get(); + var type:Dynamic = Linear; + + switch (property0) { + case "Linear": + type = Linear; + case "SineIn": + type = SineIn; + case "SineOut": + type = SineOut; + case "SineInOut": + type = SineInOut; + case "QuadIn": + type = QuadIn; + case "QuadOut": + type = QuadOut; + case "QuadInOut": + type = QuadInOut; + case "CubicIn": + type = CubicIn; + case "CubicOut": + type = CubicOut; + case "CubicInOut": + type = CubicInOut; + case "QuartIn": + type = QuartIn; + case "QuartOut": + type = QuartOut; + case "QuartInOut": + type = QuartInOut; + case "QuintIn": + type = QuintIn; + case "QuintOut": + type = QuintOut; + case "QuintInOut": + type = QuintInOut; + case "ExpoIn": + type = ExpoIn; + case "ExpoOut": + type = ExpoOut; + case "ExpoInOut": + type = ExpoInOut; + case "CircIn": + type = CircIn; + case "CircOut": + type = CircOut; + case "CircInOut": + type = CircInOut; + case "BackIn": + type = BackIn; + case "BackOut": + type = BackOut; + case "BackInOut": + type = BackInOut; + } + + anim = Tween.to({ + target: this, + props: { fromValue: toValue }, + duration: duration, + ease: type, + tick: update, + done: done + }); + } + else{ + if(anim != null){ + Tween.stop(anim); + } + + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + if(from == 3) return fromValue; + return null; + } + + function update() { + runOutput(1); + } + + function done() { + runOutput(2); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/TweenTransformNode.hx b/Sources/armory/logicnode/TweenTransformNode.hx new file mode 100644 index 0000000000..d9bd7adbf9 --- /dev/null +++ b/Sources/armory/logicnode/TweenTransformNode.hx @@ -0,0 +1,129 @@ +package armory.logicnode; + +import iron.math.Mat4; +import iron.math.Vec4; +import iron.math.Quat; +import iron.system.Tween; + +class TweenTransformNode extends LogicNode { + + public var property0:String; + + public var anim: TAnim; + public var fromValue:Mat4 = Mat4.identity(); + public var toValue:Mat4 = Mat4.identity(); + public var duration:Float = 1.0; + public var floc: Vec4 = new Vec4(); + public var frot: Quat = new Quat(); + public var fscl: Vec4 = new Vec4(); + public var tloc: Vec4 = new Vec4(); + public var trot: Quat = new Quat(); + public var tscl: Vec4 = new Vec4(); + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(from == 0){ + + if(anim != null){ + Tween.stop(anim); + } + + fromValue.setFrom(inputs[2].get()); + toValue.setFrom(inputs[3].get()); + + fromValue.decompose(floc, frot, fscl); + toValue.decompose(tloc, trot, tscl); + + duration = inputs[4].get(); + var type:Dynamic = Linear; + + switch (property0) { + case "Linear": + type = Linear; + case "SineIn": + type = SineIn; + case "SineOut": + type = SineOut; + case "SineInOut": + type = SineInOut; + case "QuadIn": + type = QuadIn; + case "QuadOut": + type = QuadOut; + case "QuadInOut": + type = QuadInOut; + case "CubicIn": + type = CubicIn; + case "CubicOut": + type = CubicOut; + case "CubicInOut": + type = CubicInOut; + case "QuartIn": + type = QuartIn; + case "QuartOut": + type = QuartOut; + case "QuartInOut": + type = QuartInOut; + case "QuintIn": + type = QuintIn; + case "QuintOut": + type = QuintOut; + case "QuintInOut": + type = QuintInOut; + case "ExpoIn": + type = ExpoIn; + case "ExpoOut": + type = ExpoOut; + case "ExpoInOut": + type = ExpoInOut; + case "CircIn": + type = CircIn; + case "CircOut": + type = CircOut; + case "CircInOut": + type = CircInOut; + case "BackIn": + type = BackIn; + case "BackOut": + type = BackOut; + case "BackInOut": + type = BackInOut; + } + + anim = Tween.to({ + target: this, + props: { floc: tloc, frot: trot, fscl: tscl, }, + duration: duration, + ease: type, + tick: update, + done: done + }); + + } + else{ + if(anim != null){ + Tween.stop(anim); + } + + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + if(from == 3) return fromValue.compose(floc, frot, fscl); + return null; + } + + function update() { + runOutput(1); + } + + function done() { + runOutput(2); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/TweenVectorNode.hx b/Sources/armory/logicnode/TweenVectorNode.hx new file mode 100644 index 0000000000..7638228646 --- /dev/null +++ b/Sources/armory/logicnode/TweenVectorNode.hx @@ -0,0 +1,116 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.system.Tween; + +class TweenVectorNode extends LogicNode { + + public var property0:String; + + public var anim: TAnim; + public var fromValue:Vec4 = new Vec4(); + public var toValue:Vec4 = new Vec4(); + public var duration:Float = 1.0; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + + if(from == 0){ + + if(anim != null){ + Tween.stop(anim); + } + + fromValue.setFrom(inputs[2].get()); + toValue.setFrom(inputs[3].get()); + duration = inputs[4].get(); + var type:Dynamic = Linear; + + switch (property0) { + case "Linear": + type = Linear; + case "SineIn": + type = SineIn; + case "SineOut": + type = SineOut; + case "SineInOut": + type = SineInOut; + case "QuadIn": + type = QuadIn; + case "QuadOut": + type = QuadOut; + case "QuadInOut": + type = QuadInOut; + case "CubicIn": + type = CubicIn; + case "CubicOut": + type = CubicOut; + case "CubicInOut": + type = CubicInOut; + case "QuartIn": + type = QuartIn; + case "QuartOut": + type = QuartOut; + case "QuartInOut": + type = QuartInOut; + case "QuintIn": + type = QuintIn; + case "QuintOut": + type = QuintOut; + case "QuintInOut": + type = QuintInOut; + case "ExpoIn": + type = ExpoIn; + case "ExpoOut": + type = ExpoOut; + case "ExpoInOut": + type = ExpoInOut; + case "CircIn": + type = CircIn; + case "CircOut": + type = CircOut; + case "CircInOut": + type = CircInOut; + case "BackIn": + type = BackIn; + case "BackOut": + type = BackOut; + case "BackInOut": + type = BackInOut; + } + + anim = Tween.to({ + target: this, + props: { fromValue: toValue }, + duration: duration, + ease: type, + tick: update, + done: done + }); + } + else{ + if(anim != null){ + Tween.stop(anim); + } + + } + + runOutput(0); + } + + override function get(from: Int): Dynamic { + if(from == 3) return fromValue; + return null; + } + + function update() { + runOutput(1); + } + + function done() { + runOutput(2); + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/ValueChangedNode.hx b/Sources/armory/logicnode/ValueChangedNode.hx new file mode 100644 index 0000000000..d5ae125816 --- /dev/null +++ b/Sources/armory/logicnode/ValueChangedNode.hx @@ -0,0 +1,37 @@ +package armory.logicnode; + +class ValueChangedNode extends LogicNode { + + var initialized = false; + var initialValue: Dynamic; + var lastValue: Dynamic; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var currentValue = inputs[1].get(); + + if (!initialized) { + initialValue = currentValue; + lastValue = initialValue; + initialized = true; + runOutput(0); + runOutput(2); + return; + } + + if (currentValue == lastValue) { + runOutput(1); + } + else { + lastValue = currentValue; + runOutput(0); + } + + if (currentValue == initialValue) { + runOutput(2); + } + } +} diff --git a/Sources/armory/logicnode/VectorClampToSizeNode.hx b/Sources/armory/logicnode/VectorClampToSizeNode.hx new file mode 100644 index 0000000000..0ce05befda --- /dev/null +++ b/Sources/armory/logicnode/VectorClampToSizeNode.hx @@ -0,0 +1,33 @@ +package armory.logicnode; + +import iron.math.Vec4; +import armory.math.Helper; + +class VectorClampToSizeNode extends LogicNode { + + /** Clamping mode ("length" or "components"). **/ + public var property0: String; + + var v: Vec4; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + v = (inputs[0].get(): Vec4).clone(); + var fmin: kha.FastFloat = inputs[1].get(); + var fmax: kha.FastFloat = inputs[2].get(); + + if (property0 == "length") { + v.clamp(fmin, fmax); + } + else if (property0 == "components") { + v.x = Helper.clamp(v.x, fmin, fmax); + v.y = Helper.clamp(v.y, fmin, fmax); + v.z = Helper.clamp(v.z, fmin, fmax); + } + + return v; + } +} diff --git a/Sources/armory/logicnode/VectorFromBooleanNode.hx b/Sources/armory/logicnode/VectorFromBooleanNode.hx new file mode 100644 index 0000000000..d210b73eb3 --- /dev/null +++ b/Sources/armory/logicnode/VectorFromBooleanNode.hx @@ -0,0 +1,36 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class VectorFromBooleanNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var boolX = inputs[0].get(); + var boolNegX = inputs[1].get(); + var boolY = inputs[2].get(); + var boolNegY = inputs[3].get(); + var boolZ = inputs[4].get(); + var boolNegZ = inputs[5].get(); + + var vector = new Vec4(); + + if (boolX == true) vector.x += 1; + + if (boolNegX == true) vector.x += -1; + + if (boolY == true) vector.y += 1; + + if (boolNegY == true) vector.y += -1; + + if (boolZ == true) vector.z += 1; + + if (boolNegZ == true) vector.z += -1; + + return vector.normalize(); + } +} diff --git a/Sources/armory/logicnode/VectorFromTransformNode.hx b/Sources/armory/logicnode/VectorFromTransformNode.hx new file mode 100644 index 0000000000..da0d327192 --- /dev/null +++ b/Sources/armory/logicnode/VectorFromTransformNode.hx @@ -0,0 +1,55 @@ +package armory.logicnode; + +import iron.math.Quat; +import iron.math.Mat4; +import iron.math.Vec4; + +class VectorFromTransformNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var m: Mat4 = inputs[0].get(); + + if (m == null) return null; + + switch(from) { + case 0: + switch (property0) { + case "Up": + return m.up(); + case "Right": + return m.right(); + case "Look": + return m.look(); + case "Quaternion": + var q = new Quat(); + q.fromMat(m); + return q.normalize(); + } + case 1: + if (property0 == "Quaternion") { + var q = new Quat(); + q.fromMat(m); + q.normalize(); + var v = new Vec4(); + v.x = q.x; v.y = q.y; v.z = q.z; + v.w = 0; //prevent vector translation + return v; + } + case 2: + if (property0 == "Quaternion") { + var q = new Quat(); + q.fromMat(m); + q.normalize(); + return q.w; + } + } + + return null; + } +} diff --git a/Sources/armory/logicnode/VectorMathNode.hx b/Sources/armory/logicnode/VectorMathNode.hx new file mode 100644 index 0000000000..65a1453435 --- /dev/null +++ b/Sources/armory/logicnode/VectorMathNode.hx @@ -0,0 +1,138 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class VectorMathNode extends LogicNode { + + public var property0: String; // Operation + public var property1: Bool; // Separator Out + var res_v = new Vec4(); + var res_f = 0.0; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var p1: Vec4 = inputs[0].get(); + if (p1 == null) return null; + res_v.setFrom(p1); + switch (property0) { + // 1 arguments: Normalize, Length + case "Normalize": + res_v.normalize(); + case "Length": + res_f = res_v.length(); + // 2 arguments: Distance, Reflect + case "Distance": { + var p2: Vec4 = inputs[1].get(); + if (p2 == null) return null; + res_f = res_v.distanceTo(p2); + } + case "Reflect": { + var p2: Vec4 = inputs[1].get(); + if (p2 == null) return null; + res_v.reflect(p2); + } + // Many arguments: Add, Subtract, Average, Dot Product, Cross Product, Multiply + case "Add": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_v.add(p2); + i++; + } + } + case "Subtract": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_v.sub(p2); + i++; + } + } + case "Average": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_v.add(p2); + res_v.mult(0.5); + i++; + } + } + case "Dot Product": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_f = res_v.dot(p2); + res_v.set(res_f, res_f, res_f); + i++; + } + } + case "Cross Product": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_v.cross(p2); + i++; + } + } + case "Multiply": { + var p2 = new Vec4(); + var i = 1; + while (i < inputs.length) { + p2 = inputs[i].get(); + if (p2 == null) return null; + res_v.x *= p2.x; + res_v.y *= p2.y; + res_v.z *= p2.z; + i++; + } + } + case "MultiplyFloats": { + var p2_f = 1.0; + var i = 1; + while (i < inputs.length) { + p2_f = inputs[i].get(); + res_v.mult(p2_f); + i++; + } + } + } + // Return and check separator + switch (from) { + case 0: return res_v; + case 1: + if (property1) { + return res_v.x; + } else { + return res_f; + } + case 2: + if (property1) { + return res_v.y; + } else { + return res_f; + } + case 3: + if (property1) { + return res_v.z; + } else { + return res_f; + } + case 4: + if (property1) return res_f; + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/VectorMixNode.hx b/Sources/armory/logicnode/VectorMixNode.hx new file mode 100644 index 0000000000..ae2b793832 --- /dev/null +++ b/Sources/armory/logicnode/VectorMixNode.hx @@ -0,0 +1,63 @@ +package armory.logicnode; + +import iron.system.Tween; +import iron.math.Vec4; + +class VectorMixNode extends LogicNode { + + public var property0: String; // Type + public var property1: String; // Ease + public var property2: Bool; // Clamp + + var v = new Vec4(); + + var ease: Float->Float = null; + + public function new(tree: LogicTree) { + super(tree); + } + + function init() { + switch (property0) { + case "Linear": + ease = Tween.easeLinear; + case "Sine": + ease = property1 == "In" ? Tween.easeSineIn : (property1 == "Out" ? Tween.easeSineOut : Tween.easeSineInOut); + case "Quad": + ease = property1 == "In" ? Tween.easeQuadIn : (property1 == "Out" ? Tween.easeQuadOut : Tween.easeQuadInOut); + case "Cubic": + ease = property1 == "In" ? Tween.easeCubicIn : (property1 == "Out" ? Tween.easeCubicOut : Tween.easeCubicInOut); + case "Quart": + ease = property1 == "In" ? Tween.easeQuartIn : (property1 == "Out" ? Tween.easeQuartOut : Tween.easeQuartInOut); + case "Quint": + ease = property1 == "In" ? Tween.easeQuintIn : (property1 == "Out" ? Tween.easeQuintOut : Tween.easeQuintInOut); + case "Expo": + ease = property1 == "In" ? Tween.easeExpoIn : (property1 == "Out" ? Tween.easeExpoOut : Tween.easeExpoInOut); + case "Circ": + ease = property1 == "In" ? Tween.easeCircIn : (property1 == "Out" ? Tween.easeCircOut : Tween.easeCircInOut); + case "Back": + ease = property1 == "In" ? Tween.easeBackIn : (property1 == "Out" ? Tween.easeBackOut : Tween.easeBackInOut); + case "Bounce": + ease = property1 == "In" ? Tween.easeBounceIn : (property1 == "Out" ? Tween.easeBounceOut : Tween.easeBounceInOut); + case "Elastic": + ease = property1 == "In" ? Tween.easeElasticIn : (property1 == "Out" ? Tween.easeElasticOut : Tween.easeElasticInOut); + default: + ease = Tween.easeLinear; + } + } + + override function get(from: Int): Dynamic { + if (ease == null) init(); + var k: Float = inputs[0].get(); //Factor + var v1: Vec4 = inputs[1].get(); + var v2: Vec4 = inputs[2].get(); + if (v1 == null || v2 == null) return null; + var f = ease(k); + v.x = v1.x + (v2.x - v1.x) * f; + v.y = v1.y + (v2.y - v1.y) * f; + v.z = v1.z + (v2.z - v1.z) * f; + + if (property2) v.clamp(0, 1); + return v; + } +} diff --git a/Sources/armory/logicnode/VectorMoveTowardsNode.hx b/Sources/armory/logicnode/VectorMoveTowardsNode.hx new file mode 100644 index 0000000000..ccfacc827e --- /dev/null +++ b/Sources/armory/logicnode/VectorMoveTowardsNode.hx @@ -0,0 +1,46 @@ +package armory.logicnode; + +import kha.FastFloat; +import iron.math.Vec4; +import iron.system.Time; +import armory.math.Helper; + +class VectorMoveTowardsNode extends LogicNode { + + var reference = new Vec4(); + var current = new Vec4(); + var target = new Vec4(); + + var endFrame = false; + + public function new(tree: LogicTree) { + super(tree); + + tree.notifyOnUpdate(function() { + endFrame = false; + }); + } + + override function get(from: Int): Dynamic { + if (!endFrame) { // Update once per frame + var v1: Vec4 = inputs[0].get(); + var v2: Vec4 = inputs[1].get(); + var delta: FastFloat = inputs[2].get(); + + if (v1 == null || v2 == null || delta == null) return null; + + if (!reference.equals(v1)) { + // Current vector changed + reference.setFrom(v1); + current.setFrom(v1); + } + + target.setFrom(v2); + current = Helper.moveTowards(current, target, delta); + + endFrame = true; + } + + return current; + } +} diff --git a/Sources/armory/logicnode/VectorNode.hx b/Sources/armory/logicnode/VectorNode.hx new file mode 100644 index 0000000000..09cb02043d --- /dev/null +++ b/Sources/armory/logicnode/VectorNode.hx @@ -0,0 +1,32 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class VectorNode extends LogicNode { + + var value = new Vec4(); + + public function new(tree: LogicTree, x: Null = null, y: Null = null, z: Null = null) { + super(tree); + + if (x != null) { + LogicNode.addLink(new FloatNode(tree, x), this, 0, 0); + LogicNode.addLink(new FloatNode(tree, y), this, 0, 1); + LogicNode.addLink(new FloatNode(tree, z), this, 0, 2); + } + } + + override function get(from: Int): Dynamic { + value = new Vec4(); + value.x = inputs[0].get(); + value.y = inputs[1].get(); + value.z = inputs[2].get(); + return value; + } + + override function set(value: Dynamic) { + inputs[0].set(value.x); + inputs[1].set(value.y); + inputs[2].set(value.z); + } +} diff --git a/Sources/armory/logicnode/VectorToObjectOrientationNode.hx b/Sources/armory/logicnode/VectorToObjectOrientationNode.hx new file mode 100644 index 0000000000..27a4b994c7 --- /dev/null +++ b/Sources/armory/logicnode/VectorToObjectOrientationNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +import iron.object.Object; +import iron.math.Vec4; + +class VectorToObjectOrientationNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var object: Object = inputs[0].get(); + var vec: Vec4 = inputs[1].get(); + + if (object == null || vec == null) return null; + + return vec.applyQuat(object.transform.rot); + } + +} diff --git a/Sources/armory/logicnode/VolumeTriggerNode.hx b/Sources/armory/logicnode/VolumeTriggerNode.hx new file mode 100644 index 0000000000..40d55aa1ad --- /dev/null +++ b/Sources/armory/logicnode/VolumeTriggerNode.hx @@ -0,0 +1,39 @@ +package armory.logicnode; + +import iron.object.Object; +using armory.object.TransformExtension; + +class VolumeTriggerNode extends LogicNode { + + public var property0: String; + var lastOverlap = false; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var object: Object = inputs[0].get(); + var volume: Object = inputs[1].get(); + + if (object == null) return false; + if (volume == null) volume = tree.object; + + var t1 = object.transform; + var t2 = volume.transform; + var overlap = t1.overlap(t2); + + var b = false; + switch (property0) { + case "begin": + b = overlap && !lastOverlap; + case "overlap": + b = overlap; + case "end": + b = !overlap && lastOverlap; + } + + lastOverlap = overlap; + return b; + } +} diff --git a/Sources/armory/logicnode/WhileNode.hx b/Sources/armory/logicnode/WhileNode.hx new file mode 100644 index 0000000000..9d085fea46 --- /dev/null +++ b/Sources/armory/logicnode/WhileNode.hx @@ -0,0 +1,28 @@ +package armory.logicnode; + +class WhileNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var b: Bool = inputs[1].get(); + while (b) { + runOutput(0); + b = inputs[1].get(); + + if (tree.loopBreak) { + tree.loopBreak = false; + break; + } + + if (tree.loopContinue) { + tree.loopContinue = false; + continue; + } + + } + runOutput(1); + } +} diff --git a/Sources/armory/logicnode/WindowInfoNode.hx b/Sources/armory/logicnode/WindowInfoNode.hx new file mode 100644 index 0000000000..661efead5c --- /dev/null +++ b/Sources/armory/logicnode/WindowInfoNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class WindowInfoNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + if (from == 0) return kha.System.windowWidth(); + else return kha.System.windowHeight(); + } +} diff --git a/Sources/armory/logicnode/WorldToScreenSpaceNode.hx b/Sources/armory/logicnode/WorldToScreenSpaceNode.hx new file mode 100644 index 0000000000..6ca33573aa --- /dev/null +++ b/Sources/armory/logicnode/WorldToScreenSpaceNode.hx @@ -0,0 +1,30 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.math.Vec2; +import iron.App; + +class WorldToScreenSpaceNode extends LogicNode { + + public var property0: String; + var v = new Vec4(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var v1: Vec4 = inputs[0].get(); + if (v1 == null) return null; + + var cam = iron.Scene.active.camera; + v.setFrom(v1); + v.applyproj(cam.V); + v.applyproj(cam.P); + + var w = App.w(); + var h = App.h(); + + return new Vec2((v.x + 1) * 0.5 * w, (-v.y + 1) * 0.5 * h); + } +} diff --git a/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx b/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx new file mode 100644 index 0000000000..75f128c14b --- /dev/null +++ b/Sources/armory/logicnode/WorldVectorToLocalSpaceNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.math.Vec4; +import iron.object.Object; + +class WorldVectorToLocalSpaceNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Vec4 { + var object: Object = inputs[0].get(); + var worldVec: Vec4 = inputs[1].get(); + + if (object == null || worldVec == null) return null; + + var localVec = new Vec4(); + localVec.sub(object.transform.world.getLoc()); + + localVec.x = worldVec.dot(object.transform.right()); + localVec.y = worldVec.dot(object.transform.look()); + localVec.z = worldVec.dot(object.transform.up()); + + return localVec; + } +} diff --git a/Sources/armory/logicnode/WriteFileNode.hx b/Sources/armory/logicnode/WriteFileNode.hx new file mode 100644 index 0000000000..2fe87a16b3 --- /dev/null +++ b/Sources/armory/logicnode/WriteFileNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class WriteFileNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + // Relative or absolute path to file + var file: String = inputs[1].get(); + var data: String = inputs[2].get(); + + #if kha_krom + var path = Krom.getFilesLocation() + "/" + file; + var bytes = haxe.io.Bytes.ofString(data); + Krom.fileSaveBytes(path, bytes.getData()); + #end + } +} diff --git a/Sources/armory/logicnode/WriteJsonNode.hx b/Sources/armory/logicnode/WriteJsonNode.hx new file mode 100644 index 0000000000..8bd70c6602 --- /dev/null +++ b/Sources/armory/logicnode/WriteJsonNode.hx @@ -0,0 +1,22 @@ +package armory.logicnode; + +class WriteJsonNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + // Relative or absolute path to file + var file: String = inputs[1].get(); + var data: Dynamic = inputs[2].get(); + var s = haxe.Json.stringify(data); + trace(s); + + #if kha_krom + var path = Krom.getFilesLocation() + "/" + file; + var bytes = haxe.io.Bytes.ofString(s); + Krom.fileSaveBytes(path, bytes.getData()); + #end + } +} diff --git a/Sources/armory/logicnode/WriteStorageNode.hx b/Sources/armory/logicnode/WriteStorageNode.hx new file mode 100644 index 0000000000..dccaa435a8 --- /dev/null +++ b/Sources/armory/logicnode/WriteStorageNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +class WriteStorageNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var key: String = inputs[1].get(); + var value: Dynamic = inputs[2].get(); + + var data = iron.system.Storage.data; + if (data == null) return; + + Reflect.setField(data, key, value); + iron.system.Storage.save(); + + runOutput(0); + } +} diff --git a/Sources/armory/math/Helper.hx b/Sources/armory/math/Helper.hx new file mode 100644 index 0000000000..27618d3e60 --- /dev/null +++ b/Sources/armory/math/Helper.hx @@ -0,0 +1,121 @@ +package armory.math; + +import kha.FastFloat; +import iron.math.Vec4; + +class Helper { + + /** + Returns angle in radians between 2 vectors perpendicular to the z axis. + **/ + public static function getAngle(va: Vec4, vb: Vec4): Float { + var vn = Vec4.zAxis(); + var dot = va.dot(vb); + var det = va.x * vb.y * vn.z + + vb.x * vn.y * va.z + + vn.x * va.y * vb.z - + va.z * vb.y * vn.x - + vb.z * vn.y * va.x - + vn.z * va.y * vb.x; + return Math.atan2(det, dot); + } + + /** + Returns a copy of the current vector summed by delta towards the target vector without passing it. + **/ + public static function moveTowards(current: Vec4, target: Vec4, delta: FastFloat): Vec4 { + var v1 = current.clone(); + var v2 = target.clone(); + + var diff = v2.clone().sub(v1); + var length = diff.length(); + + if (length <= delta || length == 0.0) v1.setFrom(v2); + else v1.add(diff.mult(1.0 / length * delta)); + + return v1; + } + + /** + Converts an angle in radians to degrees. + @return angle in degrees + **/ + inline public static function radToDeg(radians: Float): Float { + return 180 / Math.PI * radians; + } + + /** + Converts an angle in degrees to radians. + @return angle in radians + **/ + inline public static function degToRad(degrees: Float): Float { + return Math.PI / 180 * degrees; + } + + /** + Rounds the precision of a float (default 2). + @return float with rounded precision + **/ + public static function roundfp(f: Float, precision = 2): Float { + f *= std.Math.pow(10, precision); + return std.Math.round(f) / std.Math.pow(10, precision); + } + + /** + Clamps a float within some limits. + @return same float, min or max if exceeded limits. + **/ + public static function clamp(f: Float, min: Float, max: Float): Float { + return f < min ? min : f > max ? max : f; + } + + /** + Clamps an integer within some limits. + @return same integer, min or max if exceeded limits. + **/ + public static function clampInt(f: Int, min: Int, max: Int): Int { + return f < min ? min : f > max ? max : f; + } + + /** + Convenience function to map a variable from one coordinate space to + another. Equivalent to unlerp() followed by lerp(). + @param value + @param leftMin The lower bound of the input coordinate space + @param leftMax The higher bound of the input coordinate space + @param rightMin The lower bound of the output coordinate space + @param rightMax The higher bound of the output coordinate space + @return Float + **/ + public static inline function map(value: Float, leftMin: Float, leftMax: Float, rightMin: Float, rightMax: Float): Float { + return rightMin + (value - leftMin) / (leftMax - leftMin) * (rightMax- rightMin); + } + + public static inline function mapInt(value: Int, leftMin: Int, leftMax: Int, rightMin: Int, rightMax: Int): Int { + var result = Std.int(map(value, leftMin, leftMax, rightMin, rightMax)); + return result; + } + + public static inline function mapClamped(value: Float, leftMin: Float, leftMax: Float, rightMin: Float, rightMax: Float): Float { + if (value >= leftMax) return rightMax; + if (value <= leftMin) return rightMin; + return map(value, leftMin, leftMax, rightMin, rightMax); + } + + /** + Return the sign of the given value represented as `1.0` (positive value) + or `-1.0` (negative value). The sign of `0` is `0`. + **/ + public static inline function sign(value: Float): Float { + if (value == 0) return 0; + return (value < 0) ? -1.0 : 1.0; + } + + /** + Return the base-2 logarithm of a number. + **/ + public static inline function log2(v: Float): Float { + // 1.44269504089 = 1.0 / ln(2.0) + return Math.log(v) * 1.44269504089; + } +} diff --git a/Sources/armory/math/Rotator.hx b/Sources/armory/math/Rotator.hx new file mode 100644 index 0000000000..01f538b1ca --- /dev/null +++ b/Sources/armory/math/Rotator.hx @@ -0,0 +1,208 @@ +package armory.math; + +import kha.FastFloat; +import iron.math.Mat4; + +class Rotator { + public var pitch: FastFloat; // X - look up or down around the X axis + public var roll: FastFloat; // Y - axis tilt left or right around the Y axis + public var yaw: FastFloat; // Z - heading or facing angle around the Z (up) axis + + public function new(pitch: FastFloat = 0.0, roll: FastFloat = 0.0, yaw: FastFloat = 0.0) { + this.pitch = pitch; + this.roll = roll; + this.yaw = yaw; + } + + public function toDegrees(): Rotator { + pitch = Helper.radToDeg(pitch); + roll = Helper.radToDeg(roll); + yaw = Helper.radToDeg(yaw); + return this; + } + + public function toRadians(): Rotator { + pitch = Helper.degToRad(pitch); + roll = Helper.degToRad(roll); + yaw = Helper.degToRad(yaw); + return this; + } + + public function cross(r: Rotator): Rotator { + var x2 = roll * r.yaw - yaw * r.roll; + var y2 = yaw * r.pitch - pitch * r.yaw; + var z2 = pitch * r.roll - roll * r.pitch; + pitch = x2; + roll = y2; + yaw = z2; + return this; + } + + public function crossvecs(a: Rotator, b: Rotator): Rotator { + var x2 = a.roll * b.yaw - a.yaw * b.roll; + var y2 = a.yaw * b.pitch - a.pitch * b.yaw; + var z2 = a.pitch * b.roll - a.roll * b.pitch; + pitch = x2; + roll = y2; + yaw = z2; + return this; + } + + public function set(pitch: FastFloat, roll: FastFloat, yaw: FastFloat): Rotator{ + this.pitch = pitch; + this.roll = roll; + this.yaw = yaw; + return this; + } + + public function add(r: Rotator): Rotator { + pitch += r.pitch; + roll += r.roll; + yaw += r.yaw; + return this; + } + + public function addf(pitch: FastFloat, roll: FastFloat, yaw: FastFloat): Rotator { + this.pitch += pitch; + this.roll += roll; + this.yaw += yaw; + return this; + } + + public function addvecs(a: Rotator, b: Rotator): Rotator { + pitch = a.pitch + b.pitch; + roll = a.roll + b.roll; + yaw = a.yaw + b.yaw; + return this; + } + + public function subvecs(a: Rotator, b: Rotator): Rotator { + pitch = a.pitch - b.pitch; + roll = a.roll - b.roll; + yaw = a.yaw - b.yaw; + return this; + } + + public function normalize(): Rotator { + var n = length(); + if (n > 0.0) { + var invN = 1.0 / n; + this.pitch *= invN; this.roll *= invN; this.yaw *= invN; + } + return this; + } + + public function mult(f: FastFloat): Rotator { + pitch *= f; roll *= f; yaw *= f; + return this; + } + + public function dot(r: Rotator): FastFloat { + return pitch * r.pitch + roll * r.roll + yaw * r.yaw; + } + + public function setFrom(r: Rotator): Rotator { + pitch = r.pitch; roll = r.roll; yaw = r.yaw; + return this; + } + + public function clone(): Rotator { + return new Rotator(pitch, roll, yaw); + } + + public static function lerp(from: Rotator, to: Rotator, s: FastFloat): Rotator { + var target = new Rotator(); + target.pitch = from.pitch + (to.pitch - from.pitch) * s; + target.roll = from.roll + (to.roll - from.roll) * s; + target.yaw = from.yaw + (to.yaw - from.yaw) * s; + return target; + } + + public function applyproj(m: Mat4): Rotator { + var pitch = this.pitch; var roll = this.roll; var yaw = this.yaw; + + // Perspective divide + var d = 1.0 / (m._03 * pitch + m._13 * roll + m._23 * yaw + m._33); + + this.pitch = (m._00 * pitch + m._10 * roll + m._20 * yaw + m._30) * d; + this.roll = (m._01 * pitch + m._11 * roll + m._21 * yaw + m._31) * d; + this.yaw = (m._02 * pitch + m._12 * roll + m._22 * yaw + m._32) * d; + + return this; + } + + public function applymat(m: Mat4): Rotator { + var pitch = this.pitch; var roll = this.roll; var yaw = this.yaw; + + this.pitch = m._00 * pitch + m._10 * roll + m._20 * yaw + m._30; + this.roll = m._01 * pitch + m._11 * roll + m._21 * yaw + m._31; + this.yaw = m._02 * pitch + m._12 * roll + m._22 * yaw + m._32; + + return this; + } + + public inline function equals(r: Rotator): Bool { + return pitch == r.pitch && roll == r.roll && yaw == r.yaw; + } + + public inline function length(): FastFloat { + return Math.sqrt(pitch * pitch + roll * roll + yaw * yaw); + } + + public inline function normalizeTo(newLength: FastFloat): Rotator { + var v = normalize(); + v = mult(newLength); + return v; + } + + public function sub(r: Rotator): Rotator { + pitch -= r.pitch; roll -= r.roll; yaw -= r.yaw; + return this; + } + + public static inline function distance(r1: Rotator, r2: Rotator): FastFloat { + return distancef(r1.pitch, r1.roll, r1.yaw, r2.pitch, r2.roll, r2.yaw); + } + + public static inline function distancef(r1pitch: FastFloat, r1roll: FastFloat, r1yaw: FastFloat, r2pitch: FastFloat, r2roll: FastFloat, r2yaw: FastFloat): FastFloat { + var pitch = r1pitch - r2pitch; + var roll = r1roll - r2roll; + var yaw = r1yaw - r2yaw; + return Math.sqrt(pitch * pitch + roll * roll + yaw * yaw); + } + + public function distanceTo(r: Rotator): FastFloat { + return Math.sqrt((r.pitch - pitch) * (r.pitch - pitch) + (r.roll - roll) * (r.roll - roll) + (r.yaw - yaw) * (r.yaw - yaw)); + } + + public function clamp(): Rotator { + this.pitch = clampAxis(this.pitch); + this.roll = clampAxis(this.roll); + this.yaw = clampAxis(this.yaw); + return this; + } + + public static inline function clampAxis(angle: FastFloat): FastFloat { + angle = angle % 360; // Makes the angle between -360 and +360 + if (angle < 0.0) angle += 360.0; + return angle; + } + + public static function xAxis(): Rotator { return new Rotator(1.0, 0.0, 0.0); } + public static function yAxis(): Rotator { return new Rotator(0.0, 1.0, 0.0); } + public static function zAxis(): Rotator { return new Rotator(0.0, 0.0, 1.0); } + public static function one(): Rotator { return new Rotator(1.0, 1.0, 1.0); } + public static function zero(): Rotator { return new Rotator(0.0, 0.0, 0.0); } + public static function back(): Rotator { return new Rotator(0.0, -1.0, 0.0); } + public static function forward(): Rotator { return new Rotator(0.0, 1.0, 0.0); } + public static function down(): Rotator { return new Rotator(0.0, 0.0, -1.0); } + public static function up(): Rotator { return new Rotator(0.0, 0.0, 1.0); } + public static function left(): Rotator { return new Rotator(-1.0, 0.0, 0.0); } + public static function right(): Rotator { return new Rotator(1.0, 0.0, 0.0); } + public static function negativeInfinity(): Rotator { return new Rotator(Math.NEGATIVE_INFINITY, Math.NEGATIVE_INFINITY, Math.NEGATIVE_INFINITY); } + public static function positiveInfinity(): Rotator { return new Rotator(Math.POSITIVE_INFINITY, Math.POSITIVE_INFINITY, Math.POSITIVE_INFINITY); } + + public function toString(): String { + return "(" + this.pitch + ", " + this.roll + ", " + this.yaw + ")"; + } +} diff --git a/Sources/armory/network/Buffer.hx b/Sources/armory/network/Buffer.hx new file mode 100644 index 0000000000..92dbe94db4 --- /dev/null +++ b/Sources/armory/network/Buffer.hx @@ -0,0 +1,173 @@ +package armory.network; + +import haxe.io.Bytes; + +class Buffer { + public var available(default, null):Int = 0; + public var length(default, null):Int = 0; + private var currentOffset:Int = 0; + private var currentData: Bytes = null; + private var chunks:Array = []; + + public function new() {} + + public function writeByte(v:Int) { + var b = Bytes.alloc(1); + b.set(0, v); + writeBytes(b); + } + + public function writeShort(v:Int) { + var b = Bytes.alloc(2); + b.set(0, (v >> 8) & 0xFF); + b.set(1, (v >> 0) & 0xFF); + writeBytes(b); + } + + public function writeInt(v:Int) { + var b = Bytes.alloc(4); + b.set(0, (v >> 24) & 0xFF); + b.set(1, (v >> 16) & 0xFF); + b.set(2, (v >> 8) & 0xFF); + b.set(3, (v >> 0) & 0xFF); + writeBytes(b); + } + + public function writeBytes(data:Bytes) { + chunks.push(data); + available += data.length; + length = available; + } + + public function readAllAvailableBytes():Bytes { + return readBytes(available); + } + + public function readLine():String { + var bytes = readUntil("\n"); + if (bytes == null) { + return null; + } + return StringTools.trim(bytes.toString()); + } + + public function readLinesUntil(delimiter:String):Array { + var bytes = readUntil(delimiter); + if (bytes == null) { + return null; + } + return StringTools.trim(bytes.toString()).split("\n"); + } + + + public function readUntil(delimiter:String):Bytes { + var dl = delimiter.length; + + for (i in 0 ... available - dl) { + var matched = true; + for (j in 0 ... dl) { + if (peekByte(currentOffset + i + j + 1) == delimiter.charCodeAt(j)) { + continue; + } + matched = false; + break; + } + + if (matched) { + var bytes = readBytes(i + dl + 1); + return bytes; + } + } + + return null; + } + + public function readBytes(count:Int):Bytes { + var count2 = Std.int(Math.min(count, available)); + var out = Bytes.alloc(count2); + for (n in 0 ... count2) out.set(n, readByte()); + return out; + } + + public function readUnsignedShort():UInt { + var h = readByte(); + var l = readByte(); + return (h << 8) | (l << 0); + } + + public function readUnsignedInt():UInt { + var v3 = readByte(); + var v2 = readByte(); + var v1 = readByte(); + var v0 = readByte(); + return (v3 << 24) | (v2 << 16) | (v1 << 8) | (v0 << 0); + } + + public function readByte():Int { + if (available <= 0) throw 'No bytes available'; + while (currentData == null || currentOffset >= currentData.length) { + currentOffset = 0; + currentData = chunks.shift(); + } + available--; + length = available; + return currentData.get(currentOffset++); + } + + public function peekByte(offset:Int):Int { + if (available <= 0) throw 'No bytes available'; + var tempOffset = offset; + var tempData = chunks[0]; + if (tempData == null) { + tempData = currentData; + } + var chunkIndex = 0; + while (tempOffset >= tempData.length) { + tempOffset -= tempData.length; + chunkIndex++; + tempData = chunks[chunkIndex]; + } + return tempData.get(tempOffset); + } + + public function peekUntil(byte:Int):Int { + var tempOffset = currentOffset; + var tempData = chunks[0]; + if (tempData == null) { + tempData = currentData; + } + var chunkIndex = 0; + while (tempOffset >= tempData.length) { + tempOffset -= tempData.length; + chunkIndex++; + tempData = chunks[chunkIndex]; + } + while (tempOffset < tempData.length) { + if (tempData.get(tempOffset) == byte) { + return tempOffset + 1; + } + tempOffset++; + } + + return -1; + } + + public function endsWith(e:String):Bool { + var i = available - e.length; + var n = currentOffset; + + if (i <= 0) { + return false; + } + + while (i < available) { + if (peekByte(i) != e.charCodeAt(n)) { + return false; + } + i++; + n++; + } + + return true; + } +} diff --git a/Sources/armory/network/Connect.hx b/Sources/armory/network/Connect.hx new file mode 100644 index 0000000000..40abaf165b --- /dev/null +++ b/Sources/armory/network/Connect.hx @@ -0,0 +1,315 @@ +package armory.network; + +#if sys +import armory.network.WebSocketServer; +import armory.network.WebSocketSecureServer; +import sys.ssl.Key; +import sys.ssl.Certificate; +import armory.network.SocketImpl; +#end +import armory.network.WebSocket; +import armory.network.Types; +import haxe.io.Bytes; +import iron.object.Object; +import armory.system.Event; + +class Connect {} + +class Client extends Connect { + + public static var onOpenEvent: String = "Client.onOpen"; + public static var onMessageEvent: String = "Client.onMessage"; + public static var onErrorEvent: String = "Client.onError"; + public static var onCloseEvent: String = "Client.onClose"; + public static var connections:Map = []; + public static var data:Map = []; + public static var id:Map = []; + + public function new(net_Url: String, net_object: Object) { + if (net_Url != null && net_object != null) { + + if (Client.connections[net_Url] == null) { + + var object = net_object; + + try { + Client.connections[net_Url] = new armory.network.WebSocket(net_Url); + } + catch (error) { + trace(error); + return; + } + + if (object != null) { + final openEvent = Event.get(Client.onOpenEvent); + final messageEvent = Event.get(Client.onMessageEvent); + final errorEvent = Event.get(Client.onErrorEvent); + final closeEvent = Event.get(Client.onCloseEvent); + + Client.connections[net_Url].onopen = function() { + Client.data[net_Url] = "Connection open."; + Client.id[net_Url] = Util.generateUUID(); + if (openEvent != null) { + for (e in openEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + + Client.connections[net_Url].onmessage = function(message: MessageType) { + switch (message) { + case BytesMessage(content): + Client.data[net_Url] = content; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + case StrMessage(content): + Client.data[net_Url] = content; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + } + + Client.connections[net_Url].onerror = function(err) { + Client.data[net_Url] = err; + if (errorEvent != null) { + for (e in errorEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + + Client.connections[net_Url].onclose = function() { + Client.data[net_Url] = "Connection closed."; + if (closeEvent != null) { + for (e in closeEvent) { + if (e.mask == object.uid) { + e.onEvent(); + } + } + } + } + } + } + } + } +} + +class Host extends Connect { + + public static var onOpenEvent: String = "Host.onOpen"; + public static var onMessageEvent: String = "Host.onMessage"; + public static var onErrorEvent: String = "Host.onError"; + public static var onCloseEvent: String = "Host.onClose"; + public static var object: Object = null; + #if sys + public static var connections:Map> = []; + #else + public static var connections = null; + #end + public static var data:Map = []; + public static var id:Map = []; + public static var net_Url: String; + + public function new(net_Domain:String, net_Port:Int, net_Max:Int, net_object:Object) { + + if (net_object == null) return; + + object = net_object; + net_Url = "ws://" + net_Domain + ":" + net_Port; + + #if sys + if (connections[net_Url] != null) return; + connections[net_Url] = new WebSocketServer(net_Domain, net_Port, net_Max); + #end + } +} + +#if sys +class HostHandler extends WebSocketHandler { + + public function new(s: SocketImpl) { + super(s); + + if (Host.object != null) { + final openEvent = Event.get(Host.onOpenEvent); + final messageEvent = Event.get(Host.onMessageEvent); + final errorEvent = Event.get(Host.onErrorEvent); + final closeEvent = Event.get(Host.onCloseEvent); + onopen = function() { + Host.id[Host.net_Url] = id; + if (openEvent != null) { + for (e in openEvent) { + if (e.mask == Host.object.uid) { + e.onEvent(); + } + } + } + } + onmessage = function(message: MessageType) { + switch (message) { + case BytesMessage(content): + Host.data[Host.net_Url] = content; + Host.id[Host.net_Url] = id; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == Host.object.uid) { + e.onEvent(); + } + } + } + case StrMessage(content): + Host.data[Host.net_Url] = content; + Host.id[Host.net_Url] = id; + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == Host.object.uid) { + e.onEvent(); + } + } + } + } + } + onerror = function(error) { + Host.data[Host.net_Url] = error; + Host.id[Host.net_Url] = id; + if (errorEvent != null) { + for (e in errorEvent) { + if (e.mask == Host.object.uid) { + e.onEvent(); + } + } + } + } + onclose = function() { + Host.id[Host.net_Url] = id; + if (closeEvent != null) { + for (e in closeEvent) { + if (e.mask == Host.object.uid) { + e.onEvent(); + } + } + } + } + } + } +} +#end + +class SecureHost extends Connect { + + public static var onOpenEvent: String = "SecureHost.onOpen"; + public static var onMessageEvent: String = "SecureHost.onMessage"; + public static var onErrorEvent: String = "SecureHost.onError"; + public static var onCloseEvent: String = "SecureHost.onClose"; + public static var object: Object = null; + public static var net_Url: String; + #if sys + public static var connections:Map> = []; + #else + public static var connections = null; + #end + public static var data:Map = []; + public static var id:Map = []; + + public function new(net_Domain:String, net_Port:Int, net_Max:Int, net_object:Object, net_Cert: String, net_Key: String) { + if (net_object == null) return; + + object = net_object; + + net_Url = "wss://" + net_Domain + ":" + net_Port; + + #if sys + var cert = Certificate.loadFile(net_Cert); + var key = Key.loadFile(net_Key); + if (connections[net_Url] != null) return; + connections[net_Url] = new WebSocketSecureServer(net_Domain, net_Port, cert, key, cert, net_Max); + #end + } +} + +#if sys +class SecureHostHandler extends WebSocketHandler { + + public function new(s: SocketImpl) { + super(s); + + if (SecureHost.object != null) { + final openEvent = Event.get(SecureHost.onOpenEvent); + final messageEvent = Event.get(SecureHost.onMessageEvent); + final errorEvent = Event.get(SecureHost.onErrorEvent); + final closeEvent = Event.get(SecureHost.onCloseEvent); + onopen = function() { + SecureHost.id[SecureHost.net_Url] = id; + if (openEvent != null) { + for (e in openEvent) { + if (e.mask == SecureHost.object.uid) { + e.onEvent(); + } + } + } + } + onmessage = function(message: MessageType) { + switch (message) { + case BytesMessage(content): + SecureHost.data[SecureHost.net_Url] = content; + SecureHost.id[SecureHost.net_Url] = id; + send(content); + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == SecureHost.object.uid) { + e.onEvent(); + } + } + } + case StrMessage(content): + SecureHost.data[SecureHost.net_Url] = content; + SecureHost.id[SecureHost.net_Url] = id; + send(content); + if (messageEvent != null) { + for (e in messageEvent) { + if (e.mask == SecureHost.object.uid) { + e.onEvent(); + } + } + } + } + } + onerror = function(error) { + SecureHost.data[SecureHost.net_Url] = error; + SecureHost.id[SecureHost.net_Url] = id; + if (errorEvent != null) { + for (e in errorEvent) { + if (e.mask == SecureHost.object.uid) { + e.onEvent(); + } + } + } + } + onclose = function() { + SecureHost.id[SecureHost.net_Url] = id; + if (closeEvent != null) { + for (e in closeEvent) { + if (e.mask == SecureHost.object.uid) { + e.onEvent(); + } + } + } + } + } + } +} +#end diff --git a/Sources/armory/network/Handler.hx b/Sources/armory/network/Handler.hx new file mode 100644 index 0000000000..7f1e899675 --- /dev/null +++ b/Sources/armory/network/Handler.hx @@ -0,0 +1,12 @@ +package armory.network; + +class Handler extends WebSocketCommon { + public function new(socket:SocketImpl) { + super(socket); + isClient = false; + } + + public function handle() { + process(); + } +} diff --git a/Sources/armory/network/HttpHeader.hx b/Sources/armory/network/HttpHeader.hx new file mode 100644 index 0000000000..d0e76ddcf8 --- /dev/null +++ b/Sources/armory/network/HttpHeader.hx @@ -0,0 +1,15 @@ +package armory.network; + +class HttpHeader { + public static inline var SEC_WEBSOCKET_KEY:String = "Sec-WebSocket-Key"; + public static inline var SEC_WEBSOSCKET_ACCEPT:String = "Sec-WebSocket-Accept"; + public static inline var SEC_WEBSOSCKET_VERSION:String = "Sec-WebSocket-Version"; + public static inline var UPGRADE:String = "Upgrade"; + public static inline var HOST:String = "Host"; + public static inline var CONNECTION:String = "Connection"; + public static inline var USER_AGENT:String = "User-Agent"; + public static inline var PRAGMA:String = "Pragma"; + public static inline var CACHE_CONTROL:String = "Cache-Control"; + public static inline var ORIGIN:String = "Origin"; + public static inline var X_WEBSOCKET_REJECT_REASON:String = "X-WebSocket-Reject-Reason"; +} diff --git a/Sources/armory/network/HttpRequest.hx b/Sources/armory/network/HttpRequest.hx new file mode 100644 index 0000000000..3c350303c0 --- /dev/null +++ b/Sources/armory/network/HttpRequest.hx @@ -0,0 +1,52 @@ +package armory.network; + +class HttpRequest { + public var method:String = null; + public var uri:String = null; + public var httpVersion:String = null; + + public var headers:Map = new Map(); + + public function new() {} + + public function addLine(line:String) { + if (method == null) { + var parts = line.split(" "); + method = parts[0]; + uri = parts[1]; + httpVersion = StringTools.trim(parts[2]); + } else { + var n = line.indexOf(":"); + var name = line.substr(0, n); + var value = line.substr(n + 1, line.length); + headers.set(StringTools.trim(name), StringTools.trim(value)); + } + } + + public function build():String { + var sb = new StringBuf(); + + sb.add(method); + sb.add(" "); + if (uri != null) { + sb.add(uri); + sb.add(" "); + } + sb.add(httpVersion); + sb.add("\r\n"); + + for (header in headers.keys()) { + sb.add(header); + sb.add(": "); + sb.add(headers.get(header)); + sb.add("\r\n"); + } + + sb.add("\r\n"); + return sb.toString(); + } + + public function toString():String { + return build(); + } +} diff --git a/Sources/armory/network/HttpResponse.hx b/Sources/armory/network/HttpResponse.hx new file mode 100644 index 0000000000..10f1f3093e --- /dev/null +++ b/Sources/armory/network/HttpResponse.hx @@ -0,0 +1,64 @@ +package armory.network; + +class HttpResponse { + public var httpVersion:String = "HTTP/1.1"; + public var code:Int = -1; + public var text:String = ""; + public var responseDataString:String = null; + + public var headers:Map = new Map(); + + public function new() {} + + public function addLine(line:String) { + if (code == -1) { + var parts = line.split(" "); + httpVersion = parts[0]; + code = Std.parseInt(parts[1]); + text = parts[2]; + } else { + var n = line.indexOf(":"); + var name = line.substr(0, n); + var value = line.substr(n + 1, line.length); + headers.set(StringTools.trim(name), StringTools.trim(value)); + } + } + + public function build():String { + var contentLength = 0; + if (responseDataString != null && responseDataString.length > 0) { + contentLength = responseDataString.length; + } + headers.set("Content-Length", Std.string(contentLength)); + + var sb:StringBuf = new StringBuf(); + + sb.add(httpVersion); + sb.add(" "); + sb.add(code); + if (text != "") { + sb.add(" "); + sb.add(text); + } + sb.add("\r\n"); + + for (header in headers.keys()) { + sb.add(header); + sb.add(": "); + sb.add(headers.get(header)); + sb.add("\r\n"); + } + + sb.add("\r\n"); + + if (responseDataString != null && responseDataString.length > 0) { + sb.add(responseDataString); + } + + return sb.toString(); + } + + public function toString():String { + return build(); + } +} diff --git a/Sources/armory/network/LICENSE.md b/Sources/armory/network/LICENSE.md new file mode 100644 index 0000000000..1cd2aa2316 --- /dev/null +++ b/Sources/armory/network/LICENSE.md @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) [ 2018 - 2022 ] "hxWebSocket" by Ian Harrigan and contributors (fork of "haxe-ws") + +Copyright (c) [ 2022 ] "Connect.hx" by Onek8 (wrapper class for "Armory 3D") + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Sources/armory/network/Log.hx b/Sources/armory/network/Log.hx new file mode 100644 index 0000000000..ab729a6c8f --- /dev/null +++ b/Sources/armory/network/Log.hx @@ -0,0 +1,51 @@ +package armory.network; + +class Log { + public static inline var INFO:Int = 0x000001; + public static inline var DEBUG:Int = 0x000010; + public static inline var DATA:Int = 0x000100; + + public static var mask:Int = 0; + + #if sys + public static var logFn:Dynamic->Void = Sys.println; + #elseif js + public static var logFn:Dynamic->Void = js.html.Console.log; + #end + + public static function info(data:String, id:String = null) { + if (mask & INFO != INFO) { + return; + } + + if (id != null) { + logFn('INFO :: ID-${id} :: ${data}'); + } else { + logFn('INFO :: ${data}'); + } + } + + public static function debug(data:String, id:String = null) { + if (mask & DEBUG != DEBUG) { + return; + } + + if (id != null) { + logFn('DEBUG :: ID-${id} :: ${data}'); + } else { + logFn('DEBUG :: ${data}'); + } + } + + public static function data(data:String, id:String = null) { + if (mask & DATA != DATA) { + return; + } + + if (id != null) { + logFn('DATA :: ID-${id}\n------------------------------\n${data}\n------------------------------'); + } else { + logFn('${data}'); + } + } +} diff --git a/Sources/armory/network/OpCode.hx b/Sources/armory/network/OpCode.hx new file mode 100644 index 0000000000..26829b02ef --- /dev/null +++ b/Sources/armory/network/OpCode.hx @@ -0,0 +1,14 @@ +package armory.network; + +@:enum abstract OpCode(Int) { + var Continuation = 0x00; + var Text = 0x01; + var Binary = 0x02; + var Close = 0x08; + var Ping = 0x09; + var Pong = 0x0A; + + @:to public function toInt() { + return this; + } +} diff --git a/Sources/armory/network/SecureSocketImpl.hx b/Sources/armory/network/SecureSocketImpl.hx new file mode 100644 index 0000000000..dcfa76251d --- /dev/null +++ b/Sources/armory/network/SecureSocketImpl.hx @@ -0,0 +1,3 @@ +package armory.network; + +typedef SecureSocketImpl = sys.ssl.Socket; diff --git a/Sources/armory/network/SocketImpl.hx b/Sources/armory/network/SocketImpl.hx new file mode 100644 index 0000000000..cef3877751 --- /dev/null +++ b/Sources/armory/network/SocketImpl.hx @@ -0,0 +1,19 @@ +package armory.network; + +#if java + +typedef SocketImpl = armory.network.java.NioSocket; + +#elseif cs + +typedef SocketImpl = armory.network.cs.NonBlockingSocket; + +#elseif nodejs + +typedef SocketImpl = armory.network.nodejs.NodeSocket; + +#else + +typedef SocketImpl = sys.net.Socket; + +#end diff --git a/Sources/armory/network/State.hx b/Sources/armory/network/State.hx new file mode 100644 index 0000000000..8e91686b43 --- /dev/null +++ b/Sources/armory/network/State.hx @@ -0,0 +1,10 @@ +package armory.network; + +enum State { + Handshake; + Head; + HeadExtraLength; + HeadExtraMask; + Body; + Closed; +} diff --git a/Sources/armory/network/Types.hx b/Sources/armory/network/Types.hx new file mode 100644 index 0000000000..3295fbb32e --- /dev/null +++ b/Sources/armory/network/Types.hx @@ -0,0 +1,22 @@ +package armory.network; + +import haxe.io.Bytes; + +#if js +typedef BinaryType = js.html.BinaryType; +#else + +@:enum abstract BinaryType(String) { + var ARRAYBUFFER = "arraybuffer"; + + @:to public function toString() { + return this; + } +} + +#end + +enum MessageType { + BytesMessage(content: Buffer); + StrMessage(content: String); +} diff --git a/Sources/armory/network/Utf8Encoder.hx b/Sources/armory/network/Utf8Encoder.hx new file mode 100644 index 0000000000..7c13d9b0cc --- /dev/null +++ b/Sources/armory/network/Utf8Encoder.hx @@ -0,0 +1,15 @@ +package armory.network; + +import haxe.io.Bytes; + +class Utf8Encoder { + static public function encode(str:String):Bytes { + // @TODO: Proper utf8 encoding! + return Bytes.ofString(str); + } + + static public function decode(data:Bytes):String { + // @TODO: Proper utf8 decoding! + return data.toString(); + } +} diff --git a/Sources/armory/network/Util.hx b/Sources/armory/network/Util.hx new file mode 100644 index 0000000000..9ec5ca62f4 --- /dev/null +++ b/Sources/armory/network/Util.hx @@ -0,0 +1,9 @@ +package armory.network; + +import armory.network.uuid.Uuid; + +class Util { + public static function generateUUID():String { + return Uuid.v1(); + } +} diff --git a/Sources/armory/network/WebSocket.hx b/Sources/armory/network/WebSocket.hx new file mode 100644 index 0000000000..f1aa64938a --- /dev/null +++ b/Sources/armory/network/WebSocket.hx @@ -0,0 +1,365 @@ +package armory.network; + +import armory.network.Types; + +#if js + +import haxe.Constraints.Function; +import haxe.io.Bytes; + +#if (haxe_ver < 4) + typedef JsBuffer = js.html.ArrayBuffer; +#else + typedef JsBuffer = js.lib.ArrayBuffer; +#end + +class WebSocket { + private var _url:String; + + private var _ws:js.html.WebSocket = null; + + public function new(url:String, immediateOpen=true) { + _url = url; + if (immediateOpen) { + open(); + } + } + + private function createSocket() { + return new js.html.WebSocket(_url); + } + + public function open() { + if (_ws != null) { + throw "Socket already connected"; + } + _ws = createSocket(); + set_binaryType(Types.BinaryType.ARRAYBUFFER); + if (_onopenbeforeready != null) { + onopen = _onopenbeforeready; + _onopenbeforeready = null; + } + if (_onclosebeforeready != null) { + onclose = _onclosebeforeready; + _onclosebeforeready = null; + } + if (_onerrorbeforeready != null) { + onerror = _onerrorbeforeready; + _onerrorbeforeready = null; + } + if (_onmessagebeforeready != null) { + onmessage = _onmessagebeforeready; + _onmessagebeforeready = null; + } + } + + private var _onopenbeforeready:Function = null; + public var onopen(get, set):Function; + private function get_onopen():Function { + return _ws.onopen; + } + private function set_onopen(value:Function):Function { + if (_ws == null) { + _onopenbeforeready = value; + return value; + } + _ws.onopen = value; + return value; + } + + private var _onclosebeforeready:Function = null; + public var onclose(get, set):Function; + private function get_onclose():Function { + return _ws.onclose; + } + private function set_onclose(value:Function):Function { + if (_ws == null) { + _onclosebeforeready = value; + return value; + } + _ws.onclose = value; + return value; + } + + private var _onerrorbeforeready:Function = null; + public var onerror(get, set):Function; + private function get_onerror():Function { + return _ws.onerror; + } + private function set_onerror(value:Function):Function { + if (_ws == null) { + _onerrorbeforeready = value; + return value; + } + _ws.onerror = value; + return value; + } + + private var _onmessagebeforeready:Function = null; + private var _onmessage:Function = null; + public var onmessage(get, set):Function; + private function get_onmessage():Function { + return _onmessage; + } + private function set_onmessage(value:Function):Function { + if (_ws == null) { + _onmessagebeforeready = value; + return value; + } + _onmessage = value; + _ws.onmessage = function(message: Dynamic) { + if (_onmessage != null) { + if (Std.isOfType(message.data, JsBuffer)) { + var buffer = new Buffer(); + buffer.writeBytes(Bytes.ofData(message.data)); + _onmessage(BytesMessage(buffer)); + } else { + _onmessage(StrMessage(message.data)); + } + } + }; + return value; + } + + public var binaryType(get, set):BinaryType; + private function get_binaryType() { + return _ws.binaryType; + } + private function set_binaryType(value:BinaryType):BinaryType { + _ws.binaryType = value; + return value; + } + + public function close() { + _ws.close(); + onopen = null; + onclose = null; + onerror = null; + onmessage = null; + _onmessage = null; + _ws = null; + } + + public function send(msg:Any) { + if (Std.isOfType(msg, Bytes)) { + var bytes = cast(msg, Bytes); + _ws.send(bytes.getData()); + } else if (Std.isOfType(msg, Buffer)) { + var buffer = cast(msg, Buffer); + _ws.send(buffer.readAllAvailableBytes().getData()); + } else { + _ws.send(msg); + } + } +} + +#elseif sys + +#if (haxe_ver >= 4) +import sys.thread.Thread; +#elseif neko +import neko.vm.Thread; +#elseif cpp +import cpp.vm.Thread; +#end + +import haxe.crypto.Base64; +import haxe.io.Bytes; + +class WebSocket extends WebSocketCommon { + public var _protocol:String; + public var _host:String; + public var _port:Int = 0; + public var _path:String; + + private var _processThread:Thread; + private var _encodedKey:String = "wskey"; + + public var binaryType:BinaryType; + + public var additionalHeaders(get, null):Map; + + public function new(url:String, immediateOpen=true) { + parseUrl(url); + + super(createSocket()); + + if (immediateOpen) { + open(); + } + } + + inline private function parseUrl(url) + { + var urlRegExp = ~/^(\w+?):\/\/([\w\.-]+)(:(\d+))?(\/.*)?$/; + + if ( ! urlRegExp.match(url)) { + throw 'Uri not matching websocket URL "${url}"'; + } + + _protocol = urlRegExp.matched(1); + + _host = urlRegExp.matched(2); + + var parsedPort = Std.parseInt(urlRegExp.matched(4)); + if (parsedPort > 0 ) { + _port = parsedPort; + } + _path = urlRegExp.matched(5); + if (_path == null || _path.length == 0) { + _path = "/"; + } + + } + + private function createSocket():SocketImpl + { + if (_protocol == "wss") { + #if (java || cs) + throw "Secure sockets not implemented"; + #else + if (_port == 0) { + _port = 443; + } + return new SecureSocketImpl(); + #end + } else if (_protocol == "ws") { + if (_port == 0) { + _port = 80; + } + return new SocketImpl(); + } else { + throw 'Unknown protocol $_protocol'; + } + } + + + public function open() { + if (state != State.Handshake) { + throw "Socket already connected"; + } + _socket.setBlocking(true); + _socket.connect(new sys.net.Host(_host), _port); + _socket.setBlocking(false); + + #if !cs + + _processThread = Thread.create(processThread); + _processThread.sendMessage(this); + + #else + + haxe.MainLoop.addThread(function() { + Log.debug("Thread started", this.id); + processLoop(this); + Log.debug("Thread ended", this.id); + }); + + #end + + sendHandshake(); + } + + private function processThread() { + Log.debug("Thread started", this.id); + var ws:WebSocket = Thread.readMessage(true); + processLoop(this); + Log.debug("Thread ended", this.id); + } + + private function processLoop(ws:WebSocket) { + while (ws.state != State.Closed) { // TODO: should think about mutex + ws.process(); + Sys.sleep(.01); + } + } + + function get_additionalHeaders() { + if (additionalHeaders == null) { + additionalHeaders = new Map(); + } + return additionalHeaders; + } + + public function sendHandshake() { + var httpRequest = new HttpRequest(); + httpRequest.method = "GET"; + // TODO: should propably be hostname+port+path? + httpRequest.uri = _path; + httpRequest.httpVersion = "HTTP/1.1"; + + httpRequest.headers.set(HttpHeader.HOST, _host + ":" + _port); + httpRequest.headers.set(HttpHeader.USER_AGENT, "hxWebSockets"); + httpRequest.headers.set(HttpHeader.SEC_WEBSOSCKET_VERSION, "13"); + httpRequest.headers.set(HttpHeader.UPGRADE, "websocket"); + httpRequest.headers.set(HttpHeader.CONNECTION, "Upgrade"); + httpRequest.headers.set(HttpHeader.PRAGMA, "no-cache"); + httpRequest.headers.set(HttpHeader.CACHE_CONTROL, "no-cache"); + httpRequest.headers.set(HttpHeader.ORIGIN, _socket.host().host.toString() + ":" + _socket.host().port); + + + _encodedKey = generateWSKey(); + httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_KEY, _encodedKey); + + if (additionalHeaders != null) { + for ( k in additionalHeaders.keys()) { + httpRequest.headers.set(k, additionalHeaders[k]); + } + } + + sendHttpRequest(httpRequest); + } + + private override function handleData() { + switch (state) { + case State.Handshake: + var httpResponse = recvHttpResponse(); + if (httpResponse == null) { + return; + } + + handshake(httpResponse); + handleData(); + case _: + super.handleData(); + } + + } + + private function handshake(httpResponse:HttpResponse) { + if (httpResponse.code != 101) { + if (onerror != null) { + onerror(httpResponse.headers.get(HttpHeader.X_WEBSOCKET_REJECT_REASON)); + } + close(); + return; + } + + var secKey = httpResponse.headers.get(HttpHeader.SEC_WEBSOSCKET_ACCEPT); + + if (secKey == null) { + trace("This server does not use Sec-WebSocket-Key WebSocket handshake."); + } else { + if (secKey != makeWSKeyResponse(_encodedKey)) { + if (onerror != null) { + onerror("Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value"); + } + close(); + return; + } + } + + _onopenCalled = false; + state = State.Head; + } + + private function generateWSKey():String { + var b = Bytes.alloc(16); + for (i in 0...16) { + b.set(i, Std.random(255)); + } + return Base64.encode(b); + } +} + +#end diff --git a/Sources/armory/network/WebSocketCommon.hx b/Sources/armory/network/WebSocketCommon.hx new file mode 100644 index 0000000000..3db5631c6f --- /dev/null +++ b/Sources/armory/network/WebSocketCommon.hx @@ -0,0 +1,357 @@ +package armory.network; + +import armory.network.Types.MessageType; +import haxe.crypto.Base64; +import haxe.crypto.Sha1; +import haxe.io.Bytes; +import haxe.io.Error; +import armory.network.Util; + +class WebSocketCommon { + public var id:String; + public var state:State = State.Handshake; + + public var isClient = true; + + private var _socket:SocketImpl; + + private var _onopenCalled:Null = null; + private var _lastError:Dynamic = null; + + public var onopen:Void->Void; + public var onclose:Void->Void; + public var onerror:Dynamic->Void; + public var onmessage:MessageType->Void; + + private var _buffer:Buffer = new Buffer(); + + public function new(socket:SocketImpl = null) { + id = Util.generateUUID(); + if (socket == null) { + _socket = new SocketImpl(); + } else { + _socket = socket; + } + _socket.setBlocking(false); + _socket.setTimeout(0); + } + + public function send(data:Any) { + if (Std.isOfType(data, String)) { + Log.data(data, id); + sendFrame(Utf8Encoder.encode(data), OpCode.Text); + } else if (Std.isOfType(data, Bytes)) { + sendFrame(data, OpCode.Binary); + } else if (Std.isOfType(data, Buffer)) { + sendFrame(cast(data, Buffer).readAllAvailableBytes(), OpCode.Binary); + } + } + + private function sendFrame(data:Bytes, type:OpCode) { + writeBytes(prepareFrame(data, type, true)); + } + + private var _isFinal:Bool; + private var _isMasked:Bool; + private var _opcode:OpCode; + private var _frameIsBinary:Bool; + private var _partialLength:Int; + private var _length:Int; + private var _mask:Bytes; + private var _payload:Buffer = null; + private var _lastPong:Date = null; + + private function handleData() { + switch (state) { + case State.Head: + if (_buffer.available < 2) return; + + var b0 = _buffer.readByte(); + var b1 = _buffer.readByte(); + + _isFinal = ((b0 >> 7) & 1) != 0; + _opcode = cast(((b0 >> 0) & 0xF), OpCode); + _frameIsBinary = if (_opcode == OpCode.Text) false; else if (_opcode == OpCode.Binary) true; else _frameIsBinary; + _partialLength = ((b1 >> 0) & 0x7F); + _isMasked = ((b1 >> 7) & 1) != 0; + + state = State.HeadExtraLength; + handleData(); // may be more data + case State.HeadExtraLength: + if (_partialLength == 126) { + if (_buffer.available < 2) return; + _length = _buffer.readUnsignedShort(); + } else if (_partialLength == 127) { + if (_buffer.available < 8) return; + var tmp = _buffer.readUnsignedInt(); + if(tmp != 0) throw 'message too long'; + _length = _buffer.readUnsignedInt(); + } else { + _length = _partialLength; + } + state = State.HeadExtraMask; + handleData(); // may be more data + case State.HeadExtraMask: + if (_isMasked) { + if (_buffer.available < 4) return; + _mask = _buffer.readBytes(4); + } + state = State.Body; + handleData(); // may be more data + case State.Body: + if (_buffer.available < _length) return; + if (_payload == null) { + _payload = new Buffer(); + } + _payload.writeBytes(_buffer.readBytes(_length)); + + switch (_opcode) { + case OpCode.Binary | OpCode.Text | OpCode.Continuation: + if (_isFinal) { + var messageData = _payload.readAllAvailableBytes(); + var unmaskedMessageData = (_isMasked) ? applyMask(messageData, _mask) : messageData; + if (_frameIsBinary) { + if (this.onmessage != null) { + var buffer = new Buffer(); + buffer.writeBytes(unmaskedMessageData); + this.onmessage(BytesMessage(buffer)); + } + } else { + var stringPayload = Utf8Encoder.decode(unmaskedMessageData); + Log.data(stringPayload, id); + if (this.onmessage != null) { + this.onmessage(StrMessage(stringPayload)); + } + } + _payload = null; + } + case OpCode.Ping: + sendFrame(_payload.readAllAvailableBytes(), OpCode.Pong); + case OpCode.Pong: + _lastPong = Date.now(); + case OpCode.Close: + close(); + } + + if (state != State.Closed) state = State.Head; + handleData(); // may be more data + case State.Closed: + close(); + case _: + trace('State not impl: ${state}'); + } + } + + public function close() { + if (state != State.Closed) { + try { + Log.debug("Closed", id); + sendFrame(Bytes.alloc(0), OpCode.Close); + state = State.Closed; + _socket.close(); + } catch (e:Dynamic) { } + + if (onclose != null) { + onclose(); + } + } + } + + private function writeBytes(data:Bytes) { + try { + _socket.output.write(data); + _socket.output.flush(); + } catch (e:Dynamic) { + Log.debug(Std.string(e), id); + if (onerror != null) { + onerror(Std.string(e)); + } + } + } + + private function prepareFrame(data:Bytes, type:OpCode, isFinal:Bool):Bytes { + var out = new Buffer(); + var isMasked = isClient; // All clientes messages must be masked: http://tools.ietf.org/html/rfc6455#section-5.1 + var mask = generateMask(); + var sizeMask = (isMasked ? 0x80 : 0x00); + + out.writeByte(type.toInt() | (isFinal ? 0x80 : 0x00)); + + if (data.length < 126) { + out.writeByte(data.length | sizeMask); + } else if (data.length < 65536) { + out.writeByte(126 | sizeMask); + out.writeShort(data.length); + } else { + out.writeByte(127 | sizeMask); + out.writeInt(0); + out.writeInt(data.length); + } + + if (isMasked) out.writeBytes(mask); + + out.writeBytes(isMasked ? applyMask(data, mask) : data); + return out.readAllAvailableBytes(); + } + + private static function generateMask() { + var maskData = Bytes.alloc(4); + maskData.set(0, Std.random(256)); + maskData.set(1, Std.random(256)); + maskData.set(2, Std.random(256)); + maskData.set(3, Std.random(256)); + return maskData; + } + + private static function applyMask(payload:Bytes, mask:Bytes) { + var maskedPayload = Bytes.alloc(payload.length); + for (n in 0 ... payload.length) maskedPayload.set(n, payload.get(n) ^ mask.get(n % mask.length)); + return maskedPayload; + } + + private function process() { + if (_onopenCalled == false) { + _onopenCalled = true; + if (onopen != null) { + onopen(); + } + } + + if (_lastError != null) { + var error = _lastError; + _lastError = null; + if (onerror != null) { + onerror(error); + } + } + + var needClose = false; + var result = null; + try { + result = SocketImpl.select([_socket], null, null, 0.01); + } catch (e:Dynamic) { + Log.debug("Error selecting socket: " + e); + needClose = true; + } + + if (result != null && needClose == false) { + if (result.read.length > 0) { + try { + while (true) { + var data = Bytes.alloc(1024); + var read = _socket.input.readBytes(data, 0, data.length); + if (read <= 0){ + break; + } + Log.debug("Bytes read: " + read, id); + _buffer.writeBytes(data.sub(0, read)); + } + } catch (e:Dynamic) { + #if cs + + needClose = true; + if (Std.isOfType(e, cs.system.io.IOException)) { + var ioex = cast(e, cs.system.io.IOException); + if (Std.isOfType(ioex.GetBaseException(), cs.system.net.sockets.SocketException)) { + var sockex = cast(ioex.GetBaseException(), cs.system.net.sockets.SocketException); + needClose = !(sockex.SocketErrorCode == cs.system.net.sockets.SocketError.WouldBlock); + } + } + #else + + needClose = !(e == 'Blocking' || (Std.isOfType(e, Error) && (e:Error).match(Error.Blocked))); + + #end + } + + if (needClose == false) { + handleData(); + } + } + } + + if (needClose == true) { // dont want to send the Close frame here + if (state != State.Closed) { + try { + Log.debug("Closed", id); + state = State.Closed; + _socket.close(); + } catch (e:Dynamic) { } + + if (onclose != null) { + onclose(); + } + } + } + } + + public function sendHttpRequest(httpRequest:HttpRequest) { + var data = httpRequest.build(); + + Log.data(data, id); + + try { + _socket.output.write(Bytes.ofString(data)); + _socket.output.flush(); + } catch (e:Dynamic) { + if (onerror != null) { + onerror(Std.string(e)); + } + close(); + } + } + + public function sendHttpResponse(httpResponse:HttpResponse) { + var data = httpResponse.build(); + + Log.data(data, id); + + _socket.output.write(Bytes.ofString(data)); + _socket.output.flush(); + } + + public function recvHttpRequest():HttpRequest { + var response = _buffer.readLinesUntil("\r\n\r\n"); + + if (response == null) { + return null; + } + + var httpRequest = new HttpRequest(); + for (line in response) { + if (line == null || line == "") { + break; + } + httpRequest.addLine(line); + } + + Log.data(httpRequest.toString(), id); + + return httpRequest; + } + + public function recvHttpResponse():HttpResponse { + var response = _buffer.readLinesUntil("\r\n\r\n"); + + if (response == null) { + return null; + } + + var httpResponse = new HttpResponse(); + for (line in response) { + if (line == null || line == "") { + break; + } + httpResponse.addLine(line); + + } + + Log.data(httpResponse.toString(), id); + + return httpResponse; + } + + private inline function makeWSKeyResponse(key:String):String { + return Base64.encode(Sha1.make(Bytes.ofString(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + } +} diff --git a/Sources/armory/network/WebSocketHandler.hx b/Sources/armory/network/WebSocketHandler.hx new file mode 100644 index 0000000000..fb23ece411 --- /dev/null +++ b/Sources/armory/network/WebSocketHandler.hx @@ -0,0 +1,86 @@ +package armory.network; + +class WebSocketHandler extends Handler { + public static var MAX_WAIT_TIME:Int = 1000; // if no handshake has happened after this time (in seconds), we'll consider it dead and disconnect + + private var _creationTime:Float; + + public function new(socket:SocketImpl) { + super(socket); + _creationTime = Sys.time(); + _socket.setBlocking(false); + Log.debug('New socket handler', id); + } + + public override function handle() { + if (this.state == State.Handshake && Sys.time() - _creationTime > (MAX_WAIT_TIME / 1000)) { + Log.info('No handshake detected in ${MAX_WAIT_TIME}ms, closing connection', id); + this.close(); + return; + } + super.handle(); + } + + private override function handleData() { + switch (state) { + case State.Handshake: + var httpRequest = recvHttpRequest(); + if (httpRequest == null) { + return; + } + + handshake(httpRequest); + handleData(); + case _: + super.handleData(); + } + } + + public function handshake(httpRequest:HttpRequest) { + var httpResponse = new HttpResponse(); + + httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_VERSION, "13"); + if (httpRequest.method != "GET" || httpRequest.httpVersion != "HTTP/1.1") { + httpResponse.code = 400; + httpResponse.text = "Bad"; + httpResponse.headers.set(HttpHeader.CONNECTION, "close"); + httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Bad request'); + } else if (httpRequest.headers.get(HttpHeader.SEC_WEBSOSCKET_VERSION) != "13") { + httpResponse.code = 426; + httpResponse.text = "Upgrade"; + httpResponse.headers.set(HttpHeader.CONNECTION, "close"); + httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported websocket client version: ${httpRequest.headers.get(HttpHeader.SEC_WEBSOSCKET_VERSION)}, Only version 13 is supported.'); + } else if (httpRequest.headers.get(HttpHeader.UPGRADE) != "websocket") { + httpResponse.code = 426; + httpResponse.text = "Upgrade"; + httpResponse.headers.set(HttpHeader.CONNECTION, "close"); + httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported upgrade header: ${httpRequest.headers.get(HttpHeader.UPGRADE)}.'); + } else if (httpRequest.headers.get(HttpHeader.CONNECTION).indexOf("Upgrade") == -1) { + httpResponse.code = 426; + httpResponse.text = "Upgrade"; + httpResponse.headers.set(HttpHeader.CONNECTION, "close"); + httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported connection header: ${httpRequest.headers.get(HttpHeader.CONNECTION)}.'); + } else { + Log.debug('Handshaking', id); + var key = httpRequest.headers.get(HttpHeader.SEC_WEBSOCKET_KEY); + var result = makeWSKeyResponse(key); + Log.debug('Handshaking key - ${result}', id); + + httpResponse.code = 101; + httpResponse.text = "Switching Protocols"; + httpResponse.headers.set(HttpHeader.UPGRADE, "websocket"); + httpResponse.headers.set(HttpHeader.CONNECTION, "Upgrade"); + httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_ACCEPT, result); + } + + sendHttpResponse(httpResponse); + + if (httpResponse.code == 101) { + _onopenCalled = false; + state = State.Head; + Log.debug('Connected', id); + } else { + close(); + } + } +} diff --git a/Sources/armory/network/WebSocketSecureServer.hx b/Sources/armory/network/WebSocketSecureServer.hx new file mode 100644 index 0000000000..f1a4ec6231 --- /dev/null +++ b/Sources/armory/network/WebSocketSecureServer.hx @@ -0,0 +1,39 @@ +package armory.network; + +import haxe.Constraints; + +import sys.ssl.Key; +import sys.ssl.Certificate; + +@:generic +class WebSocketSecureServer + #if (haxe_ver < 4) + Void>, Handler)> + #else + Void> & Handler> + #end + extends WebSocketServer { + + private var _cert:Certificate; + private var _key:Key; + private var _caChain:Certificate; + + public function new(host:String, port:Int, cert:Certificate, key:Key, caChain:Certificate, maxConnections:Int = 1) { + super(host, port, maxConnections); + + _cert=cert; + _key=key; + _caChain=caChain; + } + + + override private function createSocket() { + var socket = new SecureSocketImpl(); + socket.setHostname(_host); + + socket.setCA(_caChain); + socket.setCertificate(_cert, _key); + socket.verifyCert = false; + return socket; + } +} diff --git a/Sources/armory/network/WebSocketServer.hx b/Sources/armory/network/WebSocketServer.hx new file mode 100644 index 0000000000..e30f55258a --- /dev/null +++ b/Sources/armory/network/WebSocketServer.hx @@ -0,0 +1,196 @@ +package armory.network; + +import haxe.Constraints; +import haxe.MainLoop; +import haxe.io.Error; + +@:generic +class WebSocketServer + #if (haxe_ver < 4) + Void>, Handler)> { + #else + Void> & Handler> { + #end + + private var _serverSocket:SocketImpl; + + private var _host:String; + private var _port:Int; + private var _maxConnections:Int; + + public var handlers:Array = []; + + private var _stopServer:Bool = false; + public var sleepAmount:Float = 0.01; + + public var onClientAdded:T->Void = null; + public var onClientRemoved:T->Void = null; + + #if threaded_handlers + private var _serverMutex:sys.thread.Mutex = new sys.thread.Mutex(); + private var _handlersClosed:Array = []; + #end + + public function new(host:String, port:Int, maxConnections:Int = 1) { + _host = host; + _port = port; + _maxConnections = maxConnections; + } + + private function createSocket() { + return new SocketImpl(); + } + + public function sendAll(data:Any) { + for (h in handlers) { + h.send(data); + } + } + + public function start() { + _stopServer = false; + _serverSocket = createSocket(); + _serverSocket.setBlocking(false); + _serverSocket.bind(new sys.net.Host(_host), _port); + _serverSocket.listen(_maxConnections); + Log.info('Starting server - ${_host}:${_port} (maxConnections: ${_maxConnections})'); + + #if cs + while (true) { + var continueLoop = tick(); + if (continueLoop == false) { + break; + } + + Sys.sleep(sleepAmount); + } + + #elseif threaded_server + MainLoop.addThread(function() { + while (true) { + var continueLoop = tick(); + if (continueLoop == false) { + break; + } + + Sys.sleep(sleepAmount); + } + }); + + #elseif target.threaded + MainLoop.addThread(function() { + while (true) { + var continueLoop = tick(); + if (continueLoop == false) { + break; + } + + Sys.sleep(sleepAmount); + } + }); + + #else + MainLoop.add(function() { + tick(); + Sys.sleep(sleepAmount); + }); + + #end + } + + private function handleNewSocket(socket) { + var handler = new T(socket); + handlers.push(handler); + + Log.debug("Adding to web server handler to list - total: " + handlers.length, handler.id); + if (onClientAdded != null) { + onClientAdded(handler); + } + + #if threaded_handlers + private function handlerThread() { + var handler:T = sys.thread.Thread.readMessage(true); + while (handler.state != State.Closed) { + handler.handle(); + Sys.sleep(sleepAmount); + } + _serverMutex.acquire(); + _handlersClosed.push(handler); + _serverMutex.release(); + } + + var thread = sys.thread.Thread.create(handlerThread); + thread.sendMessage(handler); + #end + } + + public function tick() { + if (_stopServer == true) { + for (h in handlers) { + h.close(); + } + handlers = []; + try { + _serverSocket.close(); + } catch (e:Dynamic) { } + return false; + } + + try { + var clientSocket:SocketImpl = _serverSocket.accept(); + if (clientSocket != null) { // HL doesnt throw blocking, instead returns null + handleNewSocket(clientSocket); + } + } catch (e:Dynamic) { + if (e != 'Blocking' && e != Error.Blocked) { + throw(e); + } + } + + #if !threaded_handlers + + for (h in handlers) { + h.handle(); + } + + var toRemove = []; + for (h in handlers) { + if (h.state == State.Closed) { + toRemove.push(h); + } + } + + for (h in toRemove) { + handlers.remove(h); + Log.debug("Removing web server handler from list - total: " + handlers.length, h.id); + if (onClientRemoved != null) { + onClientRemoved(h); + } + } + + #else + + _serverMutex.acquire(); + while (_handlersClosed.length > 0) { + var h = _handlersClosed.shift(); + handlers.remove(h); + Log.debug("Removing web server handler from list - total: " + handlers.length, h.id); + if (onClientRemoved != null) { + onClientRemoved(h); + } + } + _serverMutex.release(); + + #end + + return true; + } + + public function stop() { + _stopServer = true; + } + + public function totalHandlers(): Int { + return handlers.length; + } +} diff --git a/Sources/armory/network/nodejs/NodeSocket.hx b/Sources/armory/network/nodejs/NodeSocket.hx new file mode 100644 index 0000000000..edc6806765 --- /dev/null +++ b/Sources/armory/network/nodejs/NodeSocket.hx @@ -0,0 +1,110 @@ +package armory.network.nodejs; + +import js.node.Net; +import js.node.net.Server; +import js.node.net.Socket; +import sys.net.Host; + +class NodeSocket { + public function new() { + } + + private var _socket:Socket = null; + public var input:NodeSocketInput = null; + public var output:NodeSocketOutput = null; + private function setSocket(s:Socket) { + _socket = s; + input = new NodeSocketInput(this); + output = new NodeSocketOutput(this); + } + + private var _server:Server = null; + private function createServer():Void { + if (_server == null) { + _server = Net.createServer(acceptConnection); + } + } + + private static var _connections:Array = []; + private var _newConnections:Array = []; + private function acceptConnection(socket:Socket) { + socket.setTimeout(0); + var nodeSocket = new NodeSocket(); + nodeSocket.setSocket(socket); + _connections.push(nodeSocket); + _newConnections.push(nodeSocket); + } + + public function accept() { + if (_newConnections.length == 0) { + throw "Blocking"; + } + + var socket = _newConnections.pop(); + return socket; + + } + + public function listen(connections:Int):Void { + if (_host == null) { + throw "You must bind the Socket to an address!"; + } + + createServer(); + _server.listen({ + host: _host.host, + port: _port, + backlog: connections + }); + } + + private var _host:Host = null; + private var _port:Int; + public function bind(host:Host, port:Int):Void { + _host = host; + _port = port; + } + + public function setBlocking(blocking:Bool) { + + } + + public function setTimeout(timeout:Int) { + } + + public function close() { + if (_server != null) { + _server.close(); + } + if (_socket != null) { + _socket.destroy(); + } + } + + public static function select(read : Array, write : Array, others : Array, ?timeout : Float) : { read: Array, write: Array, others: Array } { + if (write != null && write.length > 0) { + throw "Not implemented"; + } + if (others != null && others.length > 0) { + throw "Not implemented"; + } + + var ret = null; + for (c in _connections) { + if (read.indexOf(c) != -1) { + if (c.input.hasData == true) { + if (ret == null) { + ret = { + read: [], + write: null, + others: null + } + } + ret.read.push(c); + } + } + } + + return ret; + } +} diff --git a/Sources/armory/network/nodejs/NodeSocketInput.hx b/Sources/armory/network/nodejs/NodeSocketInput.hx new file mode 100644 index 0000000000..a9cbc0a212 --- /dev/null +++ b/Sources/armory/network/nodejs/NodeSocketInput.hx @@ -0,0 +1,50 @@ +package armory.network.nodejs; + +import haxe.io.Bytes; +import js.node.Buffer; + +@:access(armory.network.nodejs.NodeSocket) +class NodeSocketInput { + private var _socket:NodeSocket; + + public var hasData = false; + + private var _buffer: Buffer = null; + public function new(socket:NodeSocket) { + _socket = socket; + _socket._socket.on("data", onData); + } + + private function onData(data:Any) { + var a = []; + if (_buffer != null) { + a.push(_buffer); + } + a.push(Buffer.from(data)); + _buffer = Buffer.concat(a); + hasData = true; + } + + public function readBytes(s:Bytes, pos:Int, len:Int):Int { + if (_buffer == null) { + return 0; + } + var n:Int = _buffer.length; + if (n > len) { + n = len; + } + if (len > n) { + len = n; + } + var part = _buffer.slice(0, len); + var remain = null; + if (_buffer.length > len) { + remain = _buffer.slice(len); + } + var src = part.hxToBytes(); + s.blit(pos, src, 0, len); + hasData = (remain != null); + _buffer = remain; + return n; + } +} diff --git a/Sources/armory/network/nodejs/NodeSocketOutput.hx b/Sources/armory/network/nodejs/NodeSocketOutput.hx new file mode 100644 index 0000000000..2a89d1843d --- /dev/null +++ b/Sources/armory/network/nodejs/NodeSocketOutput.hx @@ -0,0 +1,28 @@ +package armory.network.nodejs; + +import haxe.io.Bytes; +import js.node.Buffer; + +@:access(armory.network.nodejs.NodeSocket) +class NodeSocketOutput { + private var _socket:NodeSocket; + + private var _buffer:Buffer = null; + public function new(socket:NodeSocket) { + _socket = socket; + } + + public function write(data:Bytes) { + var a = []; + if (_buffer != null) { + a.push(_buffer); + } + a.push(Buffer.hxFromBytes(data)); + _buffer = Buffer.concat(a); + } + + public function flush() { + _socket._socket.write(_buffer); + _buffer = null; + } +} diff --git a/Sources/armory/network/uuid/Uuid.hx b/Sources/armory/network/uuid/Uuid.hx new file mode 100644 index 0000000000..82ff636448 --- /dev/null +++ b/Sources/armory/network/uuid/Uuid.hx @@ -0,0 +1,267 @@ +package armory.network.uuid; + +// copied as is from https://raw.githubusercontent.com/flashultra/uuid/master/src/uuid/Uuid.hx +// not added as a dep to reduce complexity + +import haxe.ds.Vector; +import haxe.Int64; +import haxe.io.Bytes; +import haxe.crypto.Md5; +import haxe.crypto.Sha1; + +class Uuid { + public inline static var DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + public inline static var URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; + public inline static var ISO_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; + public inline static var X500_DN = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; + public inline static var NIL = '00000000-0000-0000-0000-000000000000'; + + public inline static var LOWERCASE_BASE26 = "abcdefghijklmnopqrstuvwxyz"; + public inline static var UPPERCASE_BASE26 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public inline static var NO_LOOK_ALIKES_BASE51 = "2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnpqrtwxyz"; // without 1, l, I, 0, O, o, u, v, 5, S, s + public inline static var FLICKR_BASE58 = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; // without similar characters 0/O, 1/I/l + public inline static var BASE_70 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+!@#$^"; + public inline static var BASE_85 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#"; + public inline static var COOKIE_BASE90 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+-./:<=>?@[]^_`{|}~"; + public inline static var NANO_ID_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + public inline static var NUMBERS_BIN = "01"; + public inline static var NUMBERS_OCT = "01234567"; + public inline static var NUMBERS_DEC = "0123456789"; + public inline static var NUMBERS_HEX = "0123456789abcdef"; + + + static var lastMSecs:Float = 0; + static var lastNSecs = 0; + static var clockSequenceBuffer:Int = -1; + static var regexp:EReg = ~/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; + + static var rndSeed:Int64 = Int64.fromFloat(#if js js.lib.Date.now() #else Sys.time()*1000 #end); + static var state0 = splitmix64_seed(rndSeed); + static var state1 = splitmix64_seed(rndSeed + Std.random(10000) + 1); + private static var DVS:Int64 = Int64.make(0x00000001, 0x00000000); + + private static function splitmix64_seed(index:Int64):Int64 { + var result:Int64 = (index + Int64.make(0x9E3779B9, 0x7F4A7C15)); + result = (result ^ (result >> 30)) * Int64.make(0xBF58476D, 0x1CE4E5B9); + result = (result ^ (result >> 27)) * Int64.make(0x94D049BB, 0x133111EB); + return result ^ (result >> 31); + } + + public static function randomFromRange(min:Int, max:Int):Int { + var s1:Int64 = state0; + var s0:Int64 = state1; + state0 = s0; + s1 ^= s1 << 23; + state1 = s1 ^ s0 ^ (s1 >>> 18) ^ (s0 >>> 5); + var result:Int = ((state1 + s0) % (max - min + 1)).low; + result = (result < 0) ? -result : result; + return result + min; + } + + public static function randomByte():Int { + return randomFromRange(0, 255); + } + + public static function fromShort(shortUuid:String, separator:String = '-', fromAlphabet:String = FLICKR_BASE58):String { + var uuid = Uuid.convert(shortUuid,fromAlphabet,NUMBERS_HEX); + return hexToUuid(uuid,separator); + } + + public static function toShort(uuid:String, separator:String = '-',toAlphabet:String = FLICKR_BASE58):String { + uuid = StringTools.replace(uuid, separator, '').toLowerCase(); + return Uuid.convert(uuid,NUMBERS_HEX,toAlphabet); + } + + public static function fromNano(nanoUuid:String, separator:String = '-', fromAlphabet:String = NANO_ID_ALPHABET):String { + var uuid = Uuid.convert(nanoUuid,fromAlphabet,NUMBERS_HEX); + return hexToUuid(uuid,separator); + } + + public static function toNano(uuid:String, separator:String = '-',toAlphabet:String = NANO_ID_ALPHABET):String { + uuid = StringTools.replace(uuid, separator, '').toLowerCase(); + return Uuid.convert(uuid,NUMBERS_HEX,toAlphabet); + } + + public static function v1(node:Bytes = null, optClockSequence:Int = -1, msecs:Float = -1, optNsecs:Int = -1, ?randomFunc:Void->Int, separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String { + if (randomFunc == null) randomFunc = randomByte; + var buffer:Bytes = Bytes.alloc(16); + if (node == null) { + node = Bytes.alloc(6); + for (i in 0...6) + node.set(i, randomFunc()); + node.set(0, node.get(0) | 0x01); + } + if (clockSequenceBuffer == -1) { + clockSequenceBuffer = (randomFunc() << 8 | randomFunc()) & 0x3fff; + } + var clockSeq = optClockSequence; + if (optClockSequence == -1) { + clockSeq = clockSequenceBuffer; + } + if (msecs == -1) { + msecs = Math.fround(#if js js.lib.Date.now() #else Sys.time()*1000 #end); + } + var nsecs = optNsecs; + if (optNsecs == -1) { + nsecs = lastNSecs + 1; + } + var dt = (msecs - lastMSecs) + (nsecs - lastNSecs) / 10000; + if (dt < 0 && (optClockSequence == -1)) { + clockSeq = (clockSeq + 1) & 0x3fff; + } + if ((dt < 0 || msecs > lastMSecs) && optNsecs == -1) { + nsecs = 0; + } + if (nsecs >= 10000) { + throw "Can't create more than 10M uuids/sec"; + } + lastMSecs = msecs; + lastNSecs = nsecs; + clockSequenceBuffer = clockSeq; + + msecs += 12219292800000; + var imsecs:Int64 = Int64.fromFloat(msecs); + var tl:Int = (((imsecs & 0xfffffff) * 10000 + nsecs) % DVS).low; + buffer.set(0, tl >>> 24 & 0xff); + buffer.set(1, tl >>> 16 & 0xff); + buffer.set(2, tl >>> 8 & 0xff); + buffer.set(3, tl & 0xff); + + var tmh:Int = ((imsecs / DVS * 10000) & 0xfffffff).low; + buffer.set(4, tmh >>> 8 & 0xff); + buffer.set(5, tmh & 0xff); + + buffer.set(6, tmh >>> 24 & 0xf | 0x10); + buffer.set(7, tmh >>> 16 & 0xff); + + buffer.set(8, clockSeq >>> 8 | 0x80); + buffer.set(9, clockSeq & 0xff); + + for (i in 0...6) + buffer.set(i + 10, node.get(i)); + + var uuid = stringify(buffer,separator); + if ( shortUuid ) uuid = Uuid.toShort(uuid,separator,toAlphabet); + + return uuid; + } + + public static function v3(name:String, namespace:String = "", separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String { + namespace = StringTools.replace(namespace, '-', ''); + var buffer = Md5.make(Bytes.ofHex(namespace + Bytes.ofString(name).toHex())); + buffer.set(6, (buffer.get(6) & 0x0f) | 0x30); + buffer.set(8, (buffer.get(8) & 0x3f) | 0x80); + var uuid = stringify(buffer,separator); + if ( shortUuid ) uuid = Uuid.toShort(uuid,separator,toAlphabet); + return uuid; + } + + public static function v4(randBytes:Bytes = null, ?randomFunc:Void->Int, separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String { + if ( randomFunc == null) randomFunc = randomByte; + var buffer:Bytes = randBytes; + if ( buffer == null ) { + buffer = Bytes.alloc(16); + for (i in 0...16) { + buffer.set(i, randomFunc()); + } + } else { + if ( buffer.length < 16) throw "Random bytes should be at least 16 bytes"; + } + buffer.set(6, (buffer.get(6) & 0x0f) | 0x40); + buffer.set(8, (buffer.get(8) & 0x3f) | 0x80); + var uuid = stringify(buffer,separator); + if ( shortUuid ) uuid = Uuid.toShort(uuid,separator,toAlphabet); + return uuid; + } + + public static function v5(name:String, namespace:String = "", separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String { + namespace = StringTools.replace(namespace, '-', ''); + var buffer = Sha1.make(Bytes.ofHex(namespace + Bytes.ofString(name).toHex())); + buffer.set(6, (buffer.get(6) & 0x0f) | 0x50); + buffer.set(8, (buffer.get(8) & 0x3f) | 0x80); + var uuid = stringify(buffer,separator); + if ( shortUuid ) uuid = Uuid.toShort(uuid,separator,toAlphabet); + return uuid; + } + + public static function stringify(data:Bytes,separator:String = "-"):String { + return hexToUuid(data.toHex(),separator); + } + + public static function parse(uuid:String, separator:String = "-"):Bytes { + return Bytes.ofHex(StringTools.replace(uuid, separator, '')); + } + + public static function validate(uuid:String,separator:String = "-"):Bool { + if ( separator == "") { + uuid = uuid.substr(0, 8) + "-" + uuid.substr(8, 4) + "-" + uuid.substr(12, 4) + "-" + uuid.substr(16, 4) + "-" + uuid.substr(20, 12); + } else if ( separator != "-") { + uuid = StringTools.replace(uuid, separator, '-'); + } + return regexp.match(uuid); + } + + public static function version(uuid:String,separator:String = "-"):Int { + uuid = StringTools.replace(uuid, separator, ''); + return Std.parseInt("0x"+uuid.substr(12,1)); + } + + public static function hexToUuid( hex:String , separator:String):String { + return ( hex.substr(0, 8) + separator + hex.substr(8, 4) + separator + hex.substr(12, 4) + separator + hex.substr(16, 4) + separator + hex.substr(20, 12)); + } + + public static function convert(number:String, fromAlphabet:String, toAlphabet:String ):String { + var fromBase:Int = fromAlphabet.length; + var toBase:Int = toAlphabet.length; + var len = number.length; + var buf:String = ""; + var numberMap:Vector = new Vector(len); + var divide:Int = 0, newlen:Int = 0; + for (i in 0...len) { + numberMap[i] = fromAlphabet.indexOf(number.charAt(i)); + } + do { + divide = 0; + newlen = 0; + for(i in 0...len) { + divide = divide * fromBase + numberMap[i]; + if (divide >= toBase) { + numberMap[newlen++] = Math.floor( divide / toBase); + divide = divide % toBase; + } else if (newlen > 0) { + numberMap[newlen++] = 0; + } + } + len = newlen; + buf = toAlphabet.charAt(divide) + buf; + } while (newlen !=0 ); + + return buf; + } + + public static function nanoId(len:Int=21,alphabet:String=NANO_ID_ALPHABET,?randomFunc:Void->Int):String { + if ( randomFunc == null ) randomFunc = randomByte; + if ( alphabet == null ) throw "Alphabet cannot be null"; + if ( alphabet.length == 0 || alphabet.length >= 256 ) throw "Alphabet must contain between 1 and 255 symbols"; + if ( len <= 0 ) throw "Length must be greater than zero"; + var mask:Int = (2 << Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1; + var step:Int = Math.ceil(1.6 * mask * len / alphabet.length); + var sb = new StringBuf(); + while (sb.length != len) { + for(i in 0...step) { + var rnd = randomFunc(); + var aIndex:Int = rnd & mask; + if (aIndex < alphabet.length) { + sb.add(alphabet.charAt(aIndex)); + if (sb.length == len) break; + } + } + } + return sb.toString(); + } + + public static function short(toAlphabet:String = FLICKR_BASE58, ?randomFunc:Void->Int):String { + return Uuid.v4(randomFunc,true,toAlphabet); + } +} diff --git a/Sources/armory/object/TransformExtension.hx b/Sources/armory/object/TransformExtension.hx new file mode 100644 index 0000000000..fec02832f5 --- /dev/null +++ b/Sources/armory/object/TransformExtension.hx @@ -0,0 +1,72 @@ +package armory.object; + +import iron.math.Mat4; +import iron.math.Vec4; +import iron.math.Quat; +import iron.object.Transform; + +class TransformExtension { + + public static function overlap(t1: Transform, t2: Transform): Bool { + return t1.worldx() + t1.dim.x / 2 > t2.worldx() - t2.dim.x / 2 && t1.worldx() - t1.dim.x / 2 < t2.worldx() + t2.dim.x / 2 && + t1.worldy() + t1.dim.y / 2 > t2.worldy() - t2.dim.y / 2 && t1.worldy() - t1.dim.y / 2 < t2.worldy() + t2.dim.y / 2 && + t1.worldz() + t1.dim.z / 2 > t2.worldz() - t2.dim.z / 2 && t1.worldz() - t1.dim.z / 2 < t2.worldz() + t2.dim.z / 2; + } + + /** + * Returns the world (global) position. + * @return Vec4 + */ + public static inline function getWorldPosition(t: Transform): Vec4 { + return new Vec4(t.worldx(), t.worldy(), t.worldz(), 1.0); + } + + /** + * Returns the given local vector in world coordinates + * @param localVec + * @return Vec4 + */ + public static inline function getWorldVecFromLocal(t: Transform, localVec: Vec4): Vec4 { + return localVec.clone().applymat4(t.worldUnpack); + } + /** + * Returns the given world vector in local coordinates + * @param worldVec + * @return Vec4 + */ + public static inline function getLocalVecFromWorld(t: Transform, worldVec: Vec4): Vec4 { + return worldVec.clone().applymat4(Mat4.identity().getInverse(t.worldUnpack)); + } + /** + * Returns the given world vector in transform orientation + * @param worldVec + * @return Vec4 + **/ + public static inline function worldVecToOrientation(t: Transform, worldVec: Vec4): Vec4 { + var right = t.right().normalize(); + right.mult(worldVec.x); + + var look = t.look().normalize(); + look.mult(worldVec.y); + + var up = t.up().normalize(); + up.mult(worldVec.z); + + return new Vec4().add(right).add(look).add(up); + } + + /** + * Returns the given world vector in local orientation + * @param worldVec Vector in world orientation + * @return Local vector + **/ + public static inline function getWorldVectorAlongLocalAxis(t: Transform, worldVec: Vec4): Vec4 { + + var localVec = new Vec4(); + localVec.x = worldVec.dot(t.right().normalize()); + localVec.y = worldVec.dot(t.look().normalize()); + localVec.z = worldVec.dot(t.up().normalize()); + + return localVec; + } +} diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx new file mode 100644 index 0000000000..1ebbbdc8ba --- /dev/null +++ b/Sources/armory/object/Uniforms.hx @@ -0,0 +1,230 @@ +package armory.object; + +import iron.Scene; +import iron.object.Object; +import iron.data.MaterialData; +import iron.math.Vec4; + +import armory.renderpath.Postprocess; + +using StringTools; + +// Structure for setting shader uniforms +class Uniforms { + + public static function register() { + iron.object.Uniforms.externalTextureLinks = [textureLink]; + iron.object.Uniforms.externalVec2Links = [vec2Link]; + iron.object.Uniforms.externalVec3Links = [vec3Link]; + iron.object.Uniforms.externalVec4Links = []; + iron.object.Uniforms.externalFloatLinks = [floatLink]; + iron.object.Uniforms.externalIntLinks = []; + } + + public static function textureLink(object: Object, mat: MaterialData, link: String): Null { + switch (link) { + case "_nishitaLUT": { + if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); + return armory.renderpath.Nishita.data.lut; + } + #if arm_ltc + case "_ltcMat": { + if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC(); + return armory.data.ConstData.ltcMatTex; + } + case "_ltcMag": { + if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC(); + return armory.data.ConstData.ltcMagTex; + } + #end + #if arm_morph_target + case "_morphDataPos": { + return cast(object, iron.object.MeshObject).morphTarget.morphDataPos; + } + case "_morphDataNor": { + return cast(object, iron.object.MeshObject).morphTarget.morphDataNor; + } + #end + } + + var target = iron.RenderPath.active.renderTargets.get(link.endsWith("_depth") ? link.substr(0, link.length - 6) : link); + return target != null ? target.image : null; + } + + public static function vec3Link(object: Object, mat: MaterialData, link: String): Null { + var v: Vec4 = null; + switch (link) { + #if arm_hosek + case "_hosekA": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.A.x; + v.y = armory.renderpath.HosekWilkie.data.A.y; + v.z = armory.renderpath.HosekWilkie.data.A.z; + } + } + case "_hosekB": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.B.x; + v.y = armory.renderpath.HosekWilkie.data.B.y; + v.z = armory.renderpath.HosekWilkie.data.B.z; + } + } + case "_hosekC": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.C.x; + v.y = armory.renderpath.HosekWilkie.data.C.y; + v.z = armory.renderpath.HosekWilkie.data.C.z; + } + } + case "_hosekD": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.D.x; + v.y = armory.renderpath.HosekWilkie.data.D.y; + v.z = armory.renderpath.HosekWilkie.data.D.z; + } + } + case "_hosekE": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.E.x; + v.y = armory.renderpath.HosekWilkie.data.E.y; + v.z = armory.renderpath.HosekWilkie.data.E.z; + } + } + case "_hosekF": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.F.x; + v.y = armory.renderpath.HosekWilkie.data.F.y; + v.z = armory.renderpath.HosekWilkie.data.F.z; + } + } + case "_hosekG": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.G.x; + v.y = armory.renderpath.HosekWilkie.data.G.y; + v.z = armory.renderpath.HosekWilkie.data.G.z; + } + } + case "_hosekH": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.H.x; + v.y = armory.renderpath.HosekWilkie.data.H.y; + v.z = armory.renderpath.HosekWilkie.data.H.z; + } + } + case "_hosekI": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.I.x; + v.y = armory.renderpath.HosekWilkie.data.I.y; + v.z = armory.renderpath.HosekWilkie.data.I.z; + } + } + case "_hosekZ": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.Z.x; + v.y = armory.renderpath.HosekWilkie.data.Z.y; + v.z = armory.renderpath.HosekWilkie.data.Z.z; + } + } + #end + + #if rp_voxels + case "_cameraPositionSnap": { + v = iron.object.Uniforms.helpVec; + var camera = iron.Scene.active.camera; + v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz()); + var l = camera.lookWorld(); + var e = Main.voxelgiHalfExtents; + v.x += l.x * e * 0.9; + v.y += l.y * e * 0.9; + var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range + v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f); + } + #end + } + return v; + } + + public static function vec2Link(object: Object, mat: MaterialData, link: String): Null { + var v: Vec4 = null; + switch (link) { + case "_nishitaDensity": { + var w = Scene.active.world; + if (w != null) { + v = iron.object.Uniforms.helpVec; + // We only need Rayleigh and Mie density in the sky shader -> Vec2 + v.x = w.raw.nishita_density[0]; + v.y = w.raw.nishita_density[1]; + } + } + } + + return v; + } + + public static function floatLink(object: Object, mat: MaterialData, link: String): Null { + switch (link) { + #if rp_dynres + case "_dynamicScale": { + return armory.renderpath.DynamicResolutionScale.dynamicScale; + } + #end + #if arm_debug + case "_debugFloat": { + return armory.trait.internal.DebugConsole.debugFloat; + } + #end + #if rp_voxels + case "_voxelBlend": { // Blend current and last voxels + var freq = armory.renderpath.RenderPathCreator.voxelFreq; + return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq; + } + #end + #if rp_bloom + case "_bloomSampleScale": { + return Postprocess.bloom_uniforms[3]; + } + #end + } + return null; + } +} diff --git a/Sources/armory/renderpath/Downsampler.hx b/Sources/armory/renderpath/Downsampler.hx new file mode 100644 index 0000000000..cd8bf31407 --- /dev/null +++ b/Sources/armory/renderpath/Downsampler.hx @@ -0,0 +1,104 @@ +package armory.renderpath; + +import haxe.ds.ReadOnlyArray; + +import iron.RenderPath; +import iron.data.MaterialData; +import iron.object.Object; + +import armory.math.Helper; + +abstract class Downsampler { + + public static var currentMipLevel(default, null) = 0; + public static var numMipLevels(default, null) = 0; + + static var isRegistered = false; + + final path: RenderPath; + final shaderPassHandle: String; + final maxNumMips: Int; + final mipmaps: Array; + + function new(path: RenderPath, shaderPassHandle: String, maxNumMips: Int) { + this.path = path; + this.shaderPassHandle = shaderPassHandle; + this.maxNumMips = maxNumMips; + + this.mipmaps = new Array(); + mipmaps.resize(maxNumMips); + } + + public static function create(path: RenderPath, shaderPassHandle: String, rtName: String, maxNumMips: Int = 10): Downsampler { + if (!isRegistered) { + isRegistered = true; + iron.object.Uniforms.externalIntLinks.push(intLink); + } + + // TODO, implement when Kha supports render targets in compute shaders + // and allows to query whether compute shaders are available + // if (RenderPath.hasComputeSupport()) { + // return new DownsamplerCompute(path, shaderPassHandle, rtName, maxNumMips); + // } + // else { + return new DownsamplerFragment(path, shaderPassHandle, rtName, maxNumMips); + // } + } + + static function intLink(object: Object, mat: MaterialData, link: String): Null { + return switch (link) { + case "_downsampleCurrentMip": Downsampler.currentMipLevel; + case "_downsampleNumMips": Downsampler.numMipLevels; + default: null; + } + } + + public inline function getMipmaps(): ReadOnlyArray { + return mipmaps; + } + + abstract public function dispatch(srcImageName: String, numMips: Int = 0): Void; +} + +private class DownsamplerFragment extends Downsampler { + + public function new(path: RenderPath, shaderPassHandle: String, rtName: String, maxNumMips: Int) { + super(path, shaderPassHandle, maxNumMips); + + var prevScale = 1.0; + for (i in 0...maxNumMips) { + var t = new RenderTargetRaw(); + t.name = rtName + "_mip_" + i; + t.width = 0; + t.height = 0; + t.scale = (prevScale *= 0.5); + t.format = Inc.getHdrFormat(); + + mipmaps[i] = path.createRenderTarget(t); + } + + path.loadShader(shaderPassHandle); + } + + public function dispatch(srcImageName: String, numMips: Int = 0) { + Helper.clampInt(numMips, 0, maxNumMips); + if (numMips == 0) { + numMips = maxNumMips; + } + + final srcImageRT = path.renderTargets.get(srcImageName); + assert(Error, srcImageRT != null, 'Could not find render target with name $srcImageName'); + + final srcImage = srcImageRT.image; + assert(Error, srcImage != null); + + Downsampler.numMipLevels = numMips; + for (i in 0...numMips) { + Downsampler.currentMipLevel = i; + path.setTarget(mipmaps[i].raw.name); + path.clearTarget(); + path.bindTarget(i == 0 ? srcImageName : mipmaps[i - 1].raw.name, "tex"); + path.drawShader(shaderPassHandle); + } + } +} diff --git a/Sources/armory/renderpath/DynamicResolutionScale.hx b/Sources/armory/renderpath/DynamicResolutionScale.hx new file mode 100644 index 0000000000..98259b4c13 --- /dev/null +++ b/Sources/armory/renderpath/DynamicResolutionScale.hx @@ -0,0 +1,51 @@ +package armory.renderpath; + +import iron.RenderPath; + +class DynamicResolutionScale { + + public static var dynamicScale = 1.0; + + static var firstFrame = true; + static inline var startScaleMs = 30; + static inline var scaleRangeMs = 10; + static inline var maxScale = 0.6; + + public static function run(path: RenderPath) { + if (firstFrame) { + iron.App.notifyOnRender(render); + firstFrame = false; + return; + } + + // TODO: execute once per frame max + if (frameTimeAvg > startScaleMs && frameTimeAvg < 100) { + var overTime = Math.min(scaleRangeMs, frameTimeAvg - startScaleMs); + var scale = 1.0 - (overTime / scaleRangeMs) * (1.0 - maxScale); + var w = Std.int(iron.App.w() * scale); + var h = Std.int(iron.App.h() * scale); + path.setCurrentViewport(w, h); + path.setCurrentScissor(w, h); + dynamicScale = scale; + } + else dynamicScale = 1.0; + } + + static var frameTime: Float; + static var lastTime: Float = 0; + static var totalTime: Float = 0; + static var frames = 0; + static var frameTimeAvg = 0.0; + + public static function render(g: kha.graphics4.Graphics) { + frameTime = kha.Scheduler.realTime() - lastTime; + lastTime = kha.Scheduler.realTime(); + totalTime += frameTime; + frames++; + if (totalTime >= 1) { + frameTimeAvg = Std.int(totalTime / frames * 10000) / 10; + totalTime = 0; + frames = 0; + } + } +} diff --git a/Sources/armory/renderpath/HosekWilkie.hx b/Sources/armory/renderpath/HosekWilkie.hx new file mode 100644 index 0000000000..0babf5914a --- /dev/null +++ b/Sources/armory/renderpath/HosekWilkie.hx @@ -0,0 +1,123 @@ +// An Analytic Model for Full Spectral Sky-Dome Radiance +// Lukas Hosek and Alexander Wilkie +// Based on https://github.com/ddiakopoulos/sandbox +package armory.renderpath; + +import kha.math.FastVector3; +import iron.data.WorldData; + +class HosekWilkieRadianceData { + + public var A = new FastVector3(); + public var B = new FastVector3(); + public var C = new FastVector3(); + public var D = new FastVector3(); + public var E = new FastVector3(); + public var F = new FastVector3(); + public var G = new FastVector3(); + public var H = new FastVector3(); + public var I = new FastVector3(); + public var Z = new FastVector3(); + + function evaluateSpline(spline: Array, index: Int, stride: Int, value: Float): Float { + return + 1 * Math.pow(1 - value, 5) * spline[index ] + + 5 * Math.pow(1 - value, 4) * Math.pow(value, 1) * spline[index + 1 * stride] + + 10 * Math.pow(1 - value, 3) * Math.pow(value, 2) * spline[index + 2 * stride] + + 10 * Math.pow(1 - value, 2) * Math.pow(value, 3) * spline[index + 3 * stride] + + 5 * Math.pow(1 - value, 1) * Math.pow(value, 4) * spline[index + 4 * stride] + + 1 * Math.pow(value, 5) * spline[index + 5 * stride]; + } + + function clamp(n: Int, lower: Int, upper: Int): Int { + return n <= lower ? lower : n >= upper ? upper : n; + } + + function clampF(n: Float, lower: Float, upper: Float): Float { + return n <= lower ? lower : n >= upper ? upper : n; + } + + function evaluate(dataset: Array, index: Int, stride: Int, turbidity: Float, albedo: Float, sunTheta: Float): Float { + // Splines are functions of elevation^1/3 + var elevationK: Float = Math.pow(Math.max(0.0, 1.0 - sunTheta / (Math.PI / 2.0)), 1.0 / 3.0); + + // Table has values for turbidity 1..10 + var turbidity0: Int = clamp(Std.int(turbidity), 1, 10); + var turbidity1: Int = Std.int(Math.min(turbidity0 + 1, 10)); + var turbidityK: Float = clampF(turbidity - turbidity0, 0.0, 1.0); + + var datasetA0Index = index; + var datasetA1Index = index + stride * 6 * 10; + + var a0t0: Float = evaluateSpline(dataset, datasetA0Index + stride * 6 * (turbidity0 - 1), stride, elevationK); + var a1t0: Float = evaluateSpline(dataset, datasetA1Index + stride * 6 * (turbidity0 - 1), stride, elevationK); + var a0t1: Float = evaluateSpline(dataset, datasetA0Index + stride * 6 * (turbidity1 - 1), stride, elevationK); + var a1t1: Float = evaluateSpline(dataset, datasetA1Index + stride * 6 * (turbidity1 - 1), stride, elevationK); + + return a0t0 * (1 - albedo) * (1 - turbidityK) + a1t0 * albedo * (1 - turbidityK) + a0t1 * (1 - albedo) * turbidityK + a1t1 * albedo * turbidityK; + } + + function hosek_wilkie(cos_theta: Float, gamma: Float, cos_gamma: Float, A: FastVector3, B: FastVector3, C: FastVector3, D: FastVector3, E: FastVector3, F: FastVector3, G: FastVector3, H: FastVector3, I: FastVector3): FastVector3 { + var val = (1.0 + cos_gamma * cos_gamma); + var chix = val / Math.pow(1.0 + H.x * H.x - 2.0 * cos_gamma * H.x, 1.5); + var chiy = val / Math.pow(1.0 + H.y * H.y - 2.0 * cos_gamma * H.y, 1.5); + var chiz = val / Math.pow(1.0 + H.z * H.z - 2.0 * cos_gamma * H.z, 1.5); + var chi = new FastVector3(chix, chiy, chiz); + + var vx = (1.0 + A.x * Math.exp(B.x / (cos_theta + 0.01))) * (C.x + D.x * Math.exp(E.x * gamma) + F.x * (cos_gamma * cos_gamma) + G.x * chi.x + I.x * Math.sqrt(Math.max(0.0, cos_theta))); + var vy = (1.0 + A.y * Math.exp(B.y / (cos_theta + 0.01))) * (C.y + D.y * Math.exp(E.y * gamma) + F.y * (cos_gamma * cos_gamma) + G.y * chi.y + I.y * Math.sqrt(Math.max(0.0, cos_theta))); + var vz = (1.0 + A.z * Math.exp(B.z / (cos_theta + 0.01))) * (C.z + D.z * Math.exp(E.z * gamma) + F.z * (cos_gamma * cos_gamma) + G.z * chi.z + I.z * Math.sqrt(Math.max(0.0, cos_theta))); + return new FastVector3(vx, vy, vz); + } + + function setVector(v: FastVector3, index: Int, f: Float) { + index == 0 ? v.x = f : index == 1 ? v.y = f : v.z = f; + } + + public function new() {} + + public function recompute(sunTheta: Float, turbidity: kha.FastFloat, albedo: kha.FastFloat, normalizedSunY: Float) { + for (i in 0...3) { + setVector(A, i, evaluate(HosekWilkieData.datasetsRGB[i], 0, 9, turbidity, albedo, sunTheta)); + setVector(B, i, evaluate(HosekWilkieData.datasetsRGB[i], 1, 9, turbidity, albedo, sunTheta)); + setVector(C, i, evaluate(HosekWilkieData.datasetsRGB[i], 2, 9, turbidity, albedo, sunTheta)); + setVector(D, i, evaluate(HosekWilkieData.datasetsRGB[i], 3, 9, turbidity, albedo, sunTheta)); + setVector(E, i, evaluate(HosekWilkieData.datasetsRGB[i], 4, 9, turbidity, albedo, sunTheta)); + setVector(F, i, evaluate(HosekWilkieData.datasetsRGB[i], 5, 9, turbidity, albedo, sunTheta)); + setVector(G, i, evaluate(HosekWilkieData.datasetsRGB[i], 6, 9, turbidity, albedo, sunTheta)); + + // Swapped in the dataset + setVector(H, i, evaluate(HosekWilkieData.datasetsRGB[i], 8, 9, turbidity, albedo, sunTheta)); + setVector(I, i, evaluate(HosekWilkieData.datasetsRGB[i], 7, 9, turbidity, albedo, sunTheta)); + + setVector(Z, i, evaluate(HosekWilkieData.datasetsRGBRad[i], 0, 1, turbidity, albedo, sunTheta)); + } + + if (normalizedSunY != 0.0) { + var S: FastVector3 = hosek_wilkie(Math.cos(sunTheta), 0, 1.0, A, B, C, D, E, F, G, H, I); + S.x *= Z.x; + S.y *= Z.y; + S.z *= Z.z; + var dotS = S.dot(new FastVector3(0.2126, 0.7152, 0.0722)); + Z.x /= dotS; + Z.y /= dotS; + Z.z /= dotS; + Z = Z.mult(normalizedSunY); + } + } +} + +class HosekWilkie { + + public static var data: HosekWilkieRadianceData = null; + + public static function recompute(world: WorldData) { + if (world == null || world.raw.sun_direction == null) return; + if (data == null) data = new HosekWilkieRadianceData(); + // Clamp Z for night cycle + var sunZ = world.raw.sun_direction[2] > 0 ? world.raw.sun_direction[2] : 0; + var sunPositionX = Math.acos(sunZ); + var normalizedSunY: kha.FastFloat = 1.15; + data.recompute(sunPositionX, world.raw.turbidity, world.raw.ground_albedo, normalizedSunY); + } +} diff --git a/Sources/armory/renderpath/HosekWilkieData.hx b/Sources/armory/renderpath/HosekWilkieData.hx new file mode 100644 index 0000000000..9922cf733d --- /dev/null +++ b/Sources/armory/renderpath/HosekWilkieData.hx @@ -0,0 +1,3855 @@ +/* +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +/* ============================================================================ + +This file is part of a sample implementation of the analytical skylight and +solar radiance models presented in the SIGGRAPH 2012 paper + + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + +and the 2013 IEEE CG&A paper + + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + + Version: 1.4a, February 22nd, 2013 + +Version history: + +1.4a February 22nd, 2013 + Removed unnecessary and counter-intuitive solar radius parameters + from the interface of the colourspace sky dome initialisation functions. + +1.4 February 11th, 2013 + Fixed a bug which caused the relative brightness of the solar disc + and the sky dome to be off by a factor of about 6. The sun was too + bright: this affected both normal and alien sun scenarios. The + coefficients of the solar radiance function were changed to fix this. + +1.3 January 21st, 2013 (not released to the public) + Added support for solar discs that are not exactly the same size as + the terrestrial sun. Also added support for suns with a different + emission spectrum ("Alien World" functionality). + +1.2a December 18th, 2012 + Fixed a mistake and some inaccuracies in the solar radiance function + explanations found in ArHosekSkyModel.h. The actual source code is + unchanged compared to version 1.2. + +1.2 December 17th, 2012 + Native RGB data and a solar radiance function that matches the turbidity + conditions were added. + +1.1 September 2012 + The coefficients of the spectral model are now scaled so that the output + is given in physical units: W / (m^-2 * sr * nm). Also, the output of the + XYZ model is now no longer scaled to the range [0...1]. Instead, it is + the result of a simple conversion from spectral data via the CIE 2 degree + standard observer matching functions. Therefore, after multiplication + with 683 lm / W, the Y channel now corresponds to luminance in lm. + +1.0 May 11th, 2012 + Initial release. + + +Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if +an updated version of this code has been published! + +============================================================================ */ + +/* +This file contains the coefficient data for the RGB colour space version of +the model. +*/ + +package armory.renderpath; + +class HosekWilkieData { + + public static var datasetRGB1 = [ + // albedo 0, turbidity 1 + -1.099459e+000, + -1.335146e-001, + -4.083223e+000, + 5.919603e+000, + -1.104166e-001, + 1.600158e+000, + -1.326538e-006, + 4.917807e+000, + 5.127716e-001, + -1.169858e+000, + -1.832793e-001, + 9.694744e-001, + 9.495762e-002, + -4.738918e-002, + 2.194171e-001, + 1.095749e-001, + 3.603604e+000, + 3.815119e-001, + -9.665225e-001, + -1.403888e-001, + 5.194457e+000, + -1.107607e+000, + -8.135181e-001, + 4.969661e+000, + -2.300508e-001, + -2.489350e+000, + 1.279158e+000, + -1.292508e+000, + -1.299552e-001, + -2.071404e+000, + -4.752482e-002, + 1.215598e+000, + -1.904179e+000, + 3.027985e-001, + 8.707768e+000, + 6.332446e-002, + -9.264666e-001, + -1.696780e-001, + 4.574070e+000, + -4.232936e-001, + -7.575833e+000, + 5.079755e+000, + -2.576343e-001, + -4.506805e+000, + 6.908129e-001, + -1.139072e+000, + -1.796056e-001, + 1.923311e+000, + 6.788529e+000, + -2.364389e+000, + -1.064041e+000, + 1.717010e-001, + 1.534681e+000, + 5.015810e-001, + // albedo 0, turbidity 2 + -1.107257e+000, + -1.384411e-001, + -4.285744e+000, + 5.713157e+000, + -1.015992e-001, + 1.372638e+000, + 6.555893e-002, + 5.127514e+000, + 6.550471e-001, + -1.187337e+000, + -1.969013e-001, + 8.551048e-001, + 5.289708e-002, + -7.626406e-002, + 1.733153e-002, + 1.779454e-001, + 3.801038e+000, + 4.742709e-001, + -9.685321e-001, + -1.553308e-001, + 4.732492e+000, + -1.178935e+000, + -7.852791e-001, + 4.604492e+000, + -2.666518e-001, + -2.367663e+000, + 1.177527e+000, + -1.252817e+000, + -5.129949e-002, + -2.800433e+000, + -1.295992e-002, + 1.308964e+000, + -2.204331e+000, + 7.276011e-001, + 8.699265e+000, + 1.188388e-001, + -9.459509e-001, + -2.322133e-001, + 4.375041e+000, + -1.712018e-001, + -7.451681e+000, + 5.078019e+000, + -4.223538e-001, + -4.595561e+000, + 1.074719e+000, + -1.125092e+000, + -1.796750e-001, + 1.626399e+000, + 6.989743e+000, + -2.406382e+000, + -9.060383e-001, + 2.961611e-001, + 1.337715e+000, + 5.438140e-001, + // albedo 0, turbidity 3 + -1.135338e+000, + -1.716160e-001, + -1.499253e+000, + 2.373491e+000, + -1.654023e-001, + 9.566404e-001, + 1.113453e-001, + 4.528473e+000, + 6.579439e-001, + -1.132780e+000, + -1.456214e-001, + -1.736672e+000, + 1.756589e+000, + -1.087003e-001, + 3.757927e-001, + 2.525070e-001, + 7.178513e+000, + 5.003814e-001, + -1.167176e+000, + -2.927225e-001, + 5.727667e+000, + -3.139244e+000, + -6.425204e-001, + 2.822634e+000, + -1.457812e-001, + -6.787080e+000, + 1.017072e+000, + -1.042529e+000, + 4.110823e-002, + -4.000629e+000, + 4.362364e+000, + 1.090540e+000, + -1.338674e+000, + 8.246964e-001, + 1.095249e+001, + 2.912211e-001, + -1.061598e+000, + -2.096143e-001, + 3.803155e+000, + -7.977069e+000, + -3.637880e+000, + 3.707671e+000, + -1.903128e-001, + -3.397953e+000, + 9.971500e-001, + -1.073560e+000, + -2.077964e-001, + 1.492052e+000, + 1.626322e+001, + -5.015304e+000, + -4.059889e-001, + 2.659782e-001, + 6.395380e-001, + 5.634436e-001, + // albedo 0, turbidity 4 + -1.172794e+000, + -2.111186e-001, + -1.360013e+000, + 1.604080e+000, + -8.473723e-002, + 7.217312e-001, + 1.548030e-001, + 4.257010e+000, + 6.328974e-001, + -1.238374e+000, + -2.670827e-001, + 3.247678e-001, + 5.466311e-001, + -7.425952e-001, + 5.276440e-001, + 2.678026e-002, + 5.484169e+000, + 6.814734e-001, + -1.176923e+000, + -2.574586e-001, + 2.304045e+000, + -2.797678e+000, + 1.464405e+000, + 1.998552e+000, + 2.550559e-001, + -4.199772e+000, + 7.544892e-001, + -1.003284e+000, + 1.943984e-002, + -2.145066e+000, + 1.030924e+001, + -1.525413e+001, + -2.023010e+000, + 5.448699e-001, + 8.159497e+000, + 5.539148e-001, + -1.060017e+000, + -2.037206e-001, + 2.483018e+000, + -4.595459e+000, + 6.526991e+000, + 4.031804e+000, + 1.206513e-001, + -2.586527e+000, + 7.875752e-001, + -1.081141e+000, + -2.123302e-001, + 1.092275e+000, + 2.683841e+000, + -4.166938e+000, + -1.396582e+000, + 4.371205e-001, + 1.030233e+000, + 6.664862e-001, + // albedo 0, turbidity 5 + -1.222392e+000, + -2.651924e-001, + -4.625037e-001, + 3.521964e-001, + 2.148855e-002, + 5.078494e-001, + 1.791590e-001, + 3.852516e+000, + 5.998216e-001, + -1.424610e+000, + -4.710155e-001, + -1.826815e-001, + 1.786277e+000, + -1.952442e+000, + 5.277612e-001, + -1.773629e-002, + 2.415874e+000, + 6.701272e-001, + -1.130655e+000, + -1.358609e-001, + 9.171203e-001, + -4.660394e+000, + 6.251162e+000, + 1.904529e+000, + 2.639668e-001, + 1.856130e+000, + 8.228440e-001, + -9.739015e-001, + -6.674749e-002, + -4.768897e-001, + 1.248589e+001, + -1.994688e+001, + -2.353043e+000, + 5.885575e-001, + 1.287251e+000, + 4.830135e-001, + -1.082178e+000, + -1.974495e-001, + 1.050245e+000, + -4.792855e+000, + 8.663406e+000, + 3.246969e+000, + 1.556731e-001, + 8.117442e-001, + 8.050376e-001, + -1.063354e+000, + -1.727108e-001, + 9.681592e-001, + 2.736077e+000, + -4.969269e+000, + -8.360570e-001, + 5.994612e-001, + 1.024039e+000, + 6.786935e-001, + // albedo 0, turbidity 6 + -1.261936e+000, + -3.053676e-001, + -4.262222e-001, + 4.000196e-001, + -2.059388e-002, + 4.721802e-001, + 1.480028e-001, + 3.505343e+000, + 6.121337e-001, + -1.681088e+000, + -6.971919e-001, + -1.105652e-001, + 7.437426e-001, + -6.594399e-001, + 2.254221e-001, + 8.710195e-002, + 1.263913e+000, + 5.681865e-001, + -9.453001e-001, + 3.460388e-002, + 6.067038e-001, + -1.985128e+000, + 3.457236e+000, + 2.655483e+000, + -1.162354e-002, + 3.304716e+000, + 1.001950e+000, + -1.086609e+000, + -2.029011e-001, + -6.399170e-001, + 6.926885e+000, + -1.512189e+001, + -3.793051e+000, + 9.456120e-001, + 2.222222e-001, + 2.893725e-001, + -1.041259e+000, + -1.388790e-001, + 1.147331e+000, + 6.282086e+000, + 3.679836e+000, + 4.398314e+000, + -1.355232e-001, + 1.031134e+000, + 9.273509e-001, + -1.063473e+000, + -1.916051e-001, + 6.556979e-001, + -3.371891e-003, + -3.699664e+000, + -1.926783e+000, + 7.371154e-001, + 1.179975e+000, + 6.367068e-001, + // albedo 0, turbidity 7 + -1.336390e+000, + -3.778927e-001, + -7.259477e-001, + 2.270247e-001, + 4.627513e-001, + 1.366459e-001, + 2.637347e-001, + 3.292059e+000, + 4.998211e-001, + -2.119878e+000, + -1.055472e+000, + 5.422052e-001, + 7.826648e-001, + -1.286065e+000, + 9.517905e-001, + -1.432358e-001, + -2.379816e-001, + 5.910513e-001, + -7.761432e-001, + 2.124336e-001, + -6.845184e-001, + -9.812342e-001, + 4.347257e+000, + 9.671980e-001, + 3.773150e-001, + 5.789529e+000, + 9.646598e-001, + -1.118734e+000, + -3.513815e-001, + 5.500918e-001, + 9.449627e-001, + -1.262070e+001, + -1.825280e+000, + 4.731260e-001, + -3.326892e+000, + 3.568768e-001, + -1.026437e+000, + -8.257946e-002, + 3.221701e-001, + 1.198372e+001, + 1.555130e+000, + 2.560304e+000, + 1.406465e-001, + 2.912858e+000, + 8.643181e-001, + -1.069949e+000, + -2.029607e-001, + 5.825042e-001, + -2.398595e-003, + -3.278335e+000, + -1.349882e+000, + 7.208433e-001, + 8.505164e-001, + 6.625391e-001, + // albedo 0, turbidity 8 + -1.392309e+000, + -4.454945e-001, + -5.664000e-001, + 6.283393e-001, + -3.761727e-001, + 6.949802e-001, + 7.748178e-002, + 3.192797e+000, + 5.968661e-001, + -2.713405e+000, + -1.395112e+000, + 2.029230e-001, + 1.877272e-001, + -3.715859e-001, + -1.652929e-001, + 2.385861e-001, + -4.150768e-001, + 1.375467e-001, + -9.588644e-001, + 2.433900e-002, + -1.527493e+000, + -9.632874e-001, + 5.496269e+000, + 1.094931e+000, + 2.004044e-001, + 6.084554e+000, + 1.369604e+000, + -8.028546e-001, + -2.473563e-001, + 1.617898e+000, + 2.073591e+000, + -1.149446e+001, + -8.394131e-001, + 2.726847e-001, + -4.634538e+000, + 1.367293e-001, + -1.198326e+000, + -1.804865e-001, + -3.565414e-001, + 4.073200e+000, + 1.662086e+000, + 1.239770e+000, + 3.367978e-001, + 2.997402e+000, + 9.360383e-001, + -1.013531e+000, + -1.859060e-001, + 5.799857e-001, + 1.331883e+001, + -4.346873e+000, + -1.113820e+000, + 5.275714e-001, + 8.045177e-001, + 6.496373e-001, + // albedo 0, turbidity 9 + -1.530103e+000, + -6.107468e-001, + -3.841771e-001, + 1.881508e+000, + -1.464807e+000, + 6.654690e-001, + -5.950797e-006, + 2.738912e+000, + 8.101012e-001, + -2.415469e+000, + -1.057499e+000, + -4.161968e-001, + -2.357548e+000, + 6.300296e-001, + 6.224915e-001, + 1.545048e-002, + 2.038561e+000, + -1.339415e-001, + -3.096796e+000, + -1.465688e+000, + -1.199232e+000, + 4.567061e+000, + 3.260980e+000, + -9.794907e-001, + 8.950491e-001, + 2.049235e+000, + 1.331015e+000, + 2.713904e-001, + 2.852852e-001, + 1.202090e+000, + -8.206784e+000, + -5.805762e+000, + 1.804431e+000, + -6.090648e-001, + -1.990902e+000, + 3.288858e-001, + -1.456580e+000, + -3.455960e-001, + -6.409257e-002, + 1.667697e+001, + -2.311094e+000, + -9.771104e-001, + 6.759863e-001, + 1.245136e+000, + 7.911932e-001, + -9.860389e-001, + -2.099564e-001, + 2.946650e-001, + -3.547800e-003, + -2.268313e+000, + -6.205647e-002, + 4.705185e-001, + 8.657995e-001, + 6.856284e-001, + // albedo 0, turbidity 10 + -1.971736e+000, + -9.414047e-001, + -3.400557e-001, + 1.468763e+000, + -1.474284e+000, + 5.501062e-001, + -1.109750e-005, + 2.356370e+000, + 9.001702e-001, + -1.589845e+000, + -7.797079e-001, + -5.582240e-001, + -8.137376e-001, + 5.846617e-001, + 1.129459e-001, + -2.658005e-002, + 2.707248e+000, + -2.112486e-001, + -6.940173e+000, + -2.823963e+000, + -1.620848e+000, + 1.090696e+000, + 2.391730e+000, + 1.370047e+000, + 5.890462e-001, + 1.728400e+000, + 1.331253e+000, + 1.293144e+000, + -1.919778e-003, + 1.644206e+000, + -8.666967e-001, + -7.161953e+000, + -1.385018e+000, + -1.505374e-001, + -1.388643e+000, + 2.530122e-001, + -1.488880e+000, + -2.495496e-001, + -2.377137e-001, + 1.167714e+001, + -8.617124e-001, + 1.053828e+000, + 1.992744e-001, + 3.633564e-001, + 8.553304e-001, + -1.060891e+000, + -4.035829e-001, + 2.823207e-001, + -2.369798e-003, + -1.876577e+000, + -5.950265e-001, + 4.241017e-001, + 3.140802e-001, + 6.631669e-001, + // albedo 1, turbidity 1 + -1.101204e+000, + -1.351353e-001, + -4.030882e+000, + 6.096353e+000, + -1.148599e-001, + 1.606507e+000, + -1.555474e-006, + 4.436084e+000, + 5.973715e-001, + -1.154597e+000, + -1.923378e-001, + 8.512132e-001, + 2.934895e-001, + -6.522777e-002, + 1.389077e-001, + 9.091469e-002, + 3.133307e+000, + 2.108541e-001, + -1.031588e+000, + -1.546804e-001, + 5.266214e+000, + -9.491390e-001, + -7.184867e-001, + 4.875626e+000, + -1.911907e-001, + -2.865642e+000, + 1.087895e+000, + -1.159454e+000, + -9.546699e-002, + -1.508146e+000, + -2.031411e-002, + 1.040653e+000, + -2.333508e+000, + 2.540592e-001, + 8.594981e+000, + 9.316770e-002, + -1.035940e+000, + -2.021151e-001, + 4.719343e+000, + -9.019318e-001, + -7.858046e+000, + 3.901234e+000, + -2.233137e-001, + -4.344739e+000, + 6.550733e-001, + -1.096669e+000, + -1.558196e-001, + 2.057553e+000, + 6.274495e+000, + -2.678352e+000, + -1.814927e+000, + 1.550676e-001, + 1.903276e+000, + 4.998989e-001, + // albedo 1, turbidity 2 + -1.114209e+000, + -1.473531e-001, + -7.602914e+000, + 8.973685e+000, + -4.980074e-002, + 1.289198e+000, + 8.366906e-002, + 4.557987e+000, + 6.118757e-001, + -1.149397e+000, + -1.981628e-001, + 4.914096e+000, + -3.498986e+000, + -6.257090e-002, + 1.667401e-001, + 1.048980e-001, + 2.284689e+000, + 5.935965e-001, + -1.056121e+000, + -1.456172e-001, + 4.272656e-001, + 2.912649e+000, + -5.501745e-001, + 4.406542e+000, + -1.387680e-001, + 1.245555e+000, + 9.733011e-001, + -1.125047e+000, + -4.003662e-002, + 1.058457e+000, + -3.462236e+000, + 4.395278e-001, + -2.395805e+000, + 5.177589e-001, + 4.866247e+000, + 4.253189e-001, + -1.051444e+000, + -2.804541e-001, + 3.364668e+000, + 3.293787e+000, + -1.015741e+001, + 3.807407e+000, + -3.592377e-001, + -3.367415e+000, + 7.900825e-001, + -1.093847e+000, + -1.436965e-001, + 2.384780e+000, + 5.787070e+000, + -2.445987e+000, + -1.311171e+000, + 2.326563e-001, + 1.158439e+000, + 5.555416e-001, + // albedo 1, turbidity 3 + -1.134824e+000, + -1.680468e-001, + -3.325620e+000, + 4.458596e+000, + -1.135063e-001, + 1.104500e+000, + 7.794544e-002, + 4.609952e+000, + 6.854854e-001, + -1.143017e+000, + -1.565926e-001, + 3.014687e-001, + -1.763027e-001, + -3.557925e-002, + -2.342406e-001, + 2.528705e-001, + 5.884085e+000, + 4.750602e-001, + -1.136801e+000, + -2.907502e-001, + 3.682423e+000, + -4.061202e-001, + -8.728159e-001, + 4.001510e+000, + -1.522202e-001, + -5.528713e+000, + 1.044847e+000, + -1.063652e+000, + 7.808107e-002, + -1.983678e+000, + 3.648078e-001, + 2.102276e+000, + -3.065050e+000, + 8.431951e-001, + 1.038830e+001, + 2.662834e-001, + -1.061015e+000, + -2.859814e-001, + 4.223615e+000, + -2.290138e+000, + -8.314010e+000, + 4.405718e+000, + -4.613627e-001, + -4.502910e+000, + 1.008383e+000, + -1.106302e+000, + -1.697123e-001, + 2.087196e+000, + 8.238929e+000, + -2.992416e+000, + -1.821776e+000, + 3.434859e-001, + 7.755179e-001, + 5.341190e-001, + // albedo 1, turbidity 4 + -1.171110e+000, + -2.106304e-001, + -1.614361e+000, + 2.378103e+000, + -1.625969e-001, + 8.504483e-001, + 1.059312e-001, + 4.046256e+000, + 6.618227e-001, + -1.200480e+000, + -2.235733e-001, + 1.014390e+000, + -1.174074e+000, + -4.440180e-001, + 2.262406e-001, + 1.665868e-001, + 5.461829e+000, + 5.676310e-001, + -1.223587e+000, + -3.502622e-001, + 1.699106e+000, + 6.724266e-001, + 1.268567e+000, + 2.135102e+000, + 8.039374e-004, + -5.221111e+000, + 9.445690e-001, + -9.452673e-001, + 1.468459e-001, + -1.335034e+000, + 4.346628e+000, + -1.285652e+001, + -1.807046e+000, + 8.175243e-001, + 9.301065e+000, + 3.656798e-001, + -1.134681e+000, + -3.310951e-001, + 3.571244e+000, + -2.208948e+000, + 6.041580e+000, + 3.107577e+000, + -3.112127e-001, + -4.186351e+000, + 9.188333e-001, + -1.083237e+000, + -1.831394e-001, + 2.062654e+000, + 1.385424e+000, + -5.004950e+000, + -1.332669e+000, + 3.627352e-001, + 3.323150e-001, + 6.191181e-001, + // albedo 1, turbidity 5 + -1.211527e+000, + -2.590617e-001, + -1.660874e-001, + 3.627905e-001, + -1.039258e-001, + 4.697924e-001, + 1.671653e-001, + 3.507497e+000, + 6.022506e-001, + -1.433017e+000, + -4.733592e-001, + 1.724445e-001, + 9.953236e-001, + -1.874457e+000, + 4.432099e-001, + 1.715810e-002, + 2.339272e+000, + 6.441470e-001, + -1.084920e+000, + -1.587903e-001, + 8.999585e-001, + -2.537516e+000, + 5.877859e+000, + 2.014554e+000, + 9.689141e-002, + 3.177242e-001, + 9.030399e-001, + -1.008242e+000, + 2.793030e-003, + -3.507469e-001, + 1.028300e+001, + -2.080454e+001, + -2.781026e+000, + 8.995090e-001, + 3.366951e+000, + 3.473867e-001, + -1.103151e+000, + -2.799598e-001, + 2.525791e+000, + -4.255704e+000, + 9.903388e+000, + 3.722668e+000, + -3.603941e-001, + -1.303292e+000, + 9.369454e-001, + -1.102235e+000, + -2.025061e-001, + 2.085660e+000, + 1.686787e+000, + -5.010957e+000, + -1.656458e+000, + 4.584029e-001, + -2.751759e-001, + 6.184162e-001, + // albedo 1, turbidity 6 + -1.256130e+000, + -3.104904e-001, + 1.639350e-001, + 1.315502e-001, + -7.297583e-001, + 4.778480e-001, + 1.259265e-001, + 3.012108e+000, + 6.202728e-001, + -1.620114e+000, + -6.552670e-001, + -2.877157e-001, + 1.094371e+000, + 2.818914e-001, + 3.696830e-001, + 9.428521e-002, + 1.450951e+000, + 5.681308e-001, + -9.686204e-001, + -3.755647e-002, + 1.469980e+000, + -3.103414e+000, + 2.856583e+000, + 1.883209e+000, + -5.746099e-002, + 1.286383e+000, + 1.001751e+000, + -1.089377e+000, + -1.023062e-001, + -1.498891e+000, + 1.066455e+001, + -1.720184e+001, + -2.759314e+000, + 1.061258e+000, + 2.910211e+000, + 2.624701e-001, + -1.044681e+000, + -2.156857e-001, + 3.230136e+000, + -5.863862e-001, + 6.096640e+000, + 3.550019e+000, + -4.255773e-001, + -1.500033e+000, + 9.687696e-001, + -1.133658e+000, + -2.505101e-001, + 1.717840e+000, + 8.480428e-003, + -5.011789e+000, + -1.740989e+000, + 4.983430e-001, + -2.081829e-001, + 6.088641e-001, + // albedo 1, turbidity 7 + -1.335366e+000, + -3.863319e-001, + -5.279971e-001, + 3.638324e-001, + 3.230699e-001, + 8.339707e-002, + 2.483293e-001, + 2.678646e+000, + 4.998346e-001, + -2.004511e+000, + -9.957121e-001, + 1.250807e+000, + 1.625025e-002, + -3.410754e-001, + 7.858244e-001, + -9.506757e-002, + 2.651876e-002, + 5.788643e-001, + -8.714157e-001, + 1.192051e-001, + -8.486879e-001, + -3.702497e-001, + 1.818277e+000, + 1.103427e+000, + 2.454866e-001, + 3.841575e+000, + 9.847350e-001, + -1.042618e+000, + -2.285793e-001, + 3.620175e-001, + 2.983368e+000, + -9.776844e+000, + -1.971587e+000, + 6.691674e-001, + -7.901947e-001, + 3.213200e-001, + -1.099112e+000, + -1.869868e-001, + 2.044065e+000, + 2.062964e+000, + 1.265668e+000, + 2.710130e+000, + -1.099443e-001, + 2.179353e-001, + 9.024108e-001, + -1.106985e+000, + -2.396881e-001, + 1.809807e+000, + 8.523319e+000, + -5.011788e+000, + -1.590086e+000, + 3.248449e-001, + -1.003187e-001, + 6.550606e-001, + // albedo 1, turbidity 8 + -1.421285e+000, + -4.767024e-001, + -3.885004e-001, + 8.274590e-001, + -3.644229e-001, + 6.999513e-001, + 5.196710e-002, + 2.578431e+000, + 6.246310e-001, + -2.611217e+000, + -1.398846e+000, + 4.527425e-001, + -5.932142e-001, + 2.224617e-001, + -5.593581e-001, + 3.389633e-001, + -7.767112e-001, + 6.536004e-002, + -9.881543e-001, + 4.684782e-002, + -8.616613e-001, + 8.799807e-001, + 4.003130e+000, + 1.739543e+000, + -8.098378e-002, + 5.524802e+000, + 1.499673e+000, + -7.544759e-001, + -2.314808e-001, + 8.125770e-001, + -7.724135e-001, + -9.577645e+000, + -1.629433e+000, + 6.790832e-001, + -4.193895e+000, + -2.526624e-002, + -1.273719e+000, + -2.187030e-001, + 1.401798e+000, + 5.231832e+000, + 7.405093e-001, + 1.775166e+000, + -7.269476e-002, + 1.996087e+000, + 1.057450e+000, + -1.046864e+000, + -2.247559e-001, + 1.679449e+000, + 1.140057e+001, + -4.948829e+000, + -1.182664e+000, + 3.241038e-001, + -2.470012e-001, + 6.115900e-001, + // albedo 1, turbidity 9 + -1.514607e+000, + -5.985430e-001, + -1.877610e-001, + 1.756930e+000, + -1.314206e+000, + 6.115810e-001, + -5.970460e-006, + 2.412975e+000, + 8.124304e-001, + -2.308414e+000, + -1.083797e+000, + -1.179959e-001, + -1.728246e+000, + 7.784742e-001, + 5.494505e-001, + 6.203168e-003, + 9.326251e-001, + -1.419518e-001, + -3.230837e+000, + -1.438670e+000, + -9.868286e-001, + 2.974393e+000, + 1.949339e+000, + -6.337857e-001, + 8.160271e-001, + 3.278606e+000, + 1.354373e+000, + 5.149378e-001, + 2.754789e-001, + 1.040965e+000, + -4.501186e+000, + -3.399057e+000, + 9.661861e-001, + -4.736173e-001, + -4.037574e+000, + 2.794847e-001, + -1.621870e+000, + -3.192763e-001, + 8.786242e-001, + 9.785565e+000, + -2.727652e+000, + 1.903691e-002, + 5.521261e-001, + 2.138764e+000, + 8.419871e-001, + -9.951701e-001, + -2.550607e-001, + 1.498952e+000, + -2.737197e-003, + -3.101832e+000, + -5.921329e-001, + 2.864422e-001, + -4.405218e-001, + 6.631410e-001, + // albedo 1, turbidity 10 + -1.902954e+000, + -9.056918e-001, + -2.069570e-001, + 1.191499e+000, + -1.092577e+000, + 5.849556e-001, + -9.649602e-006, + 2.048407e+000, + 9.001527e-001, + -1.271627e+000, + -7.193923e-001, + -1.136606e-002, + -1.167951e-001, + 3.286175e-003, + -5.262827e-002, + -2.473874e-002, + 1.716125e+000, + -2.187133e-001, + -7.647175e+000, + -3.114129e+000, + -1.490128e+000, + -5.266488e-001, + 3.063090e+000, + 1.474262e+000, + 5.481458e-001, + 2.052174e+000, + 1.353089e+000, + 2.191403e+000, + 3.421120e-001, + 1.446510e+000, + 2.170943e+000, + -7.768187e+000, + -1.471207e+000, + -1.456708e-001, + -1.753574e+000, + 2.310576e-001, + -1.932296e+000, + -3.814739e-001, + 6.245422e-001, + 6.748294e+000, + -3.060171e-001, + 1.067747e+000, + 2.500671e-001, + -1.252596e-001, + 8.614611e-001, + -9.471101e-001, + -4.052640e-001, + 1.300174e+000, + -3.951536e-003, + -1.908284e+000, + -5.385721e-001, + 2.133578e-001, + -6.250292e-001, + 6.658012e-001 + ]; + + public static var datasetRGBRad1 = [ + // albedo 0, turbidity 1 + 1.962684e+000, + 1.159831e+000, + 4.450588e+000, + 5.079633e+000, + 4.437388e+000, + 4.324573e+000, + // albedo 0, turbidity 2 + 1.946487e+000, + 1.287515e+000, + 3.703696e+000, + 8.782833e+000, + 3.440437e+000, + 5.160333e+000, + // albedo 0, turbidity 3 + 1.882170e+000, + 1.335878e+000, + 2.648641e+000, + 1.358368e+001, + 3.105473e+000, + 5.907387e+000, + // albedo 0, turbidity 4 + 1.738159e+000, + 1.624289e+000, + -8.786695e-003, + 2.118253e+001, + 2.770255e+000, + 7.055672e+000, + // albedo 0, turbidity 5 + 1.571896e+000, + 2.301786e+000, + -4.028545e+000, + 2.966806e+001, + 1.630876e+000, + 8.711031e+000, + // albedo 0, turbidity 6 + 1.475048e+000, + 2.679086e+000, + -6.311315e+000, + 3.377896e+001, + 2.140975e+000, + 9.385283e+000, + // albedo 0, turbidity 7 + 1.326174e+000, + 3.378759e+000, + -9.831444e+000, + 3.942061e+001, + 2.852702e+000, + 1.082542e+001, + // albedo 0, turbidity 8 + 1.153344e+000, + 3.967771e+000, + -1.265181e+001, + 4.195016e+001, + 7.468239e+000, + 1.221350e+001, + // albedo 0, turbidity 9 + 9.746081e-001, + 4.051626e+000, + -1.298454e+001, + 3.754964e+001, + 1.749232e+001, + 1.420619e+001, + // albedo 0, turbidity 10 + 8.448016e-001, + 3.181809e+000, + -8.757338e+000, + 2.197962e+001, + 3.524033e+001, + 1.639549e+001, + // albedo 1, turbidity 1 + 2.029623e+000, + 1.364434e+000, + 4.201529e+000, + 5.415099e+000, + 9.825839e+000, + 1.063328e+001, + // albedo 1, turbidity 2 + 2.023126e+000, + 1.494728e+000, + 3.420413e+000, + 9.072178e+000, + 9.205157e+000, + 1.186639e+001, + // albedo 1, turbidity 3 + 1.956307e+000, + 1.648665e+000, + 2.039712e+000, + 1.430239e+001, + 9.039526e+000, + 1.330453e+001, + // albedo 1, turbidity 4 + 1.825053e+000, + 1.985022e+000, + -8.036307e-001, + 2.202493e+001, + 9.415361e+000, + 1.517659e+001, + // albedo 1, turbidity 5 + 1.650367e+000, + 2.593201e+000, + -4.469328e+000, + 2.969817e+001, + 9.410977e+000, + 1.744850e+001, + // albedo 1, turbidity 6 + 1.555202e+000, + 2.962925e+000, + -6.608170e+000, + 3.329887e+001, + 1.064559e+001, + 1.850816e+001, + // albedo 1, turbidity 7 + 1.412478e+000, + 3.439403e+000, + -9.196616e+000, + 3.685077e+001, + 1.345341e+001, + 2.003128e+001, + // albedo 1, turbidity 8 + 1.252990e+000, + 3.820805e+000, + -1.115338e+001, + 3.721593e+001, + 2.014916e+001, + 2.182320e+001, + // albedo 1, turbidity 9 + 1.091952e+000, + 3.663027e+000, + -1.031330e+001, + 2.978985e+001, + 3.296835e+001, + 2.375450e+001, + // albedo 1, turbidity 10 + 9.501691e-001, + 2.664579e+000, + -5.545167e+000, + 1.281159e+001, + 5.154768e+001, + 2.574284e+001 + ]; + + + public static var datasetRGB2 = [ + // albedo 0, turbidity 1 + -1.140530e+000, + -1.982747e-001, + -7.512730e+000, + 8.403899e+000, + -5.699038e-002, + 9.015907e-001, + 3.392161e-002, + 4.772522e+000, + 5.111184e-001, + -1.165117e+000, + -1.852955e-001, + 2.963684e+000, + -2.262274e+000, + -1.571683e-001, + 6.339974e-001, + 4.977879e-002, + 7.243307e+000, + 4.220053e-001, + -1.169936e+000, + -3.357429e-001, + 1.911291e+000, + -2.391074e-001, + -4.791643e-001, + 1.446113e+000, + -9.178108e-002, + -4.700239e+000, + 8.096219e-001, + -1.060246e+000, + -1.051633e-001, + 5.013829e-001, + 2.832309e+000, + -3.707855e-001, + 1.523131e+000, + 9.163749e-002, + 5.604183e+000, + 7.208566e-001, + -1.089753e+000, + -2.382167e-001, + 2.360312e+000, + -5.902562e+000, + -8.799894e+000, + 1.377692e+000, + -6.131633e-002, + -1.415472e+000, + 6.124057e-001, + -1.075481e+000, + -1.242391e-001, + 1.425781e+000, + 8.810319e+000, + -2.922646e+000, + 1.486520e+000, + 3.270580e-002, + 3.889783e+000, + 4.999482e-001, + // albedo 0, turbidity 2 + -1.149342e+000, + -2.076337e-001, + -7.446587e+000, + 8.014559e+000, + -4.866227e-002, + 8.203043e-001, + 6.386483e-002, + 4.894198e+000, + 5.452051e-001, + -1.120531e+000, + -1.513311e-001, + 2.735504e+000, + -2.417591e+000, + -1.361114e-001, + 4.296342e-001, + 9.427488e-002, + 8.171403e+000, + 4.102448e-001, + -1.226964e+000, + -3.516378e-001, + 1.308298e+000, + -5.097487e-002, + -4.846783e-001, + 1.654619e+000, + -1.134940e-001, + -3.347854e+000, + 1.131147e+000, + -9.664377e-001, + 2.767589e-002, + 1.658235e-001, + 2.407439e+000, + -1.300304e-001, + 9.170958e-001, + 2.742895e-001, + 6.642633e+000, + 2.550064e-001, + -1.153358e+000, + -3.126223e-001, + 2.078934e+000, + -5.857733e+000, + -8.659848e+000, + 1.758505e+000, + -9.616094e-002, + -1.230863e+000, + 9.663832e-001, + -1.053850e+000, + -1.330743e-001, + 1.481738e+000, + 1.049485e+001, + -3.528854e+000, + 9.142363e-001, + 1.244880e-001, + 2.644615e+000, + 5.001048e-001, + // albedo 0, turbidity 3 + -1.173687e+000, + -2.360362e-001, + -3.741454e+000, + 4.088507e+000, + -7.528205e-002, + 6.645237e-001, + 7.718265e-002, + 4.651220e+000, + 5.586318e-001, + -1.213757e+000, + -2.589561e-001, + 7.132551e-001, + -4.259327e-001, + -1.980821e-001, + 3.627815e-001, + 4.666560e-002, + 5.807984e+000, + 5.847377e-001, + -1.108794e+000, + -2.259870e-001, + 1.574179e+000, + -3.753731e-001, + -5.984743e-001, + 1.659414e+000, + -1.681021e-002, + 6.785219e-001, + 8.647325e-001, + -1.060896e+000, + -1.346690e-002, + -7.529656e-001, + 1.711319e+000, + -9.792435e-001, + 2.022433e-001, + 3.826487e-001, + 5.725157e+000, + 5.290714e-001, + -1.085145e+000, + -2.840715e-001, + 2.088029e+000, + -4.935097e+000, + -9.056542e+000, + 1.976149e+000, + -3.912485e-002, + -8.636064e-001, + 7.452125e-001, + -1.077983e+000, + -1.416633e-001, + 1.100848e+000, + 1.015875e+001, + -2.943712e+000, + 5.255135e-001, + 2.164224e-001, + 2.941143e+000, + 6.699937e-001, + // albedo 0, turbidity 4 + -1.223293e+000, + -2.867444e-001, + -1.624136e+000, + 1.668299e+000, + -9.537589e-002, + 5.015947e-001, + 1.130741e-001, + 4.244812e+000, + 5.082152e-001, + -1.325342e+000, + -4.280991e-001, + 4.705490e-001, + 6.926592e-002, + -4.572587e-001, + 5.344144e-001, + -2.554192e-002, + 3.093939e+000, + 6.639401e-001, + -1.113581e+000, + -1.192133e-001, + 4.011536e-001, + 7.011889e-001, + 2.052842e-001, + 9.880724e-001, + 1.807533e-002, + 4.690160e+000, + 8.576240e-001, + -1.016063e+000, + -1.038138e-001, + -2.280391e-001, + 7.898918e-001, + -1.127333e+001, + 2.074545e-001, + 5.388182e-001, + 1.364263e+000, + 4.660455e-001, + -1.099582e+000, + -2.228607e-001, + 1.332648e+000, + 5.135188e+000, + 1.653152e+000, + 1.417020e+000, + -1.087532e-001, + 1.809275e+000, + 8.080874e-001, + -1.064357e+000, + -1.520775e-001, + 8.207368e-001, + -1.323565e-003, + -5.009523e+000, + 3.946298e-001, + 4.337902e-001, + 2.593198e+000, + 6.719172e-001, + // albedo 0, turbidity 5 + -1.278702e+000, + -3.512866e-001, + -4.511055e-001, + 3.895760e-001, + -2.429672e-001, + 4.270577e-001, + 1.135348e-001, + 3.719130e+000, + 4.998867e-001, + -1.580069e+000, + -7.095475e-001, + -3.198904e-001, + 1.715748e+000, + -1.185915e+000, + 4.523161e-001, + -1.026159e-002, + 7.927188e-001, + 5.538350e-001, + -9.474023e-001, + 1.173703e-001, + 4.881381e-001, + -2.618684e+000, + 3.251661e+000, + 1.213931e+000, + -1.736274e-002, + 8.000768e+000, + 1.025998e+000, + -1.129091e+000, + -3.287694e-001, + -3.524077e-001, + 3.352892e+000, + -1.416073e+001, + -8.485617e-001, + 6.560766e-001, + -2.820937e+000, + 3.111303e-001, + -1.030884e+000, + -1.137581e-001, + 1.109855e+000, + 8.082276e+000, + 1.519214e+000, + 2.112433e+000, + -1.592299e-001, + 3.675905e+000, + 8.703367e-001, + -1.075192e+000, + -1.627166e-001, + 3.514910e-001, + 1.168164e+000, + -4.255822e+000, + -6.015348e-001, + 6.265776e-001, + 2.884818e+000, + 6.548384e-001, + // albedo 0, turbidity 6 + -1.316017e+000, + -3.889652e-001, + -5.030854e-001, + 4.488704e-001, + -3.186800e-001, + 4.570763e-001, + 8.909201e-002, + 3.659274e+000, + 5.011746e-001, + -1.731876e+000, + -8.493806e-001, + 1.194871e-001, + 2.002781e+000, + -2.006547e+000, + 4.872233e-001, + -2.854606e-002, + 2.662137e-001, + 4.611629e-001, + -9.273680e-001, + 1.380954e-001, + -3.302179e-001, + -3.553265e+000, + 4.633345e+000, + 9.696729e-001, + 8.799775e-002, + 8.291129e+000, + 1.094451e+000, + -1.099377e+000, + -3.325392e-001, + 2.501063e-001, + 2.613712e+000, + -1.328142e+001, + -5.579527e-001, + 4.992081e-001, + -3.504402e+000, + 3.022924e-001, + -1.048420e+000, + -1.227773e-001, + 5.845373e-001, + 1.105869e+001, + 3.813151e-002, + 1.330409e+000, + 1.978131e-002, + 3.959430e+000, + 8.396439e-001, + -1.063233e+000, + -1.560639e-001, + 2.840033e-001, + 8.751565e-001, + -3.411820e+000, + -1.436564e-001, + 5.846580e-001, + 2.899292e+000, + 6.799095e-001, + // albedo 0, turbidity 7 + -1.376715e+000, + -4.541567e-001, + -1.445491e+000, + 1.569898e+000, + -1.390627e-001, + 5.558270e-001, + 4.109877e-002, + 3.349451e+000, + 5.516123e-001, + -1.953391e+000, + -1.035869e+000, + 1.690563e+000, + -1.964690e-001, + -7.787096e-001, + 5.799605e-001, + 2.945626e-002, + 4.217906e-002, + 2.451373e-001, + -1.012422e+000, + 7.136451e-002, + -1.862534e+000, + -7.228653e-001, + 1.947997e-001, + 2.091805e-001, + 6.399233e-002, + 7.928994e+000, + 1.290733e+000, + -9.706708e-001, + -2.880950e-001, + 1.107797e+000, + -2.731734e+000, + -8.445995e+000, + 4.296774e-001, + 5.117648e-001, + -3.824277e+000, + 1.761207e-001, + -1.110611e+000, + -1.789409e-001, + 2.108488e-001, + 2.071430e+001, + -1.763174e+000, + 9.554695e-002, + -2.943103e-002, + 3.422079e+000, + 8.815496e-001, + -1.048334e+000, + -1.614087e-001, + 2.475184e-001, + 2.146938e-002, + -2.983901e+000, + 2.538224e-001, + 5.601370e-001, + 2.461925e+000, + 6.777394e-001, + // albedo 0, turbidity 8 + -1.393719e+000, + -5.002724e-001, + -2.408940e+000, + 2.680983e+000, + -1.362825e-001, + 7.395067e-001, + -3.300343e-006, + 3.260889e+000, + 8.132057e-001, + -2.128663e+000, + -1.151182e+000, + 2.923026e+000, + -1.931838e+000, + -4.426170e-001, + 2.309983e-001, + -5.485890e-003, + 3.279529e-001, + -2.229467e-001, + -1.618022e+000, + -3.766490e-001, + -3.163544e+000, + 1.611608e+000, + -3.967476e-001, + 3.933680e-001, + 3.006742e-001, + 6.835177e+000, + 1.613765e+000, + -5.669064e-001, + -1.481749e-001, + 2.071817e+000, + -8.157422e+000, + -5.988088e+000, + 2.387202e-001, + 1.447191e-001, + -4.296385e+000, + 5.011258e-002, + -1.241724e+000, + -2.519348e-001, + -1.908609e-001, + 2.952235e+001, + -3.333660e+000, + -1.837651e-002, + 1.022249e-001, + 2.929320e+000, + 8.867262e-001, + -1.021670e+000, + -1.667327e-001, + 1.789771e-001, + -2.178108e-003, + -2.641572e+000, + -5.641484e-002, + 5.303758e-001, + 2.138196e+000, + 6.780350e-001, + // albedo 0, turbidity 9 + -1.669332e+000, + -7.588708e-001, + -2.993557e+000, + 3.178760e+000, + -8.066442e-002, + 6.544672e-001, + -8.089880e-006, + 2.628924e+000, + 9.001272e-001, + -1.755806e+000, + -8.735348e-001, + 3.258881e+000, + -2.504785e+000, + -3.300791e-001, + 1.180565e-001, + -9.315982e-003, + 1.785154e+000, + -3.205824e-001, + -3.720277e+000, + -1.733350e+000, + -3.332272e+000, + 1.515869e+000, + 1.734218e-001, + 8.011956e-001, + 1.995440e-001, + 3.817666e+000, + 1.638502e+000, + 4.724641e-001, + 3.209828e-001, + 2.051443e+000, + -5.105574e+000, + -6.509139e+000, + -4.232041e-001, + 2.598931e-001, + -2.151756e+000, + -3.493910e-003, + -1.525600e+000, + -4.897606e-001, + -9.891121e-002, + 2.346818e+001, + -2.278152e+000, + 1.681219e-001, + -4.469389e-002, + 1.051000e+000, + 9.294666e-001, + -9.908649e-001, + -2.008182e-001, + 1.605143e-001, + -2.463113e-003, + -2.477349e+000, + -1.218647e-001, + 4.750121e-001, + 1.460813e+000, + 6.661364e-001, + // albedo 0, turbidity 10 + -2.122119e+000, + -1.125475e+000, + -3.066599e+000, + 3.145078e+000, + -5.411593e-002, + 5.133628e-001, + -7.823408e-006, + 2.268448e+000, + 9.001416e-001, + -1.528158e+000, + -9.370249e-001, + 2.567559e+000, + -1.591439e+000, + -3.634460e-001, + 1.763256e-001, + 1.119624e-003, + 1.811848e+000, + -2.637929e-001, + -6.524387e+000, + -2.673507e+000, + -2.940472e+000, + -6.025609e-001, + 7.852067e-001, + 1.073499e+000, + -3.540435e-002, + 3.517416e+000, + 1.490466e+000, + 8.886026e-001, + -9.681828e-002, + 1.430554e+000, + 4.993717e+000, + -6.071355e+000, + -6.053986e-001, + 5.092997e-001, + -1.273010e+000, + 7.491329e-002, + -1.481997e+000, + -5.897282e-001, + 2.659264e-001, + 1.267239e+000, + -5.741291e-001, + 5.983011e-002, + -2.217312e-001, + -3.016452e-001, + 9.260830e-001, + -1.010943e+000, + -2.075134e-001, + 5.066749e-002, + 1.470708e+001, + -3.780501e+000, + 7.253223e-002, + 4.045458e-001, + 1.320164e+000, + 6.559925e-001, + // albedo 1, turbidity 1 + -1.129907e+000, + -1.884011e-001, + -8.047670e+000, + 9.035776e+000, + -5.539419e-002, + 8.823349e-001, + 3.197135e-002, + 4.839388e+000, + 5.042822e-001, + -1.133821e+000, + -1.510781e-001, + 3.362822e+000, + -2.453381e+000, + -1.463925e-001, + 4.728708e-001, + 5.958140e-002, + 7.636300e+000, + 4.805162e-001, + -1.176518e+000, + -3.549902e-001, + 1.729044e+000, + -2.160966e-001, + -5.075865e-001, + 1.675584e+000, + -8.906902e-002, + -5.386842e+000, + 5.452218e-001, + -1.043563e+000, + -7.520975e-002, + 8.750644e-001, + 2.510518e+000, + 7.584882e-003, + 9.361250e-001, + 7.889083e-002, + 6.066644e+000, + 5.813108e-001, + -1.081304e+000, + -2.222253e-001, + 2.517638e+000, + -4.453820e+000, + -8.663691e+000, + 8.662558e-001, + -4.802657e-002, + -8.965449e-001, + 4.886656e-001, + -1.083774e+000, + -1.375469e-001, + 1.685818e+000, + 5.631120e+000, + -3.100752e+000, + 4.045941e-001, + 2.346895e-002, + 3.390321e+000, + 5.008309e-001, + // albedo 1, turbidity 2 + -1.143158e+000, + -2.058334e-001, + -9.660198e+000, + 1.062394e+001, + -4.434119e-002, + 8.607615e-001, + 3.177325e-002, + 4.416481e+000, + 5.918162e-001, + -1.146773e+000, + -1.727385e-001, + 4.626048e+000, + -4.684602e+000, + -8.307137e-002, + 1.619616e-001, + 1.484866e-001, + 7.572868e+000, + 2.681126e-001, + -1.151324e+000, + -3.099303e-001, + 4.125596e-001, + 2.340752e+000, + -4.214444e-001, + 1.987375e+000, + -1.913410e-001, + -3.845978e+000, + 1.337311e+000, + -1.034258e+000, + -7.778759e-003, + 7.050094e-001, + -8.036369e-001, + 3.138570e-001, + 2.469452e-001, + 3.559970e-001, + 7.485917e+000, + 4.790329e-002, + -1.096568e+000, + -2.673169e-001, + 2.575654e+000, + -8.057121e-001, + -8.884928e+000, + 1.416170e+000, + -2.091315e-001, + -1.543494e+000, + 1.065445e+000, + -1.083304e+000, + -1.528265e-001, + 1.697727e+000, + 2.503702e+000, + -2.885296e+000, + -1.298500e-001, + 1.548870e-001, + 2.479652e+000, + 5.066496e-001, + // albedo 1, turbidity 3 + -1.165736e+000, + -2.329945e-001, + -5.967964e+000, + 6.705959e+000, + -5.931355e-002, + 7.485638e-001, + 3.913878e-002, + 4.221591e+000, + 6.183926e-001, + -1.212422e+000, + -2.545910e-001, + 2.418626e+000, + -2.266104e+000, + -1.102014e-001, + 1.363887e-002, + 1.055411e-001, + 5.648062e+000, + 4.557412e-001, + -1.070436e+000, + -2.163341e-001, + 7.098718e-001, + 7.843075e-001, + -4.323930e-001, + 2.109823e+000, + -9.589700e-002, + -1.985193e-001, + 1.060428e+000, + -1.104879e+000, + -3.013622e-002, + 2.976276e-002, + 1.069707e+000, + 1.410000e-001, + -4.880020e-001, + 4.452288e-001, + 6.418590e+000, + 3.195986e-001, + -1.048969e+000, + -2.655317e-001, + 2.689426e+000, + -3.941038e+000, + -9.506461e+000, + 1.837119e+000, + -1.892124e-001, + -1.562146e+000, + 9.043414e-001, + -1.106145e+000, + -1.601642e-001, + 1.544544e+000, + 7.388492e+000, + -2.924600e+000, + -4.328453e-001, + 1.763161e-001, + 2.523111e+000, + 5.851902e-001, + // albedo 1, turbidity 4 + -1.203666e+000, + -2.776587e-001, + -2.084286e+000, + 2.450840e+000, + -8.746613e-002, + 5.258507e-001, + 7.983316e-002, + 3.860055e+000, + 5.486167e-001, + -1.340448e+000, + -4.230590e-001, + 3.462849e-001, + 4.707607e-001, + -2.512626e-001, + 1.530746e-001, + 2.724218e-002, + 3.035216e+000, + 5.876133e-001, + -1.014554e+000, + -1.168790e-001, + 9.477794e-001, + -1.061218e+000, + -4.196730e-001, + 2.058832e+000, + -5.989624e-002, + 3.058168e+000, + 9.763861e-001, + -1.137388e+000, + -9.854030e-002, + -2.984893e-001, + 3.647820e+000, + -6.585571e-001, + -1.479180e+000, + 6.102932e-001, + 3.265914e+000, + 3.480333e-001, + -1.021816e+000, + -2.344957e-001, + 2.463671e+000, + -7.240685e+000, + -8.862697e+000, + 2.514058e+000, + -2.122768e-001, + -3.313968e-002, + 9.028136e-001, + -1.126581e+000, + -1.874347e-001, + 1.454154e+000, + 1.034398e+001, + -3.237393e+000, + -8.654927e-001, + 2.457248e-001, + 1.845769e+000, + 6.002482e-001, + // albedo 1, turbidity 5 + -1.263727e+000, + -3.439354e-001, + -1.786388e-001, + 3.980166e-001, + -3.349517e-001, + 3.825166e-001, + 1.029225e-001, + 3.331096e+000, + 4.998955e-001, + -1.530010e+000, + -6.879698e-001, + 2.380415e-001, + 1.608216e+000, + -1.682679e+000, + 3.546360e-001, + -3.915220e-003, + 4.517655e-001, + 5.128605e-001, + -9.685659e-001, + 9.480403e-002, + 6.076844e-002, + -3.217561e+000, + 4.568074e+000, + 1.069299e+000, + 2.083638e-002, + 7.301088e+000, + 1.072165e+000, + -1.113925e+000, + -3.112382e-001, + 3.954133e-001, + 5.105907e+000, + -1.456866e+001, + -4.917378e-001, + 5.289909e-001, + -2.678374e+000, + 3.014709e-001, + -1.046864e+000, + -1.215754e-001, + 1.778308e+000, + 4.661489e+000, + 2.565583e-001, + 1.353680e+000, + -1.175767e-001, + 3.415972e+000, + 8.457746e-001, + -1.104480e+000, + -1.940913e-001, + 1.343668e+000, + -1.759206e-003, + -5.009204e+000, + -4.186951e-001, + 3.125710e-001, + 1.628183e+000, + 6.720408e-001, + // albedo 1, turbidity 6 + -1.286902e+000, + -3.781238e-001, + -8.977253e-002, + 3.545393e-001, + -4.866515e-001, + 3.843664e-001, + 8.281675e-002, + 3.122231e+000, + 5.046991e-001, + -1.712597e+000, + -8.549112e-001, + 4.809286e-001, + 1.515398e+000, + -2.212211e+000, + 2.539029e-001, + 2.335997e-002, + -6.089466e-002, + 4.268444e-001, + -8.807283e-001, + 1.646097e-001, + -4.437898e-001, + -3.188247e+000, + 5.984417e+000, + 1.334779e+000, + -4.026975e-002, + 7.546431e+000, + 1.175751e+000, + -1.147253e+000, + -3.538199e-001, + 6.101836e-001, + 4.437780e+000, + -1.559813e+001, + -1.103222e+000, + 6.242039e-001, + -3.091472e+000, + 2.174290e-001, + -1.038230e+000, + -1.213475e-001, + 1.547505e+000, + 5.893176e+000, + 1.368738e+000, + 1.663127e+000, + -1.377130e-001, + 3.185279e+000, + 8.736453e-001, + -1.101026e+000, + -1.874907e-001, + 1.272667e+000, + 3.596524e+000, + -5.007243e+000, + -6.352483e-001, + 3.048985e-001, + 1.931613e+000, + 6.788844e-001, + // albedo 1, turbidity 7 + -1.342753e+000, + -4.384971e-001, + -1.213491e+000, + 1.621399e+000, + -1.551441e-001, + 5.614218e-001, + 2.591739e-002, + 2.958967e+000, + 5.782132e-001, + -1.937684e+000, + -1.066019e+000, + 1.913336e+000, + -7.347719e-001, + -5.916167e-001, + 1.587590e-001, + 1.092568e-001, + -6.275002e-001, + 1.599071e-001, + -9.302391e-001, + 1.486187e-001, + -1.603835e+000, + 1.783713e-001, + 1.100461e+000, + 1.174181e+000, + -1.602361e-001, + 7.868331e+000, + 1.468971e+000, + -1.053631e+000, + -3.727050e-001, + 1.114117e+000, + -9.603286e-001, + -1.062469e+001, + -1.162140e+000, + 7.952797e-001, + -4.478765e+000, + -4.440862e-002, + -1.083629e+000, + -1.261405e-001, + 1.229344e+000, + 1.127825e+001, + 1.319010e-001, + 1.624729e+000, + -2.825898e-001, + 3.661082e+000, + 1.036911e+000, + -1.093950e+000, + -2.067455e-001, + 1.258035e+000, + 7.548645e+000, + -4.598387e+000, + -8.944932e-001, + 3.292634e-001, + 1.311304e+000, + 6.291871e-001, + // albedo 1, turbidity 8 + -1.385867e+000, + -5.068139e-001, + -1.486490e+000, + 1.969049e+000, + -1.698025e-001, + 6.629167e-001, + -5.289365e-006, + 2.760315e+000, + 8.644368e-001, + -2.107367e+000, + -1.175639e+000, + 2.313241e+000, + -1.001653e+000, + -4.843139e-001, + 1.124485e-001, + 3.901494e-005, + -3.502469e-001, + -3.204780e-001, + -1.475244e+000, + -2.833055e-001, + -2.085824e+000, + 1.192563e+000, + -7.645200e-001, + 8.380081e-001, + 2.203580e-001, + 7.157885e+000, + 1.753702e+000, + -6.644372e-001, + -2.549735e-001, + 1.600273e+000, + -8.589034e+000, + -6.144718e+000, + -7.599731e-001, + 2.898370e-001, + -5.770923e+000, + -9.656242e-002, + -1.211687e+000, + -1.653494e-001, + 8.393400e-001, + 2.792988e+001, + -3.395461e+000, + 9.933752e-001, + -3.976877e-002, + 3.776659e+000, + 9.546526e-001, + -1.063757e+000, + -2.037563e-001, + 1.117207e+000, + -1.252806e-003, + -3.332330e+000, + -6.971409e-001, + 3.388719e-001, + 1.311398e+000, + 6.635171e-001, + // albedo 1, turbidity 9 + -1.678889e+000, + -7.992295e-001, + -2.421687e+000, + 2.871029e+000, + -7.662842e-002, + 6.046208e-001, + -7.598099e-006, + 2.002314e+000, + 9.001307e-001, + -1.692144e+000, + -8.804250e-001, + 3.060895e+000, + -2.000009e+000, + -3.183563e-001, + 8.385862e-002, + -6.326713e-003, + 1.206639e+000, + -3.369967e-001, + -3.676795e+000, + -1.719207e+000, + -2.534697e+000, + 1.005285e+000, + 1.550407e-001, + 1.072910e+000, + 1.318094e-001, + 3.717018e+000, + 1.689191e+000, + 5.424542e-001, + 3.263528e-001, + 1.551055e+000, + -3.841058e+000, + -6.598996e+000, + -1.201779e+000, + 3.530669e-001, + -2.542945e+000, + -6.482523e-002, + -1.553849e+000, + -4.576860e-001, + 9.324676e-001, + 1.950982e+001, + -2.344516e+000, + 1.121020e+000, + -1.221537e-001, + 7.285496e-001, + 9.582816e-001, + -1.020650e+000, + -2.215797e-001, + 1.009774e+000, + -2.056855e-003, + -2.740338e+000, + -8.122355e-001, + 3.328967e-001, + 8.982766e-001, + 6.594676e-001, + // albedo 1, turbidity 10 + -2.247360e+000, + -1.221267e+000, + -3.072346e+000, + 3.385139e+000, + -4.387559e-002, + 5.084887e-001, + -7.418833e-006, + 1.750107e+000, + 9.001401e-001, + -1.248499e+000, + -8.442718e-001, + 3.062611e+000, + -2.020314e+000, + -2.815341e-001, + 5.254745e-002, + 3.345008e-003, + 1.433225e+000, + -2.835911e-001, + -7.004119e+000, + -2.927978e+000, + -2.649852e+000, + 7.971894e-001, + 5.466893e-001, + 1.442667e+000, + -6.063912e-002, + 2.806194e+000, + 1.547429e+000, + 1.434882e+000, + 9.114639e-002, + 1.170089e+000, + 3.512808e-002, + -5.861915e+000, + -1.411843e+000, + 5.400486e-001, + -7.746522e-001, + 2.386984e-002, + -1.559053e+000, + -5.502302e-001, + 1.200396e+000, + 1.347741e+001, + -2.344397e+000, + 8.868907e-001, + -3.292661e-001, + -1.362105e+000, + 9.217826e-001, + -1.044436e+000, + -2.360719e-001, + 7.054471e-001, + -2.904518e-003, + -2.092829e+000, + -5.119668e-001, + 4.174861e-001, + 9.687435e-001, + 6.588427e-001 + ]; + + public static var datasetRGBRad2 = [ + // albedo 0, turbidity 1 + 1.590330e+000, + 1.355401e+000, + 1.151412e+000, + 1.359116e+001, + 5.857714e+000, + 8.090833e+000, + // albedo 0, turbidity 2 + 1.552540e+000, + 1.510040e+000, + 1.276413e-001, + 1.604643e+001, + 5.912162e+000, + 8.350009e+000, + // albedo 0, turbidity 3 + 1.470871e+000, + 1.880464e+000, + -1.865398e+000, + 2.030808e+001, + 5.471461e+000, + 9.109834e+000, + // albedo 0, turbidity 4 + 1.356563e+000, + 2.373866e+000, + -4.653245e+000, + 2.570922e+001, + 5.686009e+000, + 1.009480e+001, + // albedo 0, turbidity 5 + 1.244232e+000, + 2.851519e+000, + -7.130942e+000, + 2.993449e+001, + 6.382120e+000, + 1.114578e+001, + // albedo 0, turbidity 6 + 1.173693e+000, + 3.120604e+000, + -8.491886e+000, + 3.187393e+001, + 7.290615e+000, + 1.180066e+001, + // albedo 0, turbidity 7 + 1.091845e+000, + 3.368888e+000, + -9.722083e+000, + 3.268508e+001, + 1.032424e+001, + 1.236508e+001, + // albedo 0, turbidity 8 + 9.858985e-001, + 3.500541e+000, + -1.026328e+001, + 3.092956e+001, + 1.610881e+001, + 1.331222e+001, + // albedo 0, turbidity 9 + 8.864993e-001, + 3.172888e+000, + -8.687550e+000, + 2.362161e+001, + 2.621851e+001, + 1.474967e+001, + // albedo 0, turbidity 10 + 7.946973e-001, + 2.189355e+000, + -4.207953e+000, + 9.399091e+000, + 4.062849e+001, + 1.681753e+001, + // albedo 1, turbidity 1 + 1.711696e+000, + 1.657311e+000, + 9.328021e-001, + 1.317880e+001, + 1.506751e+001, + 1.863556e+001, + // albedo 1, turbidity 2 + 1.666968e+000, + 1.849993e+000, + -2.088601e-001, + 1.586653e+001, + 1.486880e+001, + 1.940719e+001, + // albedo 1, turbidity 3 + 1.584846e+000, + 2.170022e+000, + -2.019597e+000, + 1.970826e+001, + 1.490684e+001, + 2.045055e+001, + // albedo 1, turbidity 4 + 1.469412e+000, + 2.524017e+000, + -4.197267e+000, + 2.365249e+001, + 1.664588e+001, + 2.134477e+001, + // albedo 1, turbidity 5 + 1.369714e+000, + 2.843548e+000, + -6.059031e+000, + 2.634993e+001, + 1.881361e+001, + 2.232186e+001, + // albedo 1, turbidity 6 + 1.310477e+000, + 2.984444e+000, + -6.831686e+000, + 2.682340e+001, + 2.123267e+001, + 2.259755e+001, + // albedo 1, turbidity 7 + 1.222552e+000, + 3.176523e+000, + -7.731496e+000, + 2.671760e+001, + 2.484358e+001, + 2.336863e+001, + // albedo 1, turbidity 8 + 1.115781e+000, + 3.130635e+000, + -7.581744e+000, + 2.336531e+001, + 3.171048e+001, + 2.413859e+001, + // albedo 1, turbidity 9 + 1.013181e+000, + 2.699342e+000, + -5.602709e+000, + 1.500158e+001, + 4.217613e+001, + 2.515957e+001, + // albedo 1, turbidity 10 + 8.976323e-001, + 1.726948e+000, + -1.296120e+000, + 1.183675e+000, + 5.503215e+001, + 2.643066e+001 + ]; + + public static var datasetRGB3 = [ + // albedo 0, turbidity 1 + -1.372629e+000, + -4.905585e-001, + -4.100789e+001, + 4.122169e+001, + -7.389360e-003, + 4.839359e-001, + 6.474757e-003, + 3.471755e+000, + 5.092936e-001, + -1.523025e+000, + -6.497084e-001, + 6.249857e+000, + -5.662543e+000, + -1.908402e-002, + 5.512810e-001, + -2.181049e-005, + 2.507663e+000, + 4.339598e-001, + -1.035567e+000, + -7.478740e-002, + 9.221030e-001, + -2.140047e+000, + -2.374146e-002, + 3.795517e-001, + -1.769134e-002, + 7.479831e+000, + 7.729303e-001, + -1.271086e+000, + -5.588190e-001, + 6.908023e-001, + 2.096832e+000, + -2.453967e-001, + 1.410648e+000, + 4.475036e-002, + -4.719115e+000, + 5.741186e-001, + -9.712598e-001, + -7.033926e-002, + 9.167274e-001, + -9.502097e-001, + 3.004684e-001, + 4.547054e-001, + -5.929017e-002, + 5.266196e+000, + 7.204135e-001, + -1.087457e+000, + -1.888896e-001, + 8.156686e-001, + 3.101712e-001, + -2.155419e+000, + 1.422205e+000, + 9.692261e-002, + 3.122404e+000, + 4.999430e-001, + // albedo 0, turbidity 2 + -1.425280e+000, + -5.413508e-001, + -3.454883e+001, + 3.481142e+001, + -8.686975e-003, + 4.914268e-001, + -2.479243e-006, + 3.239879e+000, + 6.094201e-001, + -1.688557e+000, + -8.070865e-001, + 7.018459e+000, + -6.244574e+000, + -2.149341e-002, + 3.993971e-001, + 1.252502e-002, + 1.630662e+000, + 1.097860e-001, + -8.664152e-001, + 7.869125e-002, + -5.236535e-001, + -1.218960e+000, + -2.059093e-002, + 6.684898e-001, + -5.584112e-002, + 8.602299e+000, + 1.410496e+000, + -1.319763e+000, + -5.985323e-001, + 1.253918e+000, + 1.914706e+000, + -3.216739e-001, + 9.011213e-001, + 1.324845e-001, + -5.252749e+000, + 6.231252e-002, + -9.706008e-001, + -5.914059e-002, + 5.693150e-001, + -1.175362e+000, + 5.221644e-001, + 7.518213e-001, + -8.247655e-002, + 5.875635e+000, + 9.850863e-001, + -1.085330e+000, + -1.956105e-001, + 8.019605e-001, + 5.338101e-001, + -3.423464e+000, + 1.110444e+000, + 1.507923e-001, + 2.864942e+000, + 4.999481e-001, + // albedo 0, turbidity 3 + -1.431967e+000, + -5.478935e-001, + -3.286288e+001, + 3.305288e+001, + -8.380797e-003, + 4.772050e-001, + -3.044274e-006, + 3.289973e+000, + 5.976303e-001, + -1.801361e+000, + -9.315889e-001, + 5.391756e+000, + -4.588592e+000, + -2.040076e-002, + 4.144684e-001, + 1.814534e-002, + 1.051795e+000, + 1.145651e-001, + -7.905357e-001, + 1.451332e-001, + -1.605661e-001, + -1.592174e+000, + 4.561348e-004, + 3.380323e-001, + -7.770275e-002, + 8.775384e+000, + 1.489512e+000, + -1.308575e+000, + -5.539232e-001, + 9.184133e-001, + 2.011479e+000, + -3.842472e-001, + 1.432274e+000, + 1.637153e-001, + -4.408856e+000, + 5.272957e-002, + -9.829872e-001, + -8.183048e-002, + 4.464556e-001, + -1.442716e+000, + 1.029641e+000, + -6.991617e-002, + 8.702356e-003, + 5.706417e+000, + 9.116452e-001, + -1.087130e+000, + -2.038013e-001, + 7.260801e-001, + 9.164376e-001, + -5.006183e+000, + 1.511271e+000, + 1.257134e-001, + 2.715439e+000, + 6.201652e-001, + // albedo 0, turbidity 4 + -1.448662e+000, + -5.799075e-001, + -2.833268e+001, + 2.858023e+001, + -9.134061e-003, + 4.404783e-001, + -2.709026e-006, + 3.029357e+000, + 5.540071e-001, + -2.061772e+000, + -1.145190e+000, + 7.918478e+000, + -7.212525e+000, + -2.020760e-002, + 2.962715e-001, + 4.689670e-002, + 8.517209e-001, + 2.334587e-001, + -6.413755e-001, + 1.780425e-001, + -2.412919e+000, + 1.064484e+000, + -1.949986e-002, + 6.769741e-001, + -1.752760e-001, + 7.262714e+000, + 1.325869e+000, + -1.304871e+000, + -3.975581e-001, + 1.219002e+000, + 7.285178e-001, + -2.710105e-001, + 7.779727e-001, + 3.247139e-001, + -8.818168e-001, + 1.839517e-001, + -1.001104e+000, + -1.994801e-001, + 3.676742e-001, + -1.409737e+000, + 2.901555e-001, + 2.506940e-001, + 2.468899e-003, + 3.398923e+000, + 8.584645e-001, + -1.111552e+000, + -2.487204e-001, + 7.410842e-001, + 1.703749e+000, + -5.007855e+000, + 1.057763e+000, + 1.354511e-001, + 2.088715e+000, + 6.600013e-001, + // albedo 0, turbidity 5 + -1.547227e+000, + -6.679466e-001, + -1.861465e+001, + 1.884045e+001, + -1.242210e-002, + 4.157339e-001, + -2.432805e-006, + 2.812423e+000, + 5.446957e-001, + -2.043890e+000, + -1.149081e+000, + 2.304118e+000, + -1.715757e+000, + -2.433628e-002, + 2.816836e-001, + 7.185458e-002, + 1.064860e+000, + 2.706789e-001, + -9.040720e-001, + -8.274472e-002, + -2.555676e-001, + -6.326215e-001, + -2.770880e-002, + 6.676024e-001, + -2.513532e-001, + 5.903839e+000, + 1.241452e+000, + -1.000013e+000, + -1.010774e-001, + 3.699166e-001, + 8.774526e-001, + -3.042007e-001, + 6.951053e-001, + 4.361813e-001, + 6.793421e-001, + 2.573892e-001, + -1.171332e+000, + -3.768188e-001, + 3.701377e-001, + -1.470757e+000, + 5.525942e-001, + 2.991456e-002, + 1.581823e-002, + 2.365233e+000, + 8.214514e-001, + -1.068667e+000, + -2.326330e-001, + 6.725059e-001, + 2.243733e+000, + -4.614370e+000, + 1.033677e+000, + 1.376291e-001, + 2.013334e+000, + 6.865304e-001, + // albedo 0, turbidity 6 + -1.592991e+000, + -7.246948e-001, + -2.598204e+001, + 2.621960e+001, + -8.365176e-003, + 4.207571e-001, + -2.742772e-006, + 2.623735e+000, + 5.873190e-001, + -2.271349e+000, + -1.280884e+000, + 6.308739e+000, + -5.758350e+000, + -1.977049e-002, + 3.671835e-001, + 6.698038e-002, + 1.150597e+000, + 1.759218e-001, + -6.368620e-001, + -7.436052e-003, + -2.230026e+000, + 1.640997e+000, + -1.548497e-002, + 3.145331e-001, + -2.492644e-001, + 5.083843e+000, + 1.260215e+000, + -1.177925e+000, + -9.628114e-002, + 3.051152e-001, + -3.749544e-002, + -2.713209e-001, + 1.164226e+000, + 4.559969e-001, + 2.175429e+000, + 2.874284e-001, + -1.078500e+000, + -3.801779e-001, + 4.788906e-001, + -4.795969e-001, + 5.977621e-001, + -4.488535e-001, + 3.386874e-002, + 1.538143e+000, + 8.062054e-001, + -1.108028e+000, + -2.596892e-001, + 5.162202e-001, + 1.557081e+000, + -4.265039e+000, + 1.182535e+000, + 1.563762e-001, + 2.095084e+000, + 6.883383e-001, + // albedo 0, turbidity 7 + -1.668427e+000, + -7.908511e-001, + -2.779690e+001, + 2.799746e+001, + -7.186935e-003, + 3.757766e-001, + -3.326858e-006, + 2.563421e+000, + 5.439687e-001, + -2.156175e+000, + -1.220004e+000, + 3.585732e+000, + -3.235988e+000, + -1.086239e-002, + 1.846143e-001, + 1.046017e-001, + 1.234427e+000, + 2.842191e-001, + -1.117051e+000, + -4.101627e-001, + -8.463730e-001, + 7.671472e-001, + -2.226609e-002, + 8.574943e-001, + -3.434124e-001, + 4.475715e+000, + 1.154824e+000, + -7.444840e-001, + 2.312078e-001, + -5.393724e-001, + 1.574213e-001, + -1.763914e-001, + 2.751692e-001, + 5.564200e-001, + 2.217672e+000, + 3.483932e-001, + -1.273036e+000, + -5.275562e-001, + 4.902512e-001, + -4.498436e-002, + 4.339366e-001, + 2.386682e-001, + 2.380879e-002, + 1.413444e+000, + 7.855923e-001, + -1.084192e+000, + -2.936753e-001, + 4.719432e-001, + 1.384436e+000, + -3.257789e+000, + 6.119543e-001, + 1.681884e-001, + 1.650441e+000, + 6.936631e-001, + // albedo 0, turbidity 8 + -1.848490e+000, + -9.512670e-001, + -3.005251e+001, + 3.024315e+001, + -5.635304e-003, + 3.447780e-001, + -2.782999e-006, + 2.309422e+000, + 5.643559e-001, + -2.300008e+000, + -1.252335e+000, + -1.218876e+000, + 1.493730e+000, + -6.107100e-003, + 7.974860e-002, + 1.023449e-001, + 1.505934e+000, + 2.360948e-001, + -1.483705e+000, + -8.547575e-001, + -7.797146e-001, + 6.447971e-001, + -2.678052e-002, + 1.091263e+000, + -3.344889e-001, + 3.830416e+000, + 1.189425e+000, + -5.348005e-001, + 3.982733e-001, + -4.071573e-001, + 3.265569e-001, + -8.658789e-002, + -2.370892e-001, + 5.369097e-001, + 1.478279e+000, + 3.143303e-001, + -1.320401e+000, + -6.043247e-001, + 3.019196e-001, + -7.732911e-002, + 4.768381e-001, + 6.745764e-001, + 3.694098e-002, + 1.158234e+000, + 8.169056e-001, + -1.101040e+000, + -3.420019e-001, + 3.775661e-001, + 1.769338e+000, + -2.990515e+000, + 1.649529e-001, + 1.970125e-001, + 1.453355e+000, + 6.759757e-001, + // albedo 0, turbidity 9 + -2.251946e+000, + -1.229349e+000, + -3.271808e+001, + 3.283114e+001, + -4.252027e-003, + 3.372289e-001, + -3.001937e-006, + 2.154046e+000, + 5.842674e-001, + -1.867834e+000, + -9.531252e-001, + -1.229365e+001, + 1.269149e+001, + -6.844772e-003, + 1.185107e-001, + 7.539587e-002, + 1.846381e+000, + 1.899412e-001, + -3.398629e+000, + -2.180862e+000, + 2.335213e+000, + -3.382823e+000, + -8.613985e-003, + 8.431602e-001, + -2.393567e-001, + 3.112460e+000, + 1.218556e+000, + 5.708381e-001, + 9.406030e-001, + -6.890113e-001, + 2.746233e+000, + -5.772068e-002, + 1.096005e-001, + 3.491978e-001, + 7.281453e-001, + 3.212049e-001, + -1.705909e+000, + -8.517224e-001, + 1.131160e-001, + -2.141434e+000, + 4.274043e-001, + 3.397600e-001, + 1.786490e-001, + 9.026101e-001, + 7.882800e-001, + -1.012865e+000, + -3.495551e-001, + 3.369038e-001, + 3.724205e+000, + -3.089586e+000, + 1.266964e-001, + 1.461790e-001, + 1.170199e+000, + 6.931052e-001, + // albedo 0, turbidity 10 + -2.890318e+000, + -1.665573e+000, + -3.493756e+001, + 3.500369e+001, + -2.984251e-003, + 2.622419e-001, + -4.259360e-006, + 1.947681e+000, + 6.905752e-001, + -1.956022e+000, + -1.062900e+000, + -1.919714e+001, + 1.975164e+001, + -8.865396e-003, + 2.165540e-001, + 5.475637e-002, + 1.761134e+000, + 3.164249e-003, + -5.612198e+000, + -3.101371e+000, + 4.098034e+000, + -6.144001e+000, + 9.944958e-003, + 2.905472e-001, + -1.707110e-001, + 3.199107e+000, + 1.337660e+000, + 8.353756e-001, + 4.855943e-001, + -1.243589e+000, + 5.147385e+000, + -7.013963e-002, + 9.380410e-001, + 2.335714e-001, + 1.727744e-001, + 2.802696e-001, + -1.524329e+000, + -7.388547e-001, + 3.259025e-001, + -4.050634e+000, + 4.058549e-001, + -2.591384e-001, + 1.898299e-001, + 3.556071e-001, + 7.884126e-001, + -1.070371e+000, + -4.207858e-001, + 1.739862e-001, + 5.293410e+000, + -3.136757e+000, + 2.323856e-001, + 1.673706e-001, + 1.007227e+000, + 6.844287e-001, + // albedo 1, turbidity 1 + -1.341720e+000, + -4.834889e-001, + -4.633447e+001, + 4.682148e+001, + -6.137296e-003, + 4.599216e-001, + 7.047323e-003, + 2.895798e+000, + 4.999398e-001, + -1.529104e+000, + -6.498631e-001, + 1.534103e+001, + -1.450675e+001, + -1.531439e-002, + 3.280082e-001, + 1.682926e-002, + 1.901587e+000, + 5.013227e-001, + -1.014776e+000, + -1.454495e-001, + -4.071085e+000, + 2.954982e+000, + -2.630348e-002, + 5.681531e-001, + -3.016505e-002, + 6.773854e+000, + 5.003504e-001, + -1.172413e+000, + -4.026320e-001, + 2.960428e+000, + 2.020710e-001, + -2.004947e-001, + 9.375572e-001, + 5.998168e-002, + -4.945934e+000, + 4.502898e-001, + -9.898161e-001, + -5.772814e-002, + 4.470024e-001, + -5.786656e-001, + 1.158168e-001, + 3.468040e-001, + -5.043360e-002, + 6.867947e+000, + 8.012363e-001, + -1.085111e+000, + -1.882675e-001, + 1.223748e+000, + 3.565495e-001, + -3.688357e+000, + 5.653723e-001, + 6.727646e-002, + 2.690130e+000, + 4.999400e-001, + // albedo 1, turbidity 2 + -1.389119e+000, + -5.290250e-001, + -4.055774e+001, + 4.105972e+001, + -7.062577e-003, + 4.560060e-001, + -1.736334e-006, + 2.775512e+000, + 6.671455e-001, + -1.584641e+000, + -7.200619e-001, + 1.248067e+001, + -1.156028e+001, + -1.659568e-002, + 3.050029e-001, + 1.099895e-002, + 1.438927e+000, + -2.138015e-002, + -9.826068e-001, + -8.887254e-002, + -2.960031e+000, + 1.808816e+000, + -2.478159e-002, + 6.035733e-001, + -4.868441e-002, + 7.347705e+000, + 1.584739e+000, + -1.150423e+000, + -4.073793e-001, + 2.412991e+000, + 4.870840e-001, + -2.337902e-001, + 8.295114e-001, + 1.129914e-001, + -5.150045e+000, + -9.016643e-002, + -1.016933e+000, + -6.311501e-002, + 5.218937e-001, + -5.716430e-001, + 1.250993e-001, + 3.601524e-001, + -5.497586e-002, + 7.060139e+000, + 1.018333e+000, + -1.073151e+000, + -1.845444e-001, + 1.155394e+000, + 3.004486e-001, + -3.431711e+000, + 4.657031e-001, + 9.401223e-002, + 2.688620e+000, + 4.999544e-001, + // albedo 1, turbidity 3 + -1.391257e+000, + -5.365815e-001, + -4.255881e+001, + 4.299132e+001, + -5.838466e-003, + 4.229134e-001, + -2.760038e-006, + 2.775531e+000, + 6.234597e-001, + -1.780062e+000, + -9.228880e-001, + 1.376172e+001, + -1.260946e+001, + -1.507526e-002, + 3.117435e-001, + 2.205045e-002, + 6.093731e-001, + 3.463446e-002, + -7.388169e-001, + 1.275670e-001, + -3.999528e+000, + 2.223993e+000, + -1.856853e-002, + 5.439310e-001, + -8.834054e-002, + 8.037139e+000, + 1.645951e+000, + -1.322387e+000, + -5.320143e-001, + 2.659359e+000, + 1.086712e+000, + -2.129712e-001, + 8.704649e-001, + 1.800315e-001, + -4.967241e+000, + -1.383720e-001, + -9.378288e-001, + -1.599895e-002, + 3.607555e-001, + -1.980561e+000, + 3.791456e-001, + 1.212268e-001, + -2.845992e-002, + 6.825542e+000, + 1.059139e+000, + -1.100832e+000, + -2.172313e-001, + 1.211561e+000, + 2.002721e+000, + -5.010011e+000, + 5.717583e-001, + 6.777702e-002, + 2.160006e+000, + 5.676392e-001, + // albedo 1, turbidity 4 + -1.409373e+000, + -5.708751e-001, + -3.034974e+001, + 3.079809e+001, + -7.280715e-003, + 3.723304e-001, + -2.436279e-006, + 2.577348e+000, + 5.913377e-001, + -1.954312e+000, + -1.116510e+000, + 5.399148e+000, + -4.299553e+000, + -1.724739e-002, + 3.742824e-001, + 4.187077e-002, + 1.044883e-001, + 1.232727e-001, + -6.772215e-001, + 2.001396e-001, + -3.670523e-001, + -1.014628e+000, + -3.497152e-003, + 4.099858e-001, + -1.584633e-001, + 7.750400e+000, + 1.514559e+000, + -1.291600e+000, + -4.977437e-001, + 9.641914e-001, + 1.562420e+000, + -3.227782e-001, + 9.055427e-001, + 3.046444e-001, + -3.385619e+000, + 9.546291e-003, + -9.750857e-001, + -8.770560e-002, + 9.054256e-001, + -1.429236e+000, + 8.974777e-001, + -1.217961e-001, + -5.194608e-002, + 4.909409e+000, + 9.589153e-001, + -1.088007e+000, + -1.959301e-001, + 9.745799e-001, + 1.260761e+000, + -5.008864e+000, + 7.271248e-001, + 1.096661e-001, + 2.717295e+000, + 6.340731e-001, + // albedo 1, turbidity 5 + -1.456050e+000, + -6.223072e-001, + -2.228088e+001, + 2.269604e+001, + -9.340812e-003, + 4.118308e-001, + -2.418083e-006, + 2.442117e+000, + 5.589638e-001, + -2.176449e+000, + -1.302416e+000, + 2.222836e+000, + -1.222730e+000, + -1.728051e-002, + 1.323513e-001, + 7.027731e-002, + 4.835745e-002, + 2.093351e-001, + -5.789641e-001, + 2.215407e-001, + 2.142291e-001, + -1.201725e+000, + -1.185728e-002, + 8.122982e-001, + -2.380420e-001, + 6.706841e+000, + 1.404146e+000, + -1.307463e+000, + -4.515174e-001, + 6.447827e-001, + 1.223841e+000, + -2.902391e-001, + 4.986588e-001, + 4.073652e-001, + -1.706696e+000, + 1.060885e-001, + -9.698678e-001, + -1.307094e-001, + 9.389347e-001, + -1.522852e+000, + 7.768797e-001, + -1.368595e-001, + -3.857426e-002, + 3.676935e+000, + 8.980966e-001, + -1.104349e+000, + -2.380323e-001, + 1.047043e+000, + 1.865421e+000, + -5.011664e+000, + 7.014954e-001, + 9.622701e-002, + 1.891360e+000, + 6.687354e-001, + // albedo 1, turbidity 6 + -1.502249e+000, + -6.724523e-001, + -2.888092e+001, + 2.930360e+001, + -6.685766e-003, + 3.685464e-001, + -2.469442e-006, + 2.310797e+000, + 5.566754e-001, + -2.217125e+000, + -1.364924e+000, + 4.048243e+000, + -3.111333e+000, + -1.317747e-002, + 1.921948e-001, + 8.627702e-002, + 1.981769e-003, + 2.213689e-001, + -6.215757e-001, + 1.687995e-001, + -5.949131e-001, + -1.551293e-001, + 3.356129e-004, + 6.897657e-001, + -2.855053e-001, + 6.271042e+000, + 1.363084e+000, + -1.216317e+000, + -3.489429e-001, + 7.566226e-001, + 5.409809e-001, + -2.830843e-001, + 6.191825e-001, + 4.755163e-001, + -9.131387e-001, + 1.383909e-001, + -1.030437e+000, + -2.034064e-001, + 8.335995e-001, + -1.050947e+000, + 8.689093e-001, + -3.672310e-001, + -4.056183e-002, + 3.111269e+000, + 8.856842e-001, + -1.078984e+000, + -2.070549e-001, + 9.683145e-001, + 1.497022e+000, + -5.007653e+000, + 7.702541e-001, + 1.285822e-001, + 2.225188e+000, + 6.587911e-001, + // albedo 1, turbidity 7 + -1.559291e+000, + -7.374039e-001, + -3.596311e+001, + 3.634470e+001, + -4.667132e-003, + 3.277964e-001, + -2.487945e-006, + 2.215652e+000, + 5.764681e-001, + -2.356929e+000, + -1.444755e+000, + 6.244526e+000, + -5.540162e+000, + -8.794510e-003, + 1.792100e-001, + 9.578517e-002, + 3.737676e-001, + 1.922194e-001, + -6.589752e-001, + -2.926910e-002, + -1.831779e+000, + 1.869962e+000, + -2.030095e-003, + 7.552089e-001, + -3.168157e-001, + 4.632196e+000, + 1.294054e+000, + -1.161046e+000, + -1.472506e-001, + 6.494138e-001, + -8.327174e-001, + -2.320724e-001, + 3.391212e-001, + 5.269637e-001, + 9.376341e-001, + 2.458573e-001, + -1.034427e+000, + -3.062504e-001, + 8.975634e-001, + 3.203531e-001, + 8.565142e-001, + -1.250162e-001, + -4.094017e-002, + 1.861304e+000, + 8.223468e-001, + -1.109954e+000, + -2.740277e-001, + 1.063811e+000, + 7.077398e-001, + -4.695734e+000, + 5.621696e-001, + 1.248956e-001, + 1.297723e+000, + 6.789720e-001, + // albedo 1, turbidity 8 + -1.788293e+000, + -9.368751e-001, + -4.382980e+001, + 4.424963e+001, + -3.652530e-003, + 3.094331e-001, + -2.810503e-006, + 1.904402e+000, + 5.861599e-001, + -2.268206e+000, + -1.312676e+000, + 2.863082e+000, + -2.373727e+000, + -5.144980e-003, + 1.711072e-001, + 9.316041e-002, + 9.309598e-001, + 1.791683e-001, + -1.376966e+000, + -7.418582e-001, + -1.349589e+000, + 1.563419e+000, + -3.124219e-003, + 6.967139e-001, + -3.061887e-001, + 3.602731e+000, + 1.255669e+000, + -6.017540e-001, + 2.815928e-001, + 5.424052e-001, + -6.885450e-001, + -1.620001e-001, + 2.980046e-001, + 4.995571e-001, + 7.371203e-001, + 2.812466e-001, + -1.278853e+000, + -5.245326e-001, + 7.870520e-001, + 3.125067e-001, + 7.748105e-001, + -7.788581e-002, + 3.490956e-003, + 1.283748e+000, + 8.130190e-001, + -1.050930e+000, + -2.786331e-001, + 1.056344e+000, + 1.053002e+000, + -4.047789e+000, + 4.432174e-001, + 1.169077e-001, + 9.532621e-001, + 6.806764e-001, + // albedo 1, turbidity 9 + -2.084927e+000, + -1.203954e+000, + -4.881638e+001, + 4.920160e+001, + -2.896045e-003, + 2.882977e-001, + -3.073517e-006, + 1.702211e+000, + 6.374180e-001, + -2.328567e+000, + -1.238023e+000, + -1.891019e+000, + 2.451520e+000, + -5.847581e-003, + 2.084702e-001, + 7.848130e-002, + 1.211048e+000, + 8.095008e-002, + -2.634632e+000, + -1.789460e+000, + -1.370558e-001, + -3.326435e-001, + 2.783737e-003, + 5.239451e-001, + -2.548881e-001, + 2.896327e+000, + 1.324116e+000, + 6.882616e-002, + 5.997821e-001, + 1.535398e-001, + 1.375209e+000, + -1.267285e-001, + 4.239743e-001, + 4.013122e-001, + 1.794675e-001, + 2.395382e-001, + -1.430918e+000, + -6.439041e-001, + 8.325980e-001, + -1.705612e+000, + 7.236426e-001, + -5.567593e-002, + 6.408718e-002, + 6.836524e-001, + 8.388887e-001, + -1.037956e+000, + -3.215402e-001, + 9.457349e-001, + 3.178114e+000, + -4.152156e+000, + 2.230992e-001, + 1.156198e-001, + 7.606223e-001, + 6.656923e-001, + // albedo 1, turbidity 10 + -2.967314e+000, + -1.728778e+000, + -3.730988e+001, + 3.755578e+001, + -2.588835e-003, + 2.927966e-001, + -3.935038e-006, + 1.592161e+000, + 6.868694e-001, + -2.123311e+000, + -1.175148e+000, + -1.314988e+001, + 1.386882e+001, + -7.828537e-003, + 1.852026e-001, + 5.481038e-002, + 1.294309e+000, + 2.428177e-002, + -5.443597e+000, + -3.156344e+000, + 2.110838e+000, + -3.421556e+000, + 1.181890e-002, + 1.196951e-001, + -1.742902e-001, + 2.404353e+000, + 1.272805e+000, + 1.029898e+000, + 5.912521e-001, + -3.983531e-001, + 3.286069e+000, + -9.252065e-002, + 1.331381e+000, + 2.560642e-001, + 8.001754e-001, + 3.624178e-001, + -1.547574e+000, + -7.881604e-001, + 1.020902e+000, + -2.897069e+000, + 5.213470e-001, + -9.242315e-001, + 1.185594e-001, + -1.150721e+000, + 7.317211e-001, + -9.621043e-001, + -1.991406e-001, + 6.531287e-001, + 3.925839e+000, + -3.596904e+000, + 6.317332e-001, + 1.531334e-001, + 1.457846e+000, + 6.966285e-001 + ]; + + public static var datasetRGBRad3 = [ + // albedo 0, turbidity 1 + 9.926518e-001, + 1.999494e+000, + -4.136109e+000, + 1.856270e+001, + 1.351028e+001, + 1.390238e+001, + // albedo 0, turbidity 2 + 9.634366e-001, + 2.119694e+000, + -4.614523e+000, + 1.919701e+001, + 1.376644e+001, + 1.418731e+001, + // albedo 0, turbidity 3 + 9.446537e-001, + 2.171610e+000, + -4.915556e+000, + 1.918240e+001, + 1.537135e+001, + 1.400530e+001, + // albedo 0, turbidity 4 + 9.073074e-001, + 2.330536e+000, + -5.577596e+000, + 1.961615e+001, + 1.688365e+001, + 1.446955e+001, + // albedo 0, turbidity 5 + 8.739124e-001, + 2.388682e+000, + -5.842995e+000, + 1.923265e+001, + 1.887735e+001, + 1.485698e+001, + // albedo 0, turbidity 6 + 8.563688e-001, + 2.391534e+000, + -5.769133e+000, + 1.828709e+001, + 2.097209e+001, + 1.469587e+001, + // albedo 0, turbidity 7 + 8.270533e-001, + 2.342790e+000, + -5.558071e+000, + 1.684993e+001, + 2.356498e+001, + 1.505975e+001, + // albedo 0, turbidity 8 + 7.908339e-001, + 2.190341e+000, + -4.852571e+000, + 1.374862e+001, + 2.806846e+001, + 1.548444e+001, + // albedo 0, turbidity 9 + 7.403619e-001, + 1.783998e+000, + -2.983854e+000, + 7.622563e+000, + 3.507610e+001, + 1.615805e+001, + // albedo 0, turbidity 10 + 6.840111e-001, + 1.154457e+000, + -2.393830e-001, + -7.896893e-001, + 4.282765e+001, + 1.779469e+001, + // albedo 1, turbidity 1 + 1.168300e+000, + 1.860993e+000, + -2.129074e+000, + 1.251952e+001, + 3.032499e+001, + 2.938716e+001, + // albedo 1, turbidity 2 + 1.150338e+000, + 1.918813e+000, + -2.413527e+000, + 1.274862e+001, + 3.087134e+001, + 2.951432e+001, + // albedo 1, turbidity 3 + 1.114719e+000, + 1.964689e+000, + -2.625423e+000, + 1.247837e+001, + 3.237949e+001, + 2.943596e+001, + // albedo 1, turbidity 4 + 1.077948e+000, + 2.006292e+000, + -2.846934e+000, + 1.190195e+001, + 3.459293e+001, + 2.937492e+001, + // albedo 1, turbidity 5 + 1.035143e+000, + 1.986681e+000, + -2.752584e+000, + 1.060972e+001, + 3.722185e+001, + 2.918594e+001, + // albedo 1, turbidity 6 + 1.015992e+000, + 1.992054e+000, + -2.812626e+000, + 1.001416e+001, + 3.847300e+001, + 2.924624e+001, + // albedo 1, turbidity 7 + 9.756887e-001, + 1.939897e+000, + -2.533281e+000, + 8.319176e+000, + 4.083907e+001, + 2.925586e+001, + // albedo 1, turbidity 8 + 9.264164e-001, + 1.716454e+000, + -1.597044e+000, + 4.739725e+000, + 4.507683e+001, + 2.878915e+001, + // albedo 1, turbidity 9 + 8.595191e-001, + 1.346034e+000, + -2.801895e-002, + -6.582906e-001, + 5.017523e+001, + 2.852953e+001, + // albedo 1, turbidity 10 + 7.754116e-001, + 7.709245e-001, + 2.200201e+000, + -7.487661e+000, + 5.436622e+001, + 2.893432e+001 + ]; + + public static var datasetsRGB = [ + datasetRGB1, + datasetRGB2, + datasetRGB3 + ]; + + public static var datasetsRGBRad = [ + datasetRGBRad1, + datasetRGBRad2, + datasetRGBRad3 + ]; + +} diff --git a/Sources/armory/renderpath/Inc.hx b/Sources/armory/renderpath/Inc.hx new file mode 100644 index 0000000000..e42a9618fb --- /dev/null +++ b/Sources/armory/renderpath/Inc.hx @@ -0,0 +1,1104 @@ +package armory.renderpath; + +import iron.RenderPath; +import iron.object.LightObject; + +import armory.math.Helper; + +class Inc { + static var path: RenderPath; + public static var superSample = 1.0; + + static var pointIndex = 0; + static var spotIndex = 0; + static var lastFrame = -1; + + #if (rp_voxels && arm_config) + static var voxelsCreated = false; + #end + + public static function init(_path: RenderPath) { + path = _path; + + #if arm_config + var config = armory.data.Config.raw; + for (l in iron.Scene.active.lights) { + l.data.raw.shadowmap_size = l.data.raw.type == "sun" ? + config.rp_shadowmap_cascade : + config.rp_shadowmap_cube; + } + superSample = config.rp_supersample; + #else + + #if (rp_supersampling == 1.5) + superSample = 1.5; + #elseif (rp_supersampling == 2) + superSample = 2.0; + #elseif (rp_supersampling == 4) + superSample = 4.0; + #end + + #end + } + + #if arm_shadowmap_atlas + public static function updatePointLightAtlasData(): Void { + var atlas = ShadowMapAtlas.shadowMapAtlases.get(ShadowMapAtlas.shadowMapAtlasName("point")); + if (atlas != null) { + if(LightObject.pointLightsData == null) { + LightObject.pointLightsData = new kha.arrays.Float32Array( + LightObject.maxLightsCluster * ShadowMapTile.tilesLightType("point") * 4 ); // max possible visible lights * 6 or 2 (faces) * 4 (xyzw) + } + + var n = iron.Scene.active.lights.length > LightObject.maxLightsCluster ? LightObject.maxLightsCluster : iron.Scene.active.lights.length; + var i = 0; + var j = 0; + for (light in iron.Scene.active.lights) { + if (i >= n) + break; + if (LightObject.discardLightCulled(light)) continue; + if (light.data.raw.type == "point") { + if (!light.data.raw.cast_shadow) { + j += 4 * 6; + continue; + } + for(k in 0...6) { + LightObject.pointLightsData[j ] = light.tileOffsetX[k]; // posx + LightObject.pointLightsData[j + 1] = light.tileOffsetY[k]; // posy + LightObject.pointLightsData[j + 2] = light.tileScale[k]; // tile scale factor relative to atlas + LightObject.pointLightsData[j + 3] = 0; // padding + j += 4; + } + } + i++; + } + } + } + + public static function bindShadowMapAtlas() { + for (atlas in ShadowMapAtlas.shadowMapAtlases) { + path.bindTarget(atlas.target, atlas.target); + } + } + + static function getShadowMapAtlas(atlas:ShadowMapAtlas):String { + inline function createDepthTarget(name: String, size: Int) { + var t = new RenderTargetRaw(); + t.name = name; + t.width = t.height = size; + t.format = "DEPTH16"; + return path.createRenderTarget(t); + } + + var rt = path.renderTargets.get(atlas.target); + // Create shadowmap atlas texture on the fly and replace existing on size change + if (rt == null) { + rt = createDepthTarget(atlas.target, atlas.sizew); + } + else if (atlas.updateRenderTarget) { + atlas.updateRenderTarget = false; + // Resize shadow map + rt.unload(); + rt = createDepthTarget(atlas.target, atlas.sizew); + } + return atlas.target; + } + + public static function drawShadowMapAtlas() { + #if rp_shadowmap + #if rp_probes + // Share shadow map with probe + if (lastFrame == RenderPath.active.frame) + return; + lastFrame = RenderPath.active.frame; + #end + // add new lights to the atlases + #if arm_debug + beginShadowsLogicProfile(); + // reset data on rejected lights + for (atlas in ShadowMapAtlas.shadowMapAtlases) { + atlas.rejectedLights = []; + } + #end + for (light in iron.Scene.active.lights) { + if (!light.lightInAtlas && !light.culledLight && light.visible && light.shadowMapScale > 0.0 + && light.data.raw.strength > 0.0 && light.data.raw.cast_shadow) { + ShadowMapAtlas.addLight(light); + } + } + // update point light data before rendering + updatePointLightAtlasData(); + + for (atlas in ShadowMapAtlas.shadowMapAtlases) { + var tilesToRemove = []; + #if arm_shadowmap_atlas_lod + var tilesToChangeSize = []; + #end + + var shadowmap = getShadowMapAtlas(atlas); + path.setTargetStream(shadowmap); + path.clearTarget(null, 1.0); + + for (tile in atlas.activeTiles) { + if (tile.light == null || !tile.light.visible || tile.light.culledLight + || !tile.light.data.raw.cast_shadow || tile.light.data.raw.strength == 0) { + tile.unlockLight = true; + tilesToRemove.push(tile); + continue; + } + + #if arm_shadowmap_atlas_lod + var newTileSize = atlas.getTileSize(tile.light.shadowMapScale); + if (newTileSize != tile.size) { + if (newTileSize == 0) { + tile.unlockLight = true; + tilesToRemove.push(tile); + continue; + } + // queue for size change + tile.newTileSize = newTileSize; + tilesToChangeSize.push(tile); + } + #end + // set the tile offset for this tile and every linked tile to this one + var j = 0; + tile.forEachTileLinked(function (lTile) { + tile.light.tileOffsetX[j] = lTile.coordsX / atlas.sizew; + tile.light.tileOffsetY[j] = lTile.coordsY / atlas.sizew; + tile.light.tileScale[j] = lTile.size / atlas.sizew; + j++; + }); + // set shadowmap size for uniform + tile.light.data.raw.shadowmap_size = atlas.sizew; + + path.light = tile.light; + + var face = 0; + var faces = ShadowMapTile.tilesLightType(tile.light.data.raw.type); + + #if arm_debug + beginShadowsRenderProfile(); + #end + tile.forEachTileLinked(function (lTile) { + if (faces > 1) { + #if arm_csm + switch (tile.light.data.raw.type) { + case "sun": tile.light.setCascade(iron.Scene.active.camera, face); + case "point": path.currentFace = face; + } + #else + path.currentFace = face; + #end + face++; + } + path.setCurrentViewportWithOffset(lTile.size, lTile.size, lTile.coordsX, lTile.coordsY); + + path.drawMeshesStream("shadowmap"); + }); + #if arm_debug + endShadowsRenderProfile(); + #end + + path.currentFace = -1; + } + path.endStream(); + + #if arm_shadowmap_atlas_lod + for (tile in tilesToChangeSize) { + tilesToRemove.push(tile); + + var newTile = ShadowMapTile.assignTiles(tile.light, atlas, tile); + if (newTile != null) + atlas.activeTiles.push(newTile); + } + // update point light data after changing size of tiles to avoid render issues + updatePointLightAtlasData(); + #end + + for (tile in tilesToRemove) { + atlas.activeTiles.remove(tile); + tile.freeTile(); + } + } + #if arm_debug + endShadowsLogicProfile(); + #end + #end //rp_shadowmap + } + + #else + public static function bindShadowMap() { + for (l in iron.Scene.active.lights) { + if (!l.visible || l.data.raw.type != "sun") continue; + var n = "shadowMap"; + path.bindTarget(n, n); + break; + } + for (i in 0...pointIndex) { + var n = "shadowMapPoint[" + i + "]"; + path.bindTarget(n, n); + } + for (i in 0...spotIndex) { + var n = "shadowMapSpot[" + i + "]"; + path.bindTarget(n, n); + } + } + + static function shadowMapName(light: LightObject): String { + switch (light.data.raw.type) { + case "sun": + return "shadowMap"; + case "point": + return "shadowMapPoint[" + pointIndex + "]"; + default: + return "shadowMapSpot[" + spotIndex + "]"; + } + } + + static function getShadowMap(l: iron.object.LightObject): String { + var target = shadowMapName(l); + var rt = path.renderTargets.get(target); + // Create shadowmap on the fly + if (rt == null) { + if (path.light.data.raw.shadowmap_cube) { + // Cubemap size + var size = path.light.data.raw.shadowmap_size; + var t = new RenderTargetRaw(); + t.name = target; + t.width = size; + t.height = size; + t.format = "DEPTH16"; + t.is_cubemap = true; + rt = path.createRenderTarget(t); + } + else { // Non-cube sm + var sizew = path.light.data.raw.shadowmap_size; + var sizeh = sizew; + #if arm_csm // Cascades - atlas on x axis + if (l.data.raw.type == "sun") { + sizew = sizew * iron.object.LightObject.cascadeCount; + } + #end + var t = new RenderTargetRaw(); + t.name = target; + t.width = sizew; + t.height = sizeh; + t.format = "DEPTH16"; + rt = path.createRenderTarget(t); + } + } + return target; + } + + public static function drawShadowMap() { + #if (rp_shadowmap) + + #if rp_probes + // Share shadow map with probe + if (lastFrame == RenderPath.active.frame) return; + lastFrame = RenderPath.active.frame; + #end + + pointIndex = 0; + spotIndex = 0; + for (l in iron.Scene.active.lights) { + if (!l.visible) continue; + + path.light = l; + var shadowmap = Inc.getShadowMap(l); + var faces = l.data.raw.shadowmap_cube ? 6 : 1; + for (i in 0...faces) { + if (faces > 1) path.currentFace = i; + path.setTarget(shadowmap); + path.clearTarget(null, 1.0); + if (l.data.raw.cast_shadow) { + path.drawMeshes("shadowmap"); + } + } + path.currentFace = -1; + + if (l.data.raw.type == "point") pointIndex++; + else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++; + } + + #end // rp_shadowmap + } + #end + + public static function applyConfig() { + #if arm_config + var config = armory.data.Config.raw; + // Resize shadow map + var l = path.light; + if (l.data.raw.type == "sun" && l.data.raw.shadowmap_size != config.rp_shadowmap_cascade) { + l.data.raw.shadowmap_size = config.rp_shadowmap_cascade; + var rt = path.renderTargets.get("shadowMap"); + if (rt != null) { + rt.unload(); + path.renderTargets.remove("shadowMap"); + } + } + else if (l.data.raw.shadowmap_size != config.rp_shadowmap_cube) { + l.data.raw.shadowmap_size = config.rp_shadowmap_cube; + var rt = path.renderTargets.get("shadowMapCube"); + if (rt != null) { + rt.unload(); + path.renderTargets.remove("shadowMapCube"); + } + } + if (superSample != config.rp_supersample) { + superSample = config.rp_supersample; + for (rt in path.renderTargets) { + if (rt.raw.width == 0 && rt.raw.scale != null) { + rt.raw.scale = getSuperSampling(); + } + } + path.resize(); + } + // Init voxels + #if rp_voxels + if (!voxelsCreated) initGI(); + #end + #end // arm_config + } + + #if (rp_translucency) + public static function initTranslucency() { + path.createDepthBuffer("main", "DEPTH24"); + var t = new RenderTargetRaw(); + t.name = "accum"; + t.width = 0; + t.height = 0; + t.displayp = getDisplayp(); + t.format = "RGBA64"; + t.scale = getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "revealage"; + t.width = 0; + t.height = 0; + t.displayp = getDisplayp(); + t.format = "R16"; + t.scale = getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + path.loadShader("shader_datas/translucent_resolve/translucent_resolve"); + } + + public static function drawTranslucency(target: String) { + path.setTarget("accum"); + path.clearTarget(0xff000000); + path.setTarget("revealage"); + path.clearTarget(0xffffffff); + path.setTarget("accum", ["revealage"]); + + #if rp_shadowmap + { + #if arm_shadowmap_atlas + bindShadowMapAtlas(); + #else + bindShadowMap(); + #end + } + #end + + path.drawMeshes("translucent"); + + #if rp_render_to_texture + { + path.setTarget(target); + } + #else + { + path.setTarget(""); + } + #end + + path.bindTarget("accum", "gbuffer0"); + path.bindTarget("revealage", "gbuffer1"); + path.drawShader("shader_datas/translucent_resolve/translucent_resolve"); + } + #end + + #if rp_bloom + public static inline function drawBloom(srcRTName: String, downsampler: Downsampler, upsampler: Upsampler) { + if (armory.data.Config.raw.rp_bloom != false) { + // This can result in little jumps in the perceived bloom radius + // when resizing the window because numMips might change, but + // all implementations using this approach have the same problem + // (including Eevee) + final minDim = Math.min(path.currentW, path.currentH); + final logMinDim = Math.max(1.0, Helper.log2(minDim) + (Main.bloomRadius - 8.0)); + final numMips = Std.int(logMinDim); + + // Sample scale for upsampling, 0.5 to use half-texel steps, + // use fraction of logMinDim to make the visual jumps + // described above less visible + Postprocess.bloom_uniforms[3] = 0.5 + logMinDim - numMips; + + downsampler.dispatch(srcRTName, numMips); + upsampler.dispatch(srcRTName, numMips); + } + } + #end + + #if rp_voxels + public static function initGI(tname = "voxels") { + #if arm_config + var config = armory.data.Config.raw; + if (config.rp_voxels != true || voxelsCreated) return; + voxelsCreated = true; + #end + + var t = new RenderTargetRaw(); + t.name = tname; + t.format = "R8"; + + var res = getVoxelRes(); + var resZ = getVoxelResZ(); + t.width = res; + t.height = res; + t.depth = Std.int(res * resZ); + t.is_image = true; + t.mipmaps = true; + path.createRenderTarget(t); + } + #end + + + + public static inline function getCubeSize(): Int { + #if (rp_shadowmap_cube == 256) + return 256; + #elseif (rp_shadowmap_cube == 512) + return 512; + #elseif (rp_shadowmap_cube == 1024) + return 1024; + #elseif (rp_shadowmap_cube == 2048) + return 2048; + #elseif (rp_shadowmap_cube == 4096) + return 4096; + #else + return 0; + #end + } + + public static inline function getCascadeSize(): Int { + #if (rp_shadowmap_cascade == 256) + return 256; + #elseif (rp_shadowmap_cascade == 512) + return 512; + #elseif (rp_shadowmap_cascade == 1024) + return 1024; + #elseif (rp_shadowmap_cascade == 2048) + return 2048; + #elseif (rp_shadowmap_cascade == 4096) + return 4096; + #elseif (rp_shadowmap_cascade == 8192) + return 8192; + #elseif (rp_shadowmap_cascade == 16384) + return 16384; + #else + return 0; + #end + } + + public static inline function getVoxelRes(): Int { + #if (rp_voxelgi_resolution == 512) + return 512; + #elseif (rp_voxelgi_resolution == 256) + return 256; + #elseif (rp_voxelgi_resolution == 128) + return 128; + #elseif (rp_voxelgi_resolution == 64) + return 64; + #elseif (rp_voxelgi_resolution == 32) + return 32; + #else + return 0; + #end + } + + public static inline function getVoxelResZ(): Float { + #if (rp_voxelgi_resolution_z == 1.0) + return 1.0; + #elseif (rp_voxelgi_resolution_z == 0.5) + return 0.5; + #elseif (rp_voxelgi_resolution_z == 0.25) + return 0.25; + #elseif (rp_voxelgi_resolution_z == 0.125) + return 0.125; + #else + return 0.0; + #end + } + + public static inline function getSuperSampling(): Float { + return superSample; + } + + public static inline function getHdrFormat(): String { + #if rp_hdr + return "RGBA64"; + #else + return "RGBA32"; + #end + } + + public static inline function getDisplayp(): Null { + #if rp_resolution_filter // Custom resolution set + return Main.resolutionSize; + #else + return null; + #end + } + + #if arm_debug + public static var shadowsLogicTime = 0.0; + public static var shadowsRenderTime = 0.0; + static var startShadowsLogicTime = 0.0; + static var startShadowsRenderTime = 0.0; + static var callBackSetup = false; + static function setupEndFrameCallback() { + if (!callBackSetup) { + callBackSetup = true; + iron.App.endFrameCallbacks.push(endFrame); + } + } + static function beginShadowsLogicProfile() { setupEndFrameCallback(); startShadowsLogicTime = kha.Scheduler.realTime(); } + static function beginShadowsRenderProfile() { startShadowsRenderTime = kha.Scheduler.realTime(); } + static function endShadowsLogicProfile() { shadowsLogicTime += kha.Scheduler.realTime() - startShadowsLogicTime - shadowsRenderTime; } + static function endShadowsRenderProfile() { shadowsRenderTime += kha.Scheduler.realTime() - startShadowsRenderTime; } + public static function endFrame() { shadowsLogicTime = 0; shadowsRenderTime = 0; } + #end +} + +#if arm_shadowmap_atlas +class ShadowMapAtlas { + + public var target: String; + public var baseTileSizeConst: Int; + public var maxAtlasSizeConst: Int; + + public var sizew: Int; + public var sizeh: Int; + + public var currTileOffset = 0; + public var tiles: Array = []; + public var activeTiles: Array = []; + public var depth = 1; + #if arm_shadowmap_atlas_lod + var tileSizes: Array = []; + var tileSizeFactor: Array = []; + #end + public var updateRenderTarget = false; + public static var shadowMapAtlases:Map = new Map(); // map a shadowmap atlas to their light type + + #if arm_debug + public var lightType: String; + public var rejectedLights: Array = []; + #end + + function new(light: LightObject) { + + var maxTileSize = shadowMapAtlasSize(light); + this.target = shadowMapAtlasName(light.data.raw.type); + this.sizew = this.sizeh = this.baseTileSizeConst = maxTileSize; + this.depth = getSubdivisions(); + this.maxAtlasSizeConst = getMaxAtlasSize(light.data.raw.type); + + #if arm_shadowmap_atlas_lod + computeTileSizes(maxTileSize, depth); + #end + + #if arm_debug + #if arm_shadowmap_atlas_single_map + this.lightType = "any"; + #else + this.lightType = light.data.raw.type; + #end + #end + + } + + /** + * Adds a light to an atlas. The atlas is decided based on the type of the light + * @param light of type LightObject to be added to an yatlas + * @return if the light was added succesfully + */ + public static function addLight(light: LightObject) { + var atlasName = shadowMapAtlasName(light.data.raw.type); + var atlas = shadowMapAtlases.get(atlasName); + if (atlas == null) { + // create a new atlas + atlas = new ShadowMapAtlas(light); + shadowMapAtlases.set(atlasName, atlas); + } + + // find a free tile for this light + var mainTile = ShadowMapTile.assignTiles(light, atlas, null); + if (mainTile == null) { + #if arm_debug + if (!atlas.rejectedLights.contains(light)) + atlas.rejectedLights.push(light); + #end + return; + } + + atlas.activeTiles.push(mainTile); + // notify the tile on light remove + light.tileNotifyOnRemove = mainTile.notifyOnLightRemove; + // notify atlas when this tile is freed + mainTile.notifyOnFree = atlas.freeActiveTile; + // "lock" light to make sure it's not eligible to be added again + light.lightInAtlas = true; + } + + static inline function shadowMapAtlasSize(light:LightObject):Int { + // TODO: this can break because we are changing shadowmap_size elsewhere. + return light.data.raw.shadowmap_size; + } + + public function getTileSize(shadowMapScale: Float): Int { + #if arm_shadowmap_atlas_lod + // find the first scale factor that is smaller to the shadowmap scale, and then return the previous one. + var i = 0; + for (sizeFactor in tileSizeFactor) { + if (sizeFactor < shadowMapScale) break; + i++; + } + return tileSizes[i - 1]; + #else + return this.baseTileSizeConst; + #end + } + + #if arm_shadowmap_atlas_lod + function computeTileSizes(maxTileSize: Int, depth: Int): Void { + // find the highest value based on the calculation done in the cluster code + var base = LightObject.zToShadowMapScale(0, 16); + var subdiv = base / depth; + for(i in 0...depth){ + this.tileSizes.push(Std.int(maxTileSize / Math.pow(2, i))); + this.tileSizeFactor.push(base); + base -= subdiv; + } + this.tileSizes.push(0); + this.tileSizeFactor.push(0.0); + } + #end + + public inline function atlasLimitReached() { + // asume square atlas + return (currTileOffset + 1) * baseTileSizeConst > maxAtlasSizeConst; + } + + public static inline function shadowMapAtlasName(type: String): String { + #if arm_shadowmap_atlas_single_map + return "shadowMapAtlas"; + #else + switch (type) { + case "point": + return "shadowMapAtlasPoint"; + case "sun": + return "shadowMapAtlasSun"; + default: + return "shadowMapAtlasSpot"; + } + #end + } + + public static inline function getSubdivisions(): Int { + #if (rp_shadowmap_atlas_lod_subdivisions == 2) + return 2; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 3) + return 3; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 4) + return 4; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 5) + return 5; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 6) + return 6; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 7) + return 7; + #elseif (rp_shadowmap_atlas_lod_subdivisions == 8) + return 8; + #elseif (!arm_shadowmap_atlas_lod) + return 1; + #end + } + + public static inline function getMaxAtlasSize(type: String): Int { + #if arm_shadowmap_atlas_single_map + #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; + #elseif (rp_shadowmap_atlas_max_size == 4096) + return 4096; + #elseif (rp_shadowmap_atlas_max_size == 8192) + return 8192; + #elseif (rp_shadowmap_atlas_max_size == 16384) + return 16384; + #end + #else + switch (type) { + case "point": { + #if (rp_shadowmap_atlas_max_size_point == 1024) + return 1024; + #elseif (rp_shadowmap_atlas_max_size_point == 2048) + return 2048; + #elseif (rp_shadowmap_atlas_max_size_point == 4096) + return 4096; + #elseif (rp_shadowmap_atlas_max_size_point == 8192) + return 8192; + #elseif (rp_shadowmap_atlas_max_size_point == 16384) + return 16384; + #end + } + case "spot": { + #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; + #elseif (rp_shadowmap_atlas_max_size_spot == 4096) + return 4096; + #elseif (rp_shadowmap_atlas_max_size_spot == 8192) + return 8192; + #elseif (rp_shadowmap_atlas_max_size_spot == 16384) + return 16384; + #end + } + case "sun": { + #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; + #elseif (rp_shadowmap_atlas_max_size_sun == 4096) + return 4096; + #elseif (rp_shadowmap_atlas_max_size_sun == 8192) + return 8192; + #elseif (rp_shadowmap_atlas_max_size_sun == 16384) + return 16384; + #end + } + default: { + #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; + #elseif (rp_shadowmap_atlas_max_size == 4096) + return 4096; + #elseif (rp_shadowmap_atlas_max_size == 8192) + return 8192; + #elseif (rp_shadowmap_atlas_max_size == 16384) + return 16384; + #end + } + + } + #end + } + + function freeActiveTile(tile: ShadowMapTile) { + activeTiles.remove(tile); + } +} + +class ShadowMapTile { + + public var light:Null = null; + public var coordsX:Int; + public var coordsY:Int; + public var size:Int; + public var tiles:Array = []; + public var linkedTile:ShadowMapTile = null; + + #if arm_shadowmap_atlas_lod + public var parentTile: ShadowMapTile = null; + public var activeSubTiles: Int = 0; + public var newTileSize: Int = -1; + + static var tilePattern = [[0, 0], [1, 0], [0, 1], [1, 1]]; + #end + + function new(coordsX: Int, coordsY: Int, size: Int) { + this.coordsX = coordsX; + this.coordsY = coordsY; + this.size = size; + } + + public static function assignTiles(light: LightObject, atlas: ShadowMapAtlas, oldTile: ShadowMapTile): ShadowMapTile { + var tileSize = 0; + + #if arm_shadowmap_atlas_lod + if (oldTile != null && oldTile.newTileSize != -1) { + // reuse tilesize instead of computing it again + tileSize = oldTile.newTileSize; + oldTile.newTileSize = -1; + } + else + #end + tileSize = atlas.getTileSize(light.shadowMapScale); + + if (tileSize == 0) + return null; + + var tiles = []; + tiles = findCreateTiles(light, oldTile, atlas, tilesLightType(light.data.raw.type), tileSize); + + // lock new tiles with light + for (tile in tiles) + tile.lockTile(light); + + return linkTiles(tiles); + } + + static inline function linkTiles(tiles: Array): ShadowMapTile { + if (tiles.length > 1) { + var linkedTile = tiles[0]; + for (i in 1...tiles.length) { + linkedTile.linkedTile = tiles[i]; + linkedTile = tiles[i]; + } + } + return tiles[0]; + } + + static inline function findCreateTiles(light: LightObject, oldTile: ShadowMapTile, atlas: ShadowMapAtlas, tilesPerLightType: Int, tileSize: Int): Array { + var tilesFound: Array = []; + + while (tilesFound.length < tilesPerLightType) { + findTiles(light, oldTile, atlas.tiles, tileSize, tilesPerLightType, tilesFound); + + if (tilesFound.length < tilesPerLightType) { + tilesFound = []; // empty tilesFound + // skip creating more tiles if limit has been reached + if (atlas.atlasLimitReached()) + break; + + createTiles(atlas.tiles, atlas.baseTileSizeConst, atlas.depth, atlas.currTileOffset, atlas.currTileOffset); + atlas.currTileOffset++; + // update texture to accomodate new size + atlas.updateRenderTarget = true; + atlas.sizew = atlas.sizeh = atlas.currTileOffset * atlas.baseTileSizeConst; + } + } + return tilesFound; + } + + inline static function findTiles(light:LightObject, oldTile: ShadowMapTile, + tiles: Array, size: Int, tilesCount: Int, tilesFound: Array): Void { + #if arm_shadowmap_atlas_lod + if (oldTile != null) { + // reuse children tiles + if (size < oldTile.size) { + oldTile.forEachTileLinked(function(lTile) { + var childTile = findFreeChildTile(lTile, size); + tilesFound.push(childTile); + }); + } + // reuse parent tiles + else { + oldTile.forEachTileLinked(function(lTile) { + // find out if parents tiles are not occupied + var parentTile = findFreeParentTile(lTile, size); + // if parent is free, add it to found tiles + if (parentTile != null) + tilesFound.push(parentTile); + }); + if (tilesFound.length < tilesCount) { + // find naively the rest of the tiles that couldn't be reused + findTilesNaive(light, tiles, size, tilesCount, tilesFound); + } + } + } + else + #end + findTilesNaive(light, tiles, size, tilesCount, tilesFound); + } + + #if arm_shadowmap_atlas_lod + static inline function findFreeChildTile(tile: ShadowMapTile, size: Int): ShadowMapTile { + var childrenTile = tile; + while (size < childrenTile.size) { + childrenTile = childrenTile.tiles[0]; + } + return childrenTile; + } + + static inline function findFreeParentTile(tile: ShadowMapTile, size: Int): ShadowMapTile { + var parentTile = tile; + while (size > parentTile.size) { + parentTile = parentTile.parentTile; + // stop if parent tile is occupied + if (parentTile.activeSubTiles > 1) { + parentTile = null; + break; + } + } + return parentTile; + } + #end + + static function findTilesNaive(light:LightObject, tiles: Array, size: Int, tilesCount: Int, tilesFound: Array): Void { + for (tile in tiles) { + if (tile.size == size) { + if (tile.light == null #if arm_shadowmap_atlas_lod && tile.activeSubTiles == 0 #end) { + tilesFound.push(tile); + // stop after finding enough tiles + if (tilesFound.length == tilesCount) + return; + } + } + else { + // skip over if end of the tree or tile is occupied + if (tile.tiles.length == 0 || tile.light != null) + continue; + findTilesNaive(light, tile.tiles, size, tilesCount, tilesFound); + // skip iterating over the rest of the tiles if found enough + if (tilesFound.length == tilesCount) + return; + } + } + } + + // create a basic tile and subdivide it if needed + public static function createTiles(tiles:Array, size:Int, depth: Int, baseX:Int, baseY:Int) { + var i = baseX; + var j = 0; + var lastTile = tiles.length; + // assume occupied tiles start from 1 line before the base x + var occupiedTiles = baseX - 1; + + while (i >= 0) { + if (i <= occupiedTiles) { // avoid overriding tiles + j = baseY; + } + while (j <= baseY) { + // create base tile of max-size + tiles.push(new ShadowMapTile(size * i, size * j, size)); + #if arm_shadowmap_atlas_lod + tiles[lastTile].tiles = subDivTile(tiles[lastTile], size, size * i, size * j, depth - 1); + #end + lastTile++; + j++; + } + i--; + } + } + + #if arm_shadowmap_atlas_lod + static function subDivTile(parent: ShadowMapTile, size: Int, baseCoordsX: Int, baseCoordsY: Int, depth: Int): Array { + var tileSize = Std.int(size / 2); + + var tiles = []; + + for (i in 0...4) { + var coordsX = baseCoordsX + tilePattern[i][0] * tileSize; + var coordsY = baseCoordsY + tilePattern[i][1] * tileSize; + + var tile = new ShadowMapTile(coordsX, coordsY, tileSize); + tile.parentTile = parent; + + if (depth > 1) + tile.tiles = subDivTile(tile, tileSize, coordsX, coordsY, depth - 1); + tiles.push(tile); + } + + return tiles; + } + #end + + public static inline function tilesLightType(type: String): Int { + switch (type) { + case "sun": + return LightObject.cascadeCount; + case "point": + return 6; + default: + return 1; + } + } + + public function notifyOnLightRemove() { + unlockLight = true; + freeTile(); + } + + inline function lockTile(light: LightObject): Void { + if (this.light != null) + return; + this.light = light; + #if arm_shadowmap_atlas_lod + // update the count of used tiles for parents + this.forEachParentTile(function (pTile) { + pTile.activeSubTiles++; + }); + #end + } + + public var unlockLight: Bool = false; + public var notifyOnFree: ShadowMapTile -> Void; + + public function freeTile(): Void { + // prevent duplicates + if (light != null && unlockLight) { + light.lightInAtlas = false; + unlockLight = false; + } + + var linkedTile = this; + var tempTile = this; + while (linkedTile != null) { + linkedTile.light = null; + #if arm_shadowmap_atlas_lod + // update the count of used tiles for parents + linkedTile.forEachParentTile(function (pTile) { + if (pTile.activeSubTiles > 0) + pTile.activeSubTiles--; + }); + #end + + linkedTile = linkedTile.linkedTile; + // unlink linked tiles + tempTile.linkedTile = null; + tempTile = linkedTile; + } + // notify atlas that this tile has been freed + if (notifyOnFree != null) { + notifyOnFree(this); + notifyOnFree = null; + } + } + + public inline function forEachTileLinked(action: ShadowMapTile->Void): Void { + var linkedTile = this; + while (linkedTile != null) { + action(linkedTile); + linkedTile = linkedTile.linkedTile; + } + } + + #if arm_shadowmap_atlas_lod + public inline function forEachParentTile(action: ShadowMapTile->Void): Void { + var parentTile = this.parentTile; + while (parentTile != null) { + action(parentTile); + parentTile = parentTile.parentTile; + } + } + #end +} +#end diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx new file mode 100644 index 0000000000..b2a9589507 --- /dev/null +++ b/Sources/armory/renderpath/Nishita.hx @@ -0,0 +1,231 @@ +package armory.renderpath; + +import kha.FastFloat; +import kha.arrays.Float32Array; +import kha.graphics4.TextureFormat; +import kha.graphics4.Usage; + +import iron.data.WorldData; +import iron.math.Vec2; +import iron.math.Vec3; + +import armory.math.Helper; + +/** + Utility class to control the Nishita sky model. +**/ +class Nishita { + + public static var data: NishitaData = null; + + /** + Recomputes the nishita lookup table after the density settings changed. + Do not call this method on every frame (it's slow)! + **/ + public static function recompute(world: WorldData) { + if (world == null || world.raw.nishita_density == null) return; + if (data == null) data = new NishitaData(); + + var density = world.raw.nishita_density; + data.computeLUT(new Vec3(density[0], density[1], density[2])); + } + + /** Sets the sky's density parameters and calls `recompute()` afterwards. **/ + public static function setDensity(world: WorldData, densityAir: FastFloat, densityDust: FastFloat, densityOzone: FastFloat) { + if (world == null) return; + + if (world.raw.nishita_density == null) world.raw.nishita_density = new Float32Array(3); + var density = world.raw.nishita_density; + density[0] = Helper.clamp(densityAir, 0, 10); + density[1] = Helper.clamp(densityDust, 0, 10); + density[2] = Helper.clamp(densityOzone, 0, 10); + + recompute(world); + } +} + +/** + This class holds the precalculated result of the inner scattering integral + of the Nishita sky model. The outer integral is calculated in + [`armory/Shaders/std/sky.glsl`](https://github.com/armory3d/armory/blob/master/Shaders/std/sky.glsl). + + @see `armory.renderpath.Nishita` +**/ +class NishitaData { + + public var lut: kha.Image; + + /** + The amount of individual sample heights stored in the LUT (and the width + of the LUT image). + **/ + public static var lutHeightSteps = 128; + /** + The amount of individual sun angle steps stored in the LUT (and the + height of the LUT image). + **/ + public static var lutAngleSteps = 128; + + /** + Amount of steps for calculating the inner scattering integral. Heigher + values are more precise but take longer to compute. + **/ + public static var jSteps = 8; + + /** Radius of the atmosphere in kilometers. **/ + public static var radiusAtmo = 6420.0; + /** + Radius of the planet in kilometers. The default value is the earth radius as + defined in Cycles. + **/ + public static var radiusPlanet = 6360.0; + + /** Rayleigh scattering coefficient. **/ + public static var rayleighCoeff = new Vec3(5.5e-6, 13.0e-6, 22.4e-6); + /** Rayleigh scattering scale parameter. **/ + public static var rayleighScale = 8e3; + + /** Mie scattering coefficient. **/ + public static var mieCoeff = 2e-5; + /** Mie scattering scale parameter. **/ + public static var mieScale = 1.2e3; + + /** Ozone scattering coefficient. **/ + // The ozone absorption coefficients are taken from Cycles code. + // Because Cycles calculates 21 wavelengths, we use the coefficients + // which are closest to the RGB wavelengths (645nm, 510nm, 440nm). + // Precalculating values by simulating Blender's spec_to_xyz() function + // to include all 21 wavelengths gave unrealistic results. + public static var ozoneCoeff = new Vec3(1.59051840791988e-6, 0.00000096707041180970, 0.00000007309568762914); + + public function new() {} + + /** Approximates the density of ozone for a given sample height. **/ + function getOzoneDensity(height: FastFloat): FastFloat { + // Values are taken from Cycles code + if (height < 10000.0 || height >= 40000.0) { + return 0.0; + } + if (height < 25000.0) { + return (height - 10000.0) / 15000.0; + } + return -((height - 40000.0) / 15000.0); + } + + /** + Ray-sphere intersection test that assumes the sphere is centered at the + origin. There is no intersection when result.x > result.y. Otherwise + this function returns the distances to the two intersection points, + which might be equal. + **/ + function raySphereIntersection(rayOrigin: Vec3, rayDirection: Vec3, sphereRadius: Float): Vec2 { + // Algorithm is described here: https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection + var a = rayDirection.dot(rayDirection); + var b = 2.0 * rayDirection.dot(rayOrigin); + var c = rayOrigin.dot(rayOrigin) - (sphereRadius * sphereRadius); + var d = (b * b) - 4.0 * a * c; + + // Ray does not intersect the sphere + if (d < 0.0) return new Vec2(1e5, -1e5); + + return new Vec2( + (-b - Math.sqrt(d)) / (2.0 * a), + (-b + Math.sqrt(d)) / (2.0 * a) + ); + } + + /** + Computes the LUT texture for the given density values. + @param density 3D vector of air density, dust density, ozone density + **/ + public function computeLUT(density: Vec3) { + var imageData = new haxe.io.Float32Array(lutHeightSteps * lutAngleSteps * 4); + + for (x in 0...lutHeightSteps) { + var height = (x / (lutHeightSteps - 1)); + + // Use quadratic height for better horizon precision + height *= height; + height *= radiusAtmo * 1000; // Denormalize height + + for (y in 0...lutAngleSteps) { + var sunTheta = y / (lutAngleSteps - 1) * 2 - 1; + + // Improve horizon precision + // See https://sebh.github.io/publications/egsr2020.pdf (5.3) + sunTheta = Helper.sign(sunTheta) * sunTheta * sunTheta; + sunTheta = sunTheta * Math.PI / 2 + Math.PI / 2; // Denormalize + + var jODepth = sampleSecondaryRay(height, sunTheta, density); + + var pixelIndex = (x + y * lutHeightSteps) * 4; + imageData[pixelIndex + 0] = jODepth.x; + imageData[pixelIndex + 1] = jODepth.y; + imageData[pixelIndex + 2] = jODepth.z; + imageData[pixelIndex + 3] = 1.0; // Unused + } + } + + lut = kha.Image.fromBytes(imageData.view.buffer, lutHeightSteps, lutAngleSteps, TextureFormat.RGBA128, Usage.StaticUsage); + } + + /** + Calculates the integral for the secondary ray. + **/ + public function sampleSecondaryRay(height: FastFloat, sunTheta: FastFloat, density: Vec3): Vec3 { + var radiusPlanetMeters = radiusPlanet * 1000; + + // Reconstruct values from the shader + var iPos = new Vec3(0, 0, height + radiusPlanetMeters); + var pSun = new Vec3(0.0, Math.sin(sunTheta), Math.cos(sunTheta)).normalize(); + + var jTime: FastFloat = 0.0; + // We compute the ray-sphere intersection in km to allow larger + // atmosphere radii (radius is squared inside raySphereIntersection()) + var jStepSize: FastFloat = raySphereIntersection(iPos.clone().mult(0.001), pSun, radiusAtmo).y / jSteps; + jStepSize *= 1000; // convert back to m + + // Optical depth accumulators for the secondary ray (Rayleigh, Mie, ozone) + var jODepth = new Vec3(); + + for (i in 0...jSteps) { + + // Calculate the secondary ray sample position and height + var jPos = iPos.clone().add(pSun.clone().mult(jTime + jStepSize * 0.5)); + var jHeight = jPos.length() - radiusPlanetMeters; + + // Accumulate optical depth + var optDepthRayleigh = Math.exp(-jHeight / rayleighScale) * density.x; + var optDepthMie = Math.exp(-jHeight / mieScale) * density.y; + var optDepthOzone = getOzoneDensity(jHeight) * density.z; + jODepth.addf(optDepthRayleigh, optDepthMie, optDepthOzone); + + jTime += jStepSize; + } + + jODepth.mult(jStepSize); + + // Precalculate a part of the secondary attenuation. + // For one variable (e.g. x) in the vector, the formula is as follows: + // + // attn.x = exp(-(coeffX * (firstOpticalDepth.x + secondOpticalDepth.x))) + // + // We can split that up via: + // + // attn.x = exp(-(coeffX * firstOpticalDepth.x + coeffX * secondOpticalDepth.x)) + // = exp(-(coeffX * firstOpticalDepth.x)) * exp(-(coeffX * secondOpticalDepth.x)) + // + // The first factor of the resulting multiplication is calculated in the + // shader, but we can already precalculate the second one. As a side + // effect this keeps the range of the LUT values small because we don't + // store the optical depth but the attenuation. + var jAttenuation = new Vec3(); + var mie = mieCoeff * jODepth.y; + jAttenuation.addf(mie, mie, mie); + jAttenuation.add(rayleighCoeff.clone().mult(jODepth.x)); + jAttenuation.add(ozoneCoeff.clone().mult(jODepth.z)); + jAttenuation.exp(jAttenuation.mult(-1)); + + return jAttenuation; + } +} diff --git a/Sources/armory/renderpath/Postprocess.hx b/Sources/armory/renderpath/Postprocess.hx new file mode 100644 index 0000000000..0dcc0c019f --- /dev/null +++ b/Sources/armory/renderpath/Postprocess.hx @@ -0,0 +1,351 @@ +package armory.renderpath; + +import iron.data.MaterialData; +import iron.math.Vec4; +import iron.object.Object; + +class Postprocess { + + public static var colorgrading_global_uniforms = [ + [6500.0, 1.0, 0.0], //0: Whitebalance, Shadow Max, Highlight Min + [1.0, 1.0, 1.0], //1: Tint + [1.0, 1.0, 1.0], //2: Saturation + [1.0, 1.0, 1.0], //3: Contrast + [1.0, 1.0, 1.0], //4: Gamma + [1.0, 1.0, 1.0], //5: Gain + [1.0, 1.0, 1.0], //6: Offset + [1.0, 1.0, 1.0] //7: LUT Strength + ]; + + public static var colorgrading_shadow_uniforms = [ + [1.0, 1.0, 1.0], //0: Saturation + [1.0, 1.0, 1.0], //1: Contrast + [1.0, 1.0, 1.0], //2: Gamma + [1.0, 1.0, 1.0], //3: Gain + [1.0, 1.0, 1.0] //4: Offset + ]; + + public static var colorgrading_midtone_uniforms = [ + [1.0, 1.0, 1.0], //0: Saturation + [1.0, 1.0, 1.0], //1: Contrast + [1.0, 1.0, 1.0], //2: Gamma + [1.0, 1.0, 1.0], //3: Gain + [1.0, 1.0, 1.0] //4: Offset + ]; + + public static var colorgrading_highlight_uniforms = [ + [1.0, 1.0, 1.0], //0: Saturation + [1.0, 1.0, 1.0], //1: Contrast + [1.0, 1.0, 1.0], //2: Gamma + [1.0, 1.0, 1.0], //3: Gain + [1.0, 1.0, 1.0] //4: Offset + ]; + + public static var camera_uniforms = [ + 1.0, //0: Camera: F-Number + 2.8333, //1: Camera: Shutter time + 100.0, //2: Camera: ISO + 0.0, //3: Camera: Exposure Compensation + 0.01, //4: Fisheye Distortion + 1, //5: DoF AutoFocus §§ If true, it ignores the DoF Distance setting + 10.0, //6: DoF Distance + 160.0, //7: DoF Focal Length mm + 128, //8: DoF F-Stop + 0, //9: Tonemapping Method + 2.0, //10: Distort + 2.0, //11: Film Grain + 0.25, //12: Sharpen + 0.7 //13: Vignette + ]; + + public static var tonemapper_uniforms = [ + 1.0, //0: Slope + 1.0, //1: Toe + 1.0, //2: Shoulder + 1.0, //3: Black Clip + 1.0 //4: White Clip + ]; + + public static var letterbox_uniforms = [ + [0.0, 0.0, 0.0], //0: Colors + [0.1] //1: Size + ]; + + public static var ssr_uniforms = [ + 0.04, //0: Step + 0.05, //1: StepMin + 5.0, //2: Search + 5.0, //3: Falloff + 0.6 //4: Jitter + ]; + + public static var bloom_uniforms = [ + 0.8, // 0: Threshold + 0.5, // 1: Knee + 0.05, // 2: Strength + 0.0, // 3: Sample scale (value set by renderpath, not used for realtime postprocess) + ]; + + public static var ssao_uniforms = [ + 1.0, + 1.0, + 8 + ]; + + public static var lenstexture_uniforms = [ + 0.1, //0: Center Min Clip + 0.5, //1: Center Max Clip + 0.1, //2: Luminance Min + 2.5, //3: Luminance Max + 2.0 //4: Brightness Exponent + ]; + + public static var chromatic_aberration_uniforms = [ + 2.0, //0: Strength + 32 //1: Samples + ]; + + public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + var v:Vec4 = null; + + switch link { + case "_globalWeight": + var ppm_index = 0; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalTint": + var ppm_index = 1; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalSaturation": + var ppm_index = 2; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalContrast": + var ppm_index = 3; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalGamma": + var ppm_index = 4; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalGain": + var ppm_index = 5; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + case "_globalOffset": + var ppm_index = 6; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_global_uniforms[ppm_index][0]; + v.y = colorgrading_global_uniforms[ppm_index][1]; + v.z = colorgrading_global_uniforms[ppm_index][2]; + + //Shadow ppm + case "_shadowSaturation": + var ppm_index = 0; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_shadow_uniforms[ppm_index][0]; + v.y = colorgrading_shadow_uniforms[ppm_index][1]; + v.z = colorgrading_shadow_uniforms[ppm_index][2]; + case "_shadowContrast": + var ppm_index = 1; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_shadow_uniforms[ppm_index][0]; + v.y = colorgrading_shadow_uniforms[ppm_index][1]; + v.z = colorgrading_shadow_uniforms[ppm_index][2]; + case "_shadowGamma": + var ppm_index = 2; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_shadow_uniforms[ppm_index][0]; + v.y = colorgrading_shadow_uniforms[ppm_index][1]; + v.z = colorgrading_shadow_uniforms[ppm_index][2]; + case "_shadowGain": + var ppm_index = 3; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_shadow_uniforms[ppm_index][0]; + v.y = colorgrading_shadow_uniforms[ppm_index][1]; + v.z = colorgrading_shadow_uniforms[ppm_index][2]; + case "_shadowOffset": + var ppm_index = 4; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_shadow_uniforms[ppm_index][0]; + v.y = colorgrading_shadow_uniforms[ppm_index][1]; + v.z = colorgrading_shadow_uniforms[ppm_index][2]; + + //Midtone ppm + case "_midtoneSaturation": + var ppm_index = 0; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_midtone_uniforms[ppm_index][0]; + v.y = colorgrading_midtone_uniforms[ppm_index][1]; + v.z = colorgrading_midtone_uniforms[ppm_index][2]; + case "_midtoneContrast": + var ppm_index = 1; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_midtone_uniforms[ppm_index][0]; + v.y = colorgrading_midtone_uniforms[ppm_index][1]; + v.z = colorgrading_midtone_uniforms[ppm_index][2]; + case "_midtoneGamma": + var ppm_index = 2; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_midtone_uniforms[ppm_index][0]; + v.y = colorgrading_midtone_uniforms[ppm_index][1]; + v.z = colorgrading_midtone_uniforms[ppm_index][2]; + case "_midtoneGain": + var ppm_index = 3; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_midtone_uniforms[ppm_index][0]; + v.y = colorgrading_midtone_uniforms[ppm_index][1]; + v.z = colorgrading_midtone_uniforms[ppm_index][2]; + case "_midtoneOffset": + var ppm_index = 4; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_midtone_uniforms[ppm_index][0]; + v.y = colorgrading_midtone_uniforms[ppm_index][1]; + v.z = colorgrading_midtone_uniforms[ppm_index][2]; + + //Highlight ppm + case "_highlightSaturation": + var ppm_index = 0; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_highlight_uniforms[ppm_index][0]; + v.y = colorgrading_highlight_uniforms[ppm_index][1]; + v.z = colorgrading_highlight_uniforms[ppm_index][2]; + case "_highlightContrast": + var ppm_index = 1; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_highlight_uniforms[ppm_index][0]; + v.y = colorgrading_highlight_uniforms[ppm_index][1]; + v.z = colorgrading_highlight_uniforms[ppm_index][2]; + case "_highlightGamma": + var ppm_index = 2; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_highlight_uniforms[ppm_index][0]; + v.y = colorgrading_highlight_uniforms[ppm_index][1]; + v.z = colorgrading_highlight_uniforms[ppm_index][2]; + case "_highlightGain": + var ppm_index = 3; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_highlight_uniforms[ppm_index][0]; + v.y = colorgrading_highlight_uniforms[ppm_index][1]; + v.z = colorgrading_highlight_uniforms[ppm_index][2]; + case "_highlightOffset": + var ppm_index = 4; + v = iron.object.Uniforms.helpVec; + v.x = colorgrading_highlight_uniforms[ppm_index][0]; + v.y = colorgrading_highlight_uniforms[ppm_index][1]; + v.z = colorgrading_highlight_uniforms[ppm_index][2]; + + //Postprocess Components + case "_PPComp1": + v = iron.object.Uniforms.helpVec; + v.x = camera_uniforms[0]; //F-Number + v.y = camera_uniforms[1]; //Shutter + v.z = camera_uniforms[2]; //ISO + case "_PPComp2": + v = iron.object.Uniforms.helpVec; + v.x = camera_uniforms[3]; //EC + v.y = camera_uniforms[4]; //Lens Distortion + v.z = camera_uniforms[5]; //DOF Autofocus + case "_PPComp3": + v = iron.object.Uniforms.helpVec; + v.x = camera_uniforms[6]; //Distance + v.y = camera_uniforms[7]; //Focal Length + v.z = camera_uniforms[8]; //F-Stop + case "_PPComp4": + v = iron.object.Uniforms.helpVec; + v.x = Std.int(camera_uniforms[9]); //Tonemapping + v.y = camera_uniforms[11]; //Film Grain + v.z = tonemapper_uniforms[0]; //Slope + case "_PPComp5": + v = iron.object.Uniforms.helpVec; + v.x = tonemapper_uniforms[1]; //Toe + v.y = tonemapper_uniforms[2]; //Shoulder + v.z = tonemapper_uniforms[3]; //Black Clip + case "_PPComp6": + v = iron.object.Uniforms.helpVec; + v.x = tonemapper_uniforms[4]; //White Clip + v.y = lenstexture_uniforms[0]; //Center Min + v.z = lenstexture_uniforms[1]; //Center Max + case "_PPComp7": + v = iron.object.Uniforms.helpVec; + v.x = lenstexture_uniforms[2]; //Lum min + v.y = lenstexture_uniforms[3]; //Lum max + v.z = lenstexture_uniforms[4]; //Expo + case "_PPComp9": + v = iron.object.Uniforms.helpVec; + v.x = ssr_uniforms[0]; //Step + v.y = ssr_uniforms[1]; //StepMin + v.z = ssr_uniforms[2]; //Search + case "_PPComp10": + v = iron.object.Uniforms.helpVec; + v.x = ssr_uniforms[3]; //Falloff + v.y = ssr_uniforms[4]; //Jitter + v.z = 0; + case "_PPComp11": + v = iron.object.Uniforms.helpVec; + v.x = bloom_uniforms[2]; // Bloom Strength + v.y = 0; // Unused + v.z = 0; // Unused + case "_PPComp12": + v = iron.object.Uniforms.helpVec; + v.x = ssao_uniforms[0]; //SSAO Strength + v.y = ssao_uniforms[1]; //SSAO Radius + v.z = ssao_uniforms[2]; //SSAO Max Steps + case "_PPComp13": + v = iron.object.Uniforms.helpVec; + v.x = chromatic_aberration_uniforms[0]; //CA Strength + v.y = chromatic_aberration_uniforms[1]; //CA Samples + v.z = 0; + case "_PPComp14": + v = iron.object.Uniforms.helpVec; + v.x = camera_uniforms[10]; //Distort + v.y = camera_uniforms[12]; //Sharpen + v.z = camera_uniforms[13]; //Vignette + } + + return v; + } + + public static function vec4Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + var v: Vec4 = null; + + switch link { + case "_BloomThresholdData": + if (Downsampler.currentMipLevel == 0) { + // See https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 + v = iron.object.Uniforms.helpVec; + v.x = bloom_uniforms[0]; + v.y = v.x - bloom_uniforms[1]; + v.z = 2 * bloom_uniforms[1]; + v.w = 0.25 / (bloom_uniforms[1] + 6.2e-5); + } + case "_PPComp15": + v = iron.object.Uniforms.helpVec; + v.x = letterbox_uniforms[0][0]; //Color + v.y = letterbox_uniforms[0][1]; + v.z = letterbox_uniforms[0][2]; + v.w = letterbox_uniforms[1][0]; //Size + } + + return v; + } + + public static function init() { + iron.object.Uniforms.externalVec3Links.push(vec3Link); + iron.object.Uniforms.externalVec4Links.push(vec4Link); + } + +} diff --git a/Sources/armory/renderpath/RenderPathCreator.hx b/Sources/armory/renderpath/RenderPathCreator.hx new file mode 100644 index 0000000000..4662117e48 --- /dev/null +++ b/Sources/armory/renderpath/RenderPathCreator.hx @@ -0,0 +1,67 @@ +// Reference: https://github.com/armory3d/armory_docs/blob/master/dev/renderpath.md +package armory.renderpath; + +import iron.RenderPath; + +class RenderPathCreator { + + public static var path: RenderPath; + + public static var commands: Void->Void = function() {}; + + #if (rp_renderer == "Forward") + public static var setTargetMeshes: Void->Void = RenderPathForward.setTargetMeshes; + public static var drawMeshes: Void->Void = RenderPathForward.drawMeshes; + public static var applyConfig: Void->Void = RenderPathForward.applyConfig; + #elseif (rp_renderer == "Deferred") + public static var setTargetMeshes: Void->Void = RenderPathDeferred.setTargetMeshes; + public static var drawMeshes: Void->Void = RenderPathDeferred.drawMeshes; + public static var applyConfig: Void->Void = RenderPathDeferred.applyConfig; + #else + public static var setTargetMeshes: Void->Void = function() {}; + public static var drawMeshes: Void->Void = function() {}; + public static var applyConfig: Void->Void = function() {}; + #end + + public static function get(): RenderPath { + path = new RenderPath(); + Inc.init(path); + + #if rp_pp + iron.App.notifyOnInit(function() { + Postprocess.init(); + }); + #end + + #if (rp_renderer == "Forward") + RenderPathForward.init(path); + path.commands = function() { + RenderPathForward.commands(); + commands(); + } + path.setupDepthTexture = RenderPathForward.setupDepthTexture; + #elseif (rp_renderer == "Deferred") + RenderPathDeferred.init(path); + path.commands = function() { + RenderPathDeferred.commands(); + commands(); + } + path.setupDepthTexture = RenderPathDeferred.setupDepthTexture; + #elseif (rp_renderer == "Raytracer") + RenderPathRaytracer.init(path); + path.commands = function() { + RenderPathRaytracer.commands(); + commands(); + } + #end + return path; + } + + #if rp_voxels + public static var voxelFrame = 0; + public static var voxelFreq = 6; // Revoxelizing frequency + #end + + // Last target before drawing to framebuffer + public static var finalTarget: RenderTarget = null; +} diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx new file mode 100644 index 0000000000..e9763a5bd9 --- /dev/null +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -0,0 +1,1103 @@ +package armory.renderpath; + +import iron.RenderPath; +import iron.Scene; + +class RenderPathDeferred { + + #if (rp_renderer == "Deferred") + static var path: RenderPath; + + #if rp_voxels + static var voxels = "voxels"; + static var voxelsLast = "voxels"; + #end + + #if rp_bloom + static var bloomDownsampler: Downsampler; + static var bloomUpsampler: Upsampler; + #end + + public static inline function setTargetMeshes() { + //Always keep the order of render targets the same as defined in compiled.inc + path.setTarget("gbuffer0", [ + "gbuffer1", + #if rp_gbuffer2 "gbuffer2", #end + #if rp_gbuffer_emission "gbuffer_emission", #end + #if rp_ssrefr "gbuffer_refraction" #end + ]); + } + + public static function drawMeshes() { + path.drawMeshes("mesh"); + } + + public static function applyConfig() { + Inc.applyConfig(); + } + + public static function init(_path: RenderPath) { + + path = _path; + + #if kha_metal + { + path.loadShader("shader_datas/clear_color_depth_pass/clear_color_depth_pass"); + path.loadShader("shader_datas/clear_color_pass/clear_color_pass"); + path.loadShader("shader_datas/clear_depth_pass/clear_depth_pass"); + path.clearShader = "shader_datas/clear_color_depth_pass/clear_color_depth_pass"; + } + #end + + #if (rp_translucency) + { + Inc.initTranslucency(); + } + #end + + #if rp_voxels + { + Inc.initGI(); + #if arm_voxelgi_temporal + { + Inc.initGI("voxelsB"); + } + #end + #if rp_voxels + path.loadShader("shader_datas/deferred_light/deferred_light_VoxelAOvar"); + #end + } + #end + + + path.createDepthBuffer("main", "DEPTH24"); + + var t = new RenderTargetRaw(); + t.name = "gbuffer0"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "tex"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = Inc.getHdrFormat(); + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "buf"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = Inc.getHdrFormat(); + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "gbuffer1"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + #if rp_gbuffer2 + { + var t = new RenderTargetRaw(); + t.name = "gbuffer2"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "taa"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_gbuffer_emission + { + var t = new RenderTargetRaw(); + t.name = "gbuffer_emission"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_depth_texture + { + var t = new RenderTargetRaw(); + t.name = "depthtex"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_material_solid + path.loadShader("shader_datas/deferred_light_solid/deferred_light"); + #elseif rp_material_mobile + path.loadShader("shader_datas/deferred_light_mobile/deferred_light"); + #else + path.loadShader("shader_datas/deferred_light/deferred_light"); + #end + + #if rp_probes + path.loadShader("shader_datas/probe_planar/probe_planar"); + path.loadShader("shader_datas/probe_cubemap/probe_cubemap"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + #end + + #if ((rp_ssgi == "RTGI") || (rp_ssgi == "RTAO")) + { + path.loadShader("shader_datas/ssgi_pass/ssgi_pass"); + path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); + path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + } + #elseif (rp_ssgi == "SSAO") + { + path.loadShader("shader_datas/ssao_pass/ssao_pass"); + path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); + path.loadShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + } + #end + + #if ((rp_ssgi != "Off") || rp_volumetriclight) + { + var t = new RenderTargetRaw(); + t.name = "singlea"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + #if rp_ssgi_half + t.scale *= 0.5; + #end + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "singleb"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + #if rp_ssgi_half + t.scale *= 0.5; + #end + path.createRenderTarget(t); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + { + var t = new RenderTargetRaw(); + t.name = "bufa"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "bufb"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_compositornodes + { + path.loadShader("shader_datas/compositor_pass/compositor_pass"); + } + #end + + #if ((!rp_compositornodes) || (rp_antialiasing == "TAA") || (rp_motionblur == "Camera") || (rp_motionblur == "Object")) + { + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + { + path.loadShader("shader_datas/smaa_edge_detect/smaa_edge_detect"); + path.loadShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); + path.loadShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + #if (rp_antialiasing == "TAA") + { + path.loadShader("shader_datas/taa_pass/taa_pass"); + } + #end + } + #end + + #if (rp_supersampling == 4) + { + path.loadShader("shader_datas/supersample_resolve/supersample_resolve"); + } + #end + + #if rp_volumetriclight + { + path.loadShader("shader_datas/volumetric_light/volumetric_light"); + path.loadShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); + path.loadShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + } + #end + + #if rp_water + { + path.loadShader("shader_datas/water_pass/water_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if rp_depth_texture + { + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if rp_bloom + { + bloomDownsampler = Downsampler.create(path, "shader_datas/bloom_pass/downsample_pass", "bloom"); + bloomUpsampler = Upsampler.create(path, "shader_datas/bloom_pass/upsample_pass", bloomDownsampler.getMipmaps()); + } + #end + + #if rp_autoexposure + { + var t = new RenderTargetRaw(); + t.name = "histogram"; + t.width = 1; + t.height = 1; + t.format = Inc.getHdrFormat(); + path.createRenderTarget(t); + + path.loadShader("shader_datas/histogram_pass/histogram_pass"); + } + #end + + #if rp_sss + { + path.loadShader("shader_datas/sss_pass/sss_pass_x"); + path.loadShader("shader_datas/sss_pass/sss_pass_y"); + } + #end + + #if (rp_ssr_half || rp_ssgi_half) + { + path.loadShader("shader_datas/downsample_depth/downsample_depth"); + var t = new RenderTargetRaw(); + t.name = "half"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = "R32"; // R16 + path.createRenderTarget(t); + } + #end + + #if rp_ssrefr + { + path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + + //holds rior and opacity + var t = new RenderTargetRaw(); + t.name = "gbuffer_refraction"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + //holds colors before refractive meshes are drawn + var t = new RenderTargetRaw(); + t.name = "refr"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + //holds background depth + var t = new RenderTargetRaw(); + t.name = "gbufferD1"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_ssr + { + path.loadShader("shader_datas/ssr_pass/ssr_pass"); + path.loadShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_x"); + path.loadShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_y3_blend"); + #if rp_ssr_half + { + var t = new RenderTargetRaw(); + t.name = "ssra"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = Inc.getHdrFormat(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "ssrb"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = Inc.getHdrFormat(); + path.createRenderTarget(t); + } + #end + } + #end + + #if ((rp_motionblur == "Camera") || (rp_motionblur == "Object")) + { + #if (rp_motionblur == "Camera") + { + path.loadShader("shader_datas/motion_blur_pass/motion_blur_pass"); + } + #else + { + path.loadShader("shader_datas/motion_blur_veloc_pass/motion_blur_veloc_pass"); + } + #end + } + #end + + #if arm_config + { + var t = new RenderTargetRaw(); + t.name = "empty_white"; + t.width = 1; + t.height = 1; + t.format = "R8"; + var rt = new RenderTarget(t); + var b = haxe.io.Bytes.alloc(1); + b.set(0, 255); + rt.image = kha.Image.fromBytes(b, t.width, t.height, kha.graphics4.TextureFormat.L8); + path.renderTargets.set(t.name, rt); + } + #end + + #if rp_chromatic_aberration + { + path.loadShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + } + + @:access(iron.RenderPath) + public static function commands() { + path.setTarget("gbuffer0"); // Only clear gbuffer0 + #if (rp_background == "Clear") + { + path.clearTarget(-1, 1.0); + } + #else + { + path.clearTarget(null, 1.0); + } + #end + + #if rp_gbuffer2 + { + path.setTarget("gbuffer2"); + path.clearTarget(0xff000000); + } + #end + + RenderPathCreator.setTargetMeshes(); + + #if rp_dynres + { + if (armory.data.Config.raw.rp_dynres != false) { + DynamicResolutionScale.run(path); + } + } + #end + + #if rp_stereo + { + path.drawStereo(drawMeshes); + } + #else + { + RenderPathCreator.drawMeshes(); + } + #end + + #if rp_decals + { + #if (!kha_opengl) + path.setDepthFrom("gbuffer0", "gbuffer1"); // Unbind depth so we can read it + path.depthToRenderTarget.set("main", path.renderTargets.get("tex")); + #end + + path.setTarget("gbuffer0", ["gbuffer1"]); + path.bindTarget("_main", "gbufferD"); + path.drawDecals("decal"); + + #if (!kha_opengl) + path.setDepthFrom("gbuffer0", "tex"); // Re-bind depth + path.depthToRenderTarget.set("main", path.renderTargets.get("gbuffer0")); + #end + } + #end + + #if (rp_ssr_half || rp_ssgi_half) + path.setTarget("half"); + path.bindTarget("_main", "texdepth"); + path.drawShader("shader_datas/downsample_depth/downsample_depth"); + #end + + #if ((rp_ssgi == "RTGI") || (rp_ssgi == "RTAO")) + { + if (armory.data.Config.raw.rp_ssgi != false) { + path.setTarget("singlea"); + #if rp_ssgi_half + path.bindTarget("half", "gbufferD"); + #else + path.bindTarget("_main", "gbufferD"); + #end + path.bindTarget("gbuffer0", "gbuffer0"); + #if (rp_ssgi == "RTGI") + path.bindTarget("gbuffer1", "gbuffer1"); + #end + path.drawShader("shader_datas/ssgi_pass/ssgi_pass"); + + path.setTarget("singleb"); + path.bindTarget("singlea", "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); + + path.setTarget("singlea"); + path.bindTarget("singleb", "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + } + } + #elseif (rp_ssgi == "SSAO") + { + if (armory.data.Config.raw.rp_ssgi != false) { + path.setTarget("singlea"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/ssao_pass/ssao_pass"); + + path.setTarget("singleb"); + path.bindTarget("singlea", "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_x"); + + path.setTarget("singlea"); + path.bindTarget("singleb", "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_edge_pass/blur_edge_pass_y"); + } + } + #end + + #if (rp_shadowmap) + // atlasing is exclusive for now + #if arm_shadowmap_atlas + Inc.drawShadowMapAtlas(); + #else + Inc.drawShadowMap(); + #end + #end + + // Voxels + #if rp_voxels + if (armory.data.Config.raw.rp_voxels != false) + { + var voxelize = path.voxelize(); + + #if arm_voxelgi_temporal + voxelize = ++RenderPathCreator.voxelFrame % RenderPathCreator.voxelFreq == 0; + + if (voxelize) { + voxels = voxels == "voxels" ? "voxelsB" : "voxels"; + voxelsLast = voxels == "voxels" ? "voxelsB" : "voxels"; + } + #end + + if (voxelize) { + var voxtex = voxels; + + path.clearImage(voxtex, 0x00000000); + path.setTarget(""); + + var res = Inc.getVoxelRes(); + path.setViewport(res, res); + + #if rp_gbuffer_emission + { + path.bindTarget("gbuffer_emission", "gbufferEmission"); + } + #end + + path.bindTarget(voxtex, "voxels"); + path.drawMeshes("voxel"); + path.generateMipmaps(voxels); + } + } + + #end + + // --- + // Deferred light + // --- + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer1"); // Unbind depth so we can read it + #end + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer1", "gbuffer1"); + #if rp_gbuffer2_direct + path.bindTarget("gbuffer2", "gbuffer2"); + #end + + #if rp_gbuffer2 + { + path.bindTarget("gbuffer2", "gbuffer2"); + } + #end + + #if rp_gbuffer_emission + { + path.bindTarget("gbuffer_emission", "gbufferEmission"); + } + #end + + #if (rp_ssgi != "Off") + { + if (armory.data.Config.raw.rp_ssgi != false) { + path.bindTarget("singlea", "ssaotex"); + } + else { + path.bindTarget("empty_white", "ssaotex"); + } + } + #end + + var voxelao_pass = false; + #if rp_voxels + if (armory.data.Config.raw.rp_voxels != false) + { + #if (arm_config && rp_voxels) + voxelao_pass = true; + #end + path.bindTarget(voxels, "voxels"); + #if arm_voxelgi_temporal + { + path.bindTarget(voxelsLast, "voxelsLast"); + } + #end + } + #end + + #if rp_shadowmap + { + #if arm_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + } + #end + + #if ((rp_voxelgi_shadows) || (rp_voxelgi_refraction)) + { + path.bindTarget(voxels, "voxels"); + } + #end + + #if rp_material_solid + path.drawShader("shader_datas/deferred_light_solid/deferred_light"); + #elseif rp_material_mobile + path.drawShader("shader_datas/deferred_light_mobile/deferred_light"); + #else + voxelao_pass ? + path.drawShader("shader_datas/deferred_light/deferred_light_VoxelAOvar") : + path.drawShader("shader_datas/deferred_light/deferred_light"); + #end + + #if rp_probes + if (!path.isProbe) { + var probes = iron.Scene.active.probes; + for (i in 0...probes.length) { + var p = probes[i]; + if (!p.visible || p.culled) continue; + path.currentProbeIndex = i; + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer1", "gbuffer1"); + path.bindTarget(p.raw.name, "probeTex"); + if (p.data.raw.type == "planar") { + path.drawVolume(p, "shader_datas/probe_planar/probe_planar"); + } + else if (p.data.raw.type == "cubemap") { + path.drawVolume(p, "shader_datas/probe_cubemap/probe_cubemap"); + } + } + } + #end + + #if rp_water + { + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/water_pass/water_pass"); + } + #end + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); // Re-bind depth + #end + + #if (rp_background == "World") + { + if (Scene.active.raw.world_ref != null) { + path.setTarget("tex"); // Re-binds depth + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + } + } + #end + + #if rp_blending + { + path.setTarget("tex"); + path.drawMeshes("blend"); + } + #end + + #if rp_translucency + { + Inc.drawTranslucency("tex"); + } + #end + + #if rp_volumetriclight + { + path.setTarget("singlea"); + path.bindTarget("_main", "gbufferD"); + #if arm_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + path.drawShader("shader_datas/volumetric_light/volumetric_light"); + + path.setTarget("singleb"); + path.bindTarget("singlea", "tex"); + path.drawShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); + + path.setTarget("tex"); + path.bindTarget("singleb", "tex"); + path.drawShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + } + #end + + #if rp_bloom + { + inline Inc.drawBloom("tex", bloomDownsampler, bloomUpsampler); + } + #end + + #if rp_sss + { + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/sss_pass/sss_pass_x"); + + path.setTarget("tex"); + path.bindTarget("buf", "tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/sss_pass/sss_pass_y"); + } + #end + + #if rp_ssrefr + { + if (armory.data.Config.raw.rp_ssrefr != false) { + //save depth + path.setTarget("gbufferD1"); + path.bindTarget("_main", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + //save render + path.setTarget("refr"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + RenderPathCreator.setTargetMeshes(); + path.drawMeshes("refraction"); + + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer1", "gbuffer1"); + + #if rp_gbuffer2 + { + path.bindTarget("gbuffer2", "gbuffer2"); + } + #end + + #if rp_gbuffer_emission + { + path.bindTarget("gbuffer_emission", "gbufferEmission"); + } + #end + + #if (rp_ssgi != "Off") + { + if (armory.data.Config.raw.rp_ssgi != false) { + path.bindTarget("singlea", "ssaotex"); + } + else { + path.bindTarget("empty_white", "ssaotex"); + } + } + #end + + var voxelao_pass = false; + #if rp_voxelao + if (armory.data.Config.raw.rp_voxels != false) + { + #if arm_config + voxelao_pass = true; + #end + path.bindTarget(voxels, "voxels"); + #if arm_voxelgi_temporal + { + path.bindTarget(voxelsLast, "voxelsLast"); + } + #end + } + #end + + #if rp_shadowmap + { + #if arm_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + } + #end + + #if rp_material_solid + path.drawShader("shader_datas/deferred_light_solid/deferred_light"); + #elseif rp_material_mobile + path.drawShader("shader_datas/deferred_light_mobile/deferred_light"); + #else + voxelao_pass ? + path.drawShader("shader_datas/deferred_light/deferred_light_VoxelAOvar") : + path.drawShader("shader_datas/deferred_light/deferred_light"); + #end + + path.setTarget("tex"); + path.bindTarget("refr", "tex1"); + path.bindTarget("tex", "tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbufferD1", "gbufferD1"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); + path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + } + } + #end + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); // Re-bind depth + #end + + #if rp_ssr + { + if (armory.data.Config.raw.rp_ssr != false) { + #if rp_ssr_half + var targeta = "ssra"; + var targetb = "ssrb"; + #else + var targeta = "buf"; + var targetb = "gbuffer1"; + #end + + path.setTarget(targeta); + path.bindTarget("tex", "tex"); + #if rp_ssr_half + path.bindTarget("half", "gbufferD"); + #else + path.bindTarget("_main", "gbufferD"); + #end + + path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("gbuffer1", "gbuffer1"); + path.drawShader("shader_datas/ssr_pass/ssr_pass"); + + path.setTarget(targetb); + path.bindTarget(targeta, "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_x"); + + path.setTarget("tex"); + path.bindTarget(targetb, "tex"); + path.bindTarget("gbuffer0", "gbuffer0"); + path.drawShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_y3_blend"); + } + } + #end + + #if ((rp_motionblur == "Camera") || (rp_motionblur == "Object")) + { + if (armory.data.Config.raw.rp_motionblur != false) { + #if (rp_motionblur == "Camera") + { + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer1"); // Unbind depth so we can read it + #end + } + #end + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + #if (rp_motionblur == "Camera") + { + path.bindTarget("_main", "gbufferD"); + path.drawShader("shader_datas/motion_blur_pass/motion_blur_pass"); + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); // Re-bind depth + #end + } + #else + { + path.bindTarget("gbuffer2", "sveloc"); + path.drawShader("shader_datas/motion_blur_veloc_pass/motion_blur_veloc_pass"); + } + #end + path.setTarget("tex"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + } + #end + + #if rp_chromatic_aberration + { + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + path.setTarget("tex"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + + // We are just about to enter compositing, add more custom passes here + // #if rp_custom_pass + // { + // } + // #end + + // Begin compositor + #if rp_autoexposure + { + path.setTarget("histogram"); + #if (rp_antialiasing == "TAA") + path.bindTarget("taa", "tex"); + #else + path.bindTarget("tex", "tex"); + #end + path.drawShader("shader_datas/histogram_pass/histogram_pass"); + } + #end + + #if (rp_supersampling == 4) + var framebuffer = "buf"; + #else + var framebuffer = ""; + #end + + RenderPathCreator.finalTarget = path.currentTarget; + + var target = ""; + #if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA") || (!rp_render_to_texture)) + { + target = framebuffer; + } + #else + { + target = "buf"; + } + #end + path.setTarget(target); + + path.bindTarget("tex", "tex"); + #if rp_compositordepth + { + path.bindTarget("_main", "gbufferD"); + } + #end + + #if rp_autoexposure + { + path.bindTarget("histogram", "histogram"); + } + #end + + #if rp_compositornodes + { + #if rp_probes + var isProbe = path.isProbe; + #else + var isProbe = false; + #end + if (!isProbe) path.drawShader("shader_datas/compositor_pass/compositor_pass"); + else path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #else + { + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + // End compositor + + #if rp_overlays + { + path.setTarget(target); + path.clearTarget(null, 1.0); + path.drawMeshes("overlay"); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + { + path.setTarget("bufa"); + path.clearTarget(0x00000000); + path.bindTarget("buf", "colorTex"); + path.drawShader("shader_datas/smaa_edge_detect/smaa_edge_detect"); + + path.setTarget("bufb"); + path.clearTarget(0x00000000); + path.bindTarget("bufa", "edgesTex"); + path.drawShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); + + #if (rp_antialiasing == "TAA") + path.isProbe ? path.setTarget(framebuffer) : path.setTarget("bufa"); + #else + path.setTarget(framebuffer); + #end + path.bindTarget("buf", "colorTex"); + path.bindTarget("bufb", "blendTex"); + #if (rp_antialiasing == "TAA") + { + path.bindTarget("gbuffer2", "sveloc"); + } + #end + path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + + #if (rp_antialiasing == "TAA") + { + if (!path.isProbe) { // No last frame for probe + path.setTarget(framebuffer); + path.bindTarget("bufa", "tex"); + path.bindTarget("taa", "tex2"); + path.bindTarget("gbuffer2", "sveloc"); + path.drawShader("shader_datas/taa_pass/taa_pass"); + + path.setTarget("taa"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + } + #end + } + #end + + #if (rp_supersampling == 4) + { + var finalTarget = ""; + path.setTarget(finalTarget); + path.bindTarget(framebuffer, "tex"); + path.drawShader("shader_datas/supersample_resolve/supersample_resolve"); + } + #end + } + + public static function setupDepthTexture() { + #if (!kha_opengl) + path.setDepthFrom("gbuffer0", "gbuffer1"); // Unbind depth so we can read it + path.depthToRenderTarget.set("main", path.renderTargets.get("tex")); // tex and gbuffer0 share a depth buffer + #end + + // Copy the depth buffer to the depth texture + path.setTarget("depthtex"); + path.bindTarget("_main", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + #if (!kha_opengl) + path.setDepthFrom("gbuffer0", "tex"); // Re-bind depth + path.depthToRenderTarget.set("main", path.renderTargets.get("gbuffer0")); + #end + + // Prepare to draw meshes + setTargetMeshes(); + path.bindTarget("depthtex", "depthtex"); + } + #end +} diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx new file mode 100644 index 0000000000..79341f9416 --- /dev/null +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -0,0 +1,657 @@ +package armory.renderpath; + +import iron.RenderPath; +import iron.Scene; + +class RenderPathForward { + #if (rp_renderer == "Forward") + + static var path: RenderPath; + + #if rp_voxels + static var voxels = "voxels"; + static var voxelsLast = "voxels"; + #end + + #if rp_bloom + static var bloomDownsampler: Downsampler; + static var bloomUpsampler: Upsampler; + #end + + public static function setTargetMeshes() { + #if rp_render_to_texture + { + path.setTarget("lbuffer0", [ + #if rp_ssr "lbuffer1", #end + #if rp_ssrefr "gbuffer_refraction" #end + ]); + } + #else + { + path.setTarget(""); + } + #end + } + + public static function drawMeshes() { + path.drawMeshes("mesh"); + + #if (rp_background == "World") + { + if (Scene.active.raw.world_ref != null) { + RenderPathCreator.setTargetMeshes(); + path.drawSkydome("shader_datas/World_" + Scene.active.raw.world_ref + "/World_" + Scene.active.raw.world_ref); + } + } + #end + + #if rp_blending + { + RenderPathCreator.setTargetMeshes(); + path.drawMeshes("blend"); + } + #end + + #if rp_translucency + { + RenderPathCreator.setTargetMeshes(); + Inc.drawTranslucency("lbuffer0"); + } + #end + } + + public static function applyConfig() { + Inc.applyConfig(); + } + + public static function init(_path: RenderPath) { + path = _path; + #if kha_metal + { + path.loadShader("shader_datas/clear_color_depth_pass/clear_color_depth_pass"); + path.loadShader("shader_datas/clear_color_pass/clear_color_pass"); + path.loadShader("shader_datas/clear_depth_pass/clear_depth_pass"); + path.clearShader = "shader_datas/clear_color_depth_pass/clear_color_depth_pass"; + } + #end + + #if rp_depth_texture + { + var t = new RenderTargetRaw(); + t.name = "depthtex"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_render_to_texture + { + path.createDepthBuffer("main", "DEPTH24"); + + var t = new RenderTargetRaw(); + t.name = "lbuffer0"; + t.width = 0; + t.height = 0; + t.format = Inc.getHdrFormat(); + t.displayp = Inc.getDisplayp(); + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + #if rp_ssr + { + var t = new RenderTargetRaw(); + t.name = "lbuffer1"; + t.width = 0; + t.height = 0; + t.format = "RGBA64"; + t.displayp = Inc.getDisplayp(); + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_compositornodes + { + path.loadShader("shader_datas/compositor_pass/compositor_pass"); + } + #else + { + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if ((rp_supersampling == 4) || (rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA") || (rp_depth_texture)) + { + var t = new RenderTargetRaw(); + t.name = "buf"; + t.width = 0; + t.height = 0; + t.format = "RGBA32"; + t.displayp = Inc.getDisplayp(); + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + } + #end + + #if (rp_supersampling == 4) + { + path.loadShader("shader_datas/supersample_resolve/supersample_resolve"); + } + #end + } + #end + + #if (rp_translucency) + { + Inc.initTranslucency(); + } + #end + + #if rp_voxels + { + Inc.initGI(); + #if arm_voxelgi_temporal + { + Inc.initGI("voxelsB"); + } + #end + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA") || (rp_ssr && !rp_ssr_half) || (rp_water) || (rp_depth_texture)) + { + var t = new RenderTargetRaw(); + t.name = "bufa"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "bufb"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + path.loadShader("shader_datas/smaa_edge_detect/smaa_edge_detect"); + path.loadShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); + path.loadShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + + #if (rp_antialiasing == "TAA") + { + path.loadShader("shader_datas/taa_pass/taa_pass"); + } + #end + #end + + #if rp_volumetriclight + { + path.loadShader("shader_datas/volumetric_light/volumetric_light"); + path.loadShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); + path.loadShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + + var t = new RenderTargetRaw(); + t.name = "singlea"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "singleb"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R8"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_water + { + path.loadShader("shader_datas/water_pass/water_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if rp_depth_texture + { + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if rp_bloom + { + bloomDownsampler = Downsampler.create(path, "shader_datas/bloom_pass/downsample_pass", "bloom"); + bloomUpsampler = Upsampler.create(path, "shader_datas/bloom_pass/upsample_pass", bloomDownsampler.getMipmaps()); + } + #end + + #if (rp_ssr_half || rp_ssgi_half) + { + path.loadShader("shader_datas/downsample_depth/downsample_depth"); + var t = new RenderTargetRaw(); + t.name = "half"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = "R32"; // R16 + path.createRenderTarget(t); + } + #end + + #if rp_ssrefr + { + path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + + //holds rior and opacity + var t = new RenderTargetRaw(); + t.name = "gbuffer_refraction"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + //holds colors before refractive meshes are drawn + var t = new RenderTargetRaw(); + t.name = "refr"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + //holds background depth + var t = new RenderTargetRaw(); + t.name = "gbufferD1"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + + #if rp_ssr + { + path.loadShader("shader_datas/ssr_pass/ssr_pass"); + path.loadShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_x"); + path.loadShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_y3_blend"); + + #if rp_ssr_half + { + var t = new RenderTargetRaw(); + t.name = "ssra"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = Inc.getHdrFormat(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "ssrb"; + t.width = 0; + t.height = 0; + t.scale = Inc.getSuperSampling() * 0.5; + t.format = Inc.getHdrFormat(); + path.createRenderTarget(t); + } + #end + } + #end + + #if rp_chromatic_aberration + { + path.loadShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + path.loadShader("shader_datas/copy_pass/copy_pass"); + } + #end + } + + public static function commands() { + #if rp_shadowmap + { + #if arm_shadowmap_atlas + Inc.drawShadowMapAtlas(); + #else + Inc.drawShadowMap(); + #end + } + #end + + // Voxels + #if rp_voxels + if (armory.data.Config.raw.rp_voxels != false) + { + var voxelize = path.voxelize(); + + #if arm_voxelgi_temporal + voxelize = ++RenderPathCreator.voxelFrame % RenderPathCreator.voxelFreq == 0; + + if (voxelize) { + voxels = voxels == "voxels" ? "voxelsB" : "voxels"; + voxelsLast = voxels == "voxels" ? "voxelsB" : "voxels"; + } + #end + + if (voxelize) { + var voxtex = voxels; + + path.clearImage(voxtex, 0x00000000); + path.setTarget(""); + + var res = Inc.getVoxelRes(); + path.setViewport(res, res); + + #if rp_gbuffer_emission + { + path.bindTarget("gbuffer_emission", "gbufferEmission"); + } + #end + + path.bindTarget(voxtex, "voxels"); + path.drawMeshes("voxel"); + path.generateMipmaps(voxels); + } + } + #end + + RenderPathCreator.setTargetMeshes(); + + #if (rp_background == "Clear") + { + path.clearTarget(-1, 1.0); + } + #else + { + path.clearTarget(null, 1.0); + } + #end + + #if rp_depthprepass + { + path.drawMeshes("depth"); + RenderPathCreator.setTargetMeshes(); + } + #end + + #if rp_shadowmap + { + #if arm_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + } + #end + + #if rp_voxels + { + path.bindTarget(voxels, "voxels"); + #if arm_voxelgi_temporal + { + path.bindTarget(voxelsLast, "voxelsLast"); + } + #end + } + #end + + #if rp_stereo + { + path.drawStereo(drawMeshes); + } + #else + { + RenderPathCreator.drawMeshes(); + } + #end + + #if rp_render_to_texture + { + #if (rp_ssr_half || rp_ssgi_half) + path.setTarget("half"); + path.bindTarget("_main", "texdepth"); + path.drawShader("shader_datas/downsample_depth/downsample_depth"); + #end + + #if rp_ssrefr + { + if (armory.data.Config.raw.rp_ssrefr != false) { + path.setTarget("gbufferD1"); + path.bindTarget("_main", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + path.setTarget("refr"); + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + RenderPathCreator.setTargetMeshes(); + path.drawMeshes("refraction"); + + path.setTarget("lbuffer0"); + path.bindTarget("refr", "tex1"); + path.bindTarget("lbuffer0", "tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("gbufferD1", "gbufferD1"); + path.bindTarget("lbuffer1", "gbuffer0"); + path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); + path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + } + } + #end + + #if rp_ssr + { + if (armory.data.Config.raw.rp_ssr != false) { + #if rp_ssr_half + var targeta = "ssra"; + var targetb = "ssrb"; + #else + var targeta = "bufa"; + var targetb = "bufb"; + #end + + path.setTarget(targeta); + path.bindTarget("lbuffer0", "tex"); + #if rp_ssr_half + path.bindTarget("half", "gbufferD"); + #else + path.bindTarget("_main", "gbufferD"); + #end + path.bindTarget("lbuffer1", "gbuffer0"); + path.bindTarget("lbuffer0", "gbuffer1"); + path.drawShader("shader_datas/ssr_pass/ssr_pass"); + + path.setTarget(targetb); + path.bindTarget(targeta, "tex"); + path.bindTarget("lbuffer1", "gbuffer0"); + path.drawShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_x"); + + path.setTarget("lbuffer0"); + path.bindTarget(targetb, "tex"); + path.bindTarget("lbuffer1", "gbuffer0"); + path.drawShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_y3_blend"); + } + } + #end + + #if rp_bloom + { + inline Inc.drawBloom("lbuffer0", bloomDownsampler, bloomUpsampler); + } + #end + + #if rp_volumetriclight + { + path.setTarget("singlea"); + path.bindTarget("_main", "gbufferD"); + #if arm_shadowmap_atlas + Inc.bindShadowMapAtlas(); + #else + Inc.bindShadowMap(); + #end + path.drawShader("shader_datas/volumetric_light/volumetric_light"); + + path.setTarget("singleb"); + path.bindTarget("singlea", "tex"); + path.drawShader("shader_datas/blur_bilat_pass/blur_bilat_pass_x"); + + path.setTarget("lbuffer0"); + path.bindTarget("singleb", "tex"); + path.drawShader("shader_datas/blur_bilat_blend_pass/blur_bilat_blend_pass_y"); + } + #end + + #if rp_water + { + path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it + path.depthToRenderTarget.set("main", path.renderTargets.get("buf")); + + path.setTarget("bufa"); + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + path.setTarget("lbuffer0"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/water_pass/water_pass"); + + path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth + path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0")); + } + #end + + #if rp_chromatic_aberration + { + path.setTarget("bufa"); + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/chromatic_aberration_pass/chromatic_aberration_pass"); + + path.setTarget("lbuffer0"); + path.bindTarget("bufa", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if (rp_supersampling == 4) + var framebuffer = "buf"; + #else + var framebuffer = ""; + #end + + #if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA")) + { + RenderPathCreator.finalTarget = path.currentTarget; + path.setTarget(framebuffer); + } + #else + { + path.setTarget("buf"); + RenderPathCreator.finalTarget = path.currentTarget; + } + #end + + #if rp_compositordepth + { + path.bindTarget("_main", "gbufferD"); + } + #end + + #if rp_compositornodes + { + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/compositor_pass/compositor_pass"); + } + #else + { + path.bindTarget("lbuffer0", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) + { + path.setTarget("bufa"); + path.clearTarget(0x00000000); + path.bindTarget("buf", "colorTex"); + path.drawShader("shader_datas/smaa_edge_detect/smaa_edge_detect"); + + path.setTarget("bufb"); + path.clearTarget(0x00000000); + path.bindTarget("bufa", "edgesTex"); + path.drawShader("shader_datas/smaa_blend_weight/smaa_blend_weight"); + + path.setTarget(framebuffer); + path.bindTarget("buf", "colorTex"); + path.bindTarget("bufb", "blendTex"); + path.drawShader("shader_datas/smaa_neighborhood_blend/smaa_neighborhood_blend"); + } + #end + + #if (rp_supersampling == 4) + { + var finalTarget = ""; + path.setTarget(finalTarget); + path.bindTarget(framebuffer, "tex"); + path.drawShader("shader_datas/supersample_resolve/supersample_resolve"); + } + #end + } + #end + + #if rp_overlays + { + path.clearTarget(null, 1.0); + path.drawMeshes("overlay"); + } + #end + } + + public static function setupDepthTexture() { + // When render to texture is off, lbuffer0 does not exist, so for + // now do nothing then and pass an empty uniform to the shader + #if rp_render_to_texture + #if (!kha_opengl) + path.setDepthFrom("lbuffer0", "bufa"); // Unbind depth so we can read it + path.depthToRenderTarget.set("main", path.renderTargets.get("buf")); + #end + + // Copy the depth buffer to the depth texture + path.setTarget("depthtex"); + path.bindTarget("_main", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + + #if (!kha_opengl) + path.setDepthFrom("lbuffer0", "buf"); // Re-bind depth + path.depthToRenderTarget.set("main", path.renderTargets.get("lbuffer0")); + #end + #end // rp_render_to_texture + + RenderPathCreator.setTargetMeshes(); + path.bindTarget("depthtex", "depthtex"); + } + #end +} diff --git a/Sources/armory/renderpath/RenderPathRaytracer.hx b/Sources/armory/renderpath/RenderPathRaytracer.hx new file mode 100644 index 0000000000..fe8812c854 --- /dev/null +++ b/Sources/armory/renderpath/RenderPathRaytracer.hx @@ -0,0 +1,150 @@ +package armory.renderpath; + +#if kha_dxr + +import kha.graphics5.CommandList; +import kha.graphics5.ConstantBuffer; +import kha.graphics5.RayTraceTarget; +import kha.graphics5.RayTracePipeline; +import kha.graphics5.AccelerationStructure; +import kha.graphics5.VertexBuffer; +import kha.graphics5.IndexBuffer; +import kha.graphics5.VertexStructure; +import kha.graphics5.VertexData; +import kha.graphics5.TextureFormat; + +import iron.RenderPath; + +class RenderPathRaytracer { + + #if (rp_renderer == "Raytracer") + + static var path: RenderPath; + static var ready = false; + + static inline var bufferCount = 2; + static var currentBuffer = -1; + static var commandList: CommandList; + static var framebuffers = new haxe.ds.Vector(bufferCount); + static var constantBuffer: ConstantBuffer; + static var target: RayTraceTarget; + static var pipeline: RayTracePipeline; + static var accel: AccelerationStructure; + static var frame = 0.0; + + @:access(iron.data.Geometry) + public static function init(_path: RenderPath) { + path = _path; + + kha.Assets.loadBlobFromPath("raytrace.cso", function(rayTraceShader: kha.Blob) { + ready = true; + + // Command list + commandList = new CommandList(); + for (i in 0...bufferCount) { + framebuffers[i] = new kha.graphics5.RenderTarget(iron.App.w(), iron.App.h(), 16, false, TextureFormat.RGBA32, + -1, -i - 1 /* hack in an index for backbuffer render targets */); + } + commandList.end(); // TODO: Otherwise "Reset fails because the command list was not closed" + + // Pipeline + constantBuffer = new ConstantBuffer(21 * 4); + pipeline = new RayTracePipeline(commandList, rayTraceShader, constantBuffer); + + // Acceleration structure + var structure = new VertexStructure(); + structure.add("pos", VertexData.Float3); + structure.add("nor", VertexData.Float3); + structure.add("tex", VertexData.Float2); + var md = iron.Scene.active.meshes[0].data; + var geom = md.geom; + var verts = Std.int(geom.positions.length / 4); + var vb = new VertexBuffer(verts, structure, kha.graphics5.Usage.StaticUsage); + var vba = vb.lock(); + // iron.data.Geometry.buildVertices(vba, geom.positions, geom.normals); + for (i in 0...verts) { + vba[i * 8 ] = (geom.positions[i * 4 ] / 32767) * md.scalePos; + vba[i * 8 + 1] = (geom.positions[i * 4 + 1] / 32767) * md.scalePos; + vba[i * 8 + 2] = (geom.positions[i * 4 + 2] / 32767) * md.scalePos; + vba[i * 8 + 3] = geom.normals [i * 2 ] / 32767; + vba[i * 8 + 4] = geom.normals [i * 2 + 1] / 32767; + vba[i * 8 + 5] = geom.positions[i * 4 + 3] / 32767; + vba[i * 8 + 6] = (geom.uvs[i * 2 ] / 32767) * md.scaleTex; + vba[i * 8 + 7] = (geom.uvs[i * 2 + 1] / 32767) * md.scaleTex; + } + vb.unlock(); + + var id = geom.indices[0]; + var ib = new IndexBuffer(id.length, kha.graphics5.Usage.StaticUsage); + var iba = ib.lock(); + for (i in 0...iba.length) iba[i] = id[i]; + ib.unlock(); + + accel = new AccelerationStructure(commandList, vb, ib); + + // Output + target = new RayTraceTarget(iron.App.w(), iron.App.h()); + }); + } + + public static function commands() { + if (!ready) return; + + var g = iron.App.framebuffer.g5; + currentBuffer = (currentBuffer + 1) % bufferCount; + + constantBuffer.lock(); + + var cam = iron.Scene.active.camera; + var ct = cam.transform; + var helpMat = iron.math.Mat4.identity(); + helpMat.setFrom(cam.V); + helpMat.multmat(cam.P); + helpMat.getInverse(helpMat); + constantBuffer.setFloat(0, ct.worldx()); + constantBuffer.setFloat(4, ct.worldy()); + constantBuffer.setFloat(8, ct.worldz()); + constantBuffer.setFloat(12, 1); + + constantBuffer.setFloat(16, helpMat._00); + constantBuffer.setFloat(20, helpMat._01); + constantBuffer.setFloat(24, helpMat._02); + constantBuffer.setFloat(28, helpMat._03); + constantBuffer.setFloat(32, helpMat._10); + constantBuffer.setFloat(36, helpMat._11); + constantBuffer.setFloat(40, helpMat._12); + constantBuffer.setFloat(44, helpMat._13); + constantBuffer.setFloat(48, helpMat._20); + constantBuffer.setFloat(52, helpMat._21); + constantBuffer.setFloat(56, helpMat._22); + constantBuffer.setFloat(60, helpMat._23); + constantBuffer.setFloat(64, helpMat._30); + constantBuffer.setFloat(68, helpMat._31); + constantBuffer.setFloat(72, helpMat._32); + constantBuffer.setFloat(76, helpMat._33); + + constantBuffer.setFloat(80, frame); + frame += 1.0; + constantBuffer.unlock(); + + g.begin(framebuffers[currentBuffer]); + + commandList.begin(); + + g.setAccelerationStructure(accel); + g.setRayTracePipeline(pipeline); + g.setRayTraceTarget(target); + + g.dispatchRays(commandList); + g.copyRayTraceTarget(commandList, framebuffers[currentBuffer], target); + commandList.end(); + + g.end(); + // g.swapBuffers(); + + if (iron.system.Input.getMouse().down()) frame = 1.0; + } + #end +} + +#end diff --git a/Sources/armory/renderpath/RenderToTexture.hx b/Sources/armory/renderpath/RenderToTexture.hx new file mode 100644 index 0000000000..8e19ee6867 --- /dev/null +++ b/Sources/armory/renderpath/RenderToTexture.hx @@ -0,0 +1,21 @@ +package armory.renderpath; + +class RenderToTexture{ + /** + The current kha g2 object to be rendered to. + **/ + public static var g: Null = null; + + public static inline function ensureEmptyRenderTarget(location: String){ + assert(Error, g == null, + 'render texture already exists at $location. Please clear the texture before setting. + If used in logic node, please consult its documentation.' + ); + } + + public static inline function ensure2DContext(location: String) { + assert(Error, g != null, + '$location must be executed inside of a render2D callback. If used in logic node, please consult its documentation.' + ); + } +} \ No newline at end of file diff --git a/Sources/armory/renderpath/Upsampler.hx b/Sources/armory/renderpath/Upsampler.hx new file mode 100644 index 0000000000..7d2cf349a2 --- /dev/null +++ b/Sources/armory/renderpath/Upsampler.hx @@ -0,0 +1,83 @@ +package armory.renderpath; + +import haxe.ds.ReadOnlyArray; + +import iron.RenderPath; +import iron.data.MaterialData; +import iron.object.Object; + +import armory.math.Helper; + +abstract class Upsampler { + + public static var currentMipLevel(default, null) = 0; + public static var numMipLevels(default, null) = 0; + + static var isRegistered = false; + + final path: RenderPath; + final shaderPassHandle: String; + final mipmaps: ReadOnlyArray; + + function new(path: RenderPath, shaderPassHandle: String, mipmaps: ReadOnlyArray) { + this.path = path; + this.shaderPassHandle = shaderPassHandle; + this.mipmaps = mipmaps; + } + + public static function create(path: RenderPath, shaderPassHandle: String, mipmaps: ReadOnlyArray): Upsampler { + if (!isRegistered) { + isRegistered = true; + iron.object.Uniforms.externalIntLinks.push(intLink); + } + + // TODO, see Downsampler.hx + // if (RenderPath.hasComputeSupport()) { + // return new UpsamplerCompute(path, shaderPassHandle, mipmaps); + // } + // else { + return new UpsamplerFragment(path, shaderPassHandle, mipmaps); + // } + } + + static function intLink(object: Object, mat: MaterialData, link: String): Null { + return switch (link) { + case "_upsampleCurrentMip": Upsampler.currentMipLevel; + case "_upsampleNumMips": Upsampler.numMipLevels; + default: null; + } + } + + abstract public function dispatch(dstImageName: String, numMips: Int = 0): Void; +} + +private class UpsamplerFragment extends Upsampler { + + public function new(path: RenderPath, shaderPassHandle: String, mipmaps: ReadOnlyArray) { + super(path, shaderPassHandle, mipmaps); + path.loadShader(shaderPassHandle); + } + + public function dispatch(dstImageName: String, numMips: Int = 0) { + Helper.clampInt(numMips, 0, mipmaps.length); + if (numMips == 0) { + numMips = mipmaps.length; + } + + final srcImageRT = path.renderTargets.get(dstImageName); + assert(Error, srcImageRT != null); + + final srcImage = srcImageRT.image; + assert(Error, srcImage != null); + + Upsampler.numMipLevels = numMips; + for (i in 0...Upsampler.numMipLevels) { + final mipLevel = Upsampler.numMipLevels - 1 - i; + Upsampler.currentMipLevel = mipLevel; + + path.setTarget(mipLevel == 0 ? dstImageName : mipmaps[mipLevel - 1].raw.name); + path.bindTarget(mipmaps[mipLevel].raw.name, "tex"); + path.drawShader(shaderPassHandle); + } + } +} diff --git a/Sources/armory/system/Assert.hx b/Sources/armory/system/Assert.hx new file mode 100644 index 0000000000..977fb7e959 --- /dev/null +++ b/Sources/armory/system/Assert.hx @@ -0,0 +1,131 @@ +package armory.system; + +import haxe.Exception; +import haxe.PosInfos; +import haxe.exceptions.PosException; +import haxe.macro.Context; +import haxe.macro.Expr; + +using haxe.macro.ExprTools; + +class Assert { + + /** + Checks whether the given expression evaluates to true. If this is not + the case, an `ArmAssertionException` is thrown or a warning is printed + (depending on the assertion level). + + The assert level describes the severity of the assertion. If the + severity is lower than the level stored in the `arm_assert_level` flag, + the assertion is omitted from the code so that it doesn't decrease the + runtime performance. + + @param level The severity of this assertion. + @param condition The conditional expression to test. + @param message Optional message to display when the assertion fails. + + @see `AssertLevel` + **/ + macro public static function assert(level: ExprOf, condition: ExprOf, ?message: ExprOf): Expr { + final levelVal: AssertLevel = AssertLevel.fromExpr(level); + final assertThreshold = AssertLevel.fromString(Context.definedValue("arm_assert_level")); + + if (levelVal < assertThreshold) { + return macro {}; + } + + switch (levelVal) { + case Warning: + return macro { + if (!$condition) { + @:pos(condition.pos) + trace(@:privateAccess armory.system.Assert.ArmAssertionException.formatMessage($v{condition.toString()}, ${message})); + } + } + case Error: + return macro { + if (!$condition) { + #if arm_assert_quit kha.System.stop(); #end + + @:pos(condition.pos) + @:privateAccess armory.system.Assert.throwAssertionError($v{condition.toString()}, ${message}); + } + } + default: + throw new Exception('Unsupported assert level: $levelVal'); + } + } + + /** + Helper function to prevent Haxe "bug" that actually throws an error + even when using `macro throw` (inlining this method also does not work). + **/ + static function throwAssertionError(exprString: String, message: String, ?pos: PosInfos) { + throw new ArmAssertionException(exprString, message, pos); + } +} + +/** + Exception that is thrown when an assertion fails. + + @see `Assert` +**/ +class ArmAssertionException extends PosException { + + /** + @param exprString The string representation of the failed assert condition. + @param message Custom error message, use `null` to omit this. + **/ + public inline function new(exprString: String, message: Null, ?previous: Exception, ?pos: Null) { + super('\n${formatMessage(exprString, message)}', previous, pos); + } + + static inline function formatMessage(exprString: String, message: Null): String { + final optMsg = message != null ? '\n\tMessage: $message' : ""; + + return 'Failed assertion:$optMsg\n\tExpression: ($exprString)'; + } +} + +enum abstract AssertLevel(Int) from Int to Int { + /** + Assertions with this severity don't throw exceptions and only print to + the console. + **/ + var Warning: AssertLevel = 0; + + /** + Assertions with this severity throw an `ArmAssertionException` if they + fail, and optionally quit the game if the `arm_assert_quit` flag is set. + **/ + var Error: AssertLevel = 1; + + /** + Completely disable assertions. Don't use this level in `assert()` calls! + **/ + var NoAssertions: AssertLevel = 2; + + public static function fromExpr(e: ExprOf): AssertLevel { + switch (e.expr) { + case EConst(CIdent(v)): return fromString(v); + default: throw new Exception('Unsupported expression: $e'); + }; + } + + /** + Converts a string into an `AssertLevel`, the string must be spelled + exactly as the assert level. `null` defaults to + `AssertLevel.NoAssertions`. + **/ + public static function fromString(s: Null): AssertLevel { + return switch (s) { + case "Warning": Warning; + case "Error": Error; + case "NoAssertions" | null: NoAssertions; + default: throw new Exception('Could not convert "$s" to AssertLevel'); + } + } + + @:op(A < B) static function lt(a: AssertLevel, b: AssertLevel): Bool; + @:op(A > B) static function gt(a: AssertLevel, b: AssertLevel): Bool; +} diff --git a/Sources/armory/system/Event.hx b/Sources/armory/system/Event.hx new file mode 100644 index 0000000000..ee81a7b81d --- /dev/null +++ b/Sources/armory/system/Event.hx @@ -0,0 +1,83 @@ +package armory.system; + +/** + Detailed documentation of the event system: + [Armory Wiki: Events](https://github.com/armory3d/armory/wiki/events). +**/ +class Event { + + static var events = new Map>(); + + /** + Send an event with the given name to all corresponding listeners. This + function directly executes the `onEvent` callbacks of those listeners. + + For an explanation of the `mask` value, please refer to the + [wiki](https://github.com/armory3d/armory/wiki/events#event-masks). + **/ + public static function send(name: String, mask = -1) { + var entries = get(name); + if (entries != null) for (e in entries) if (mask == -1 || mask == e.mask ) e.onEvent(); + } + + /** + Return the array of event listeners registered for events with the + given name, or `null` if no listener is currently registered for the event. + **/ + public static function get(name: String): Array { + return events.get(name); + } + + /** + Add a listener to the event with the given name and return the + corresponding listener object. The `onEvent` callback will be called + when a matching event is sent. + + For an explanation of the `mask` value, please refer to the + [wiki](https://github.com/armory3d/armory/wiki/events#event-masks). + **/ + public static function add(name: String, onEvent: Void->Void, mask = -1): TEvent { + var e: TEvent = { name: name, onEvent: onEvent, mask: mask }; + var entries = events.get(name); + if (entries != null) entries.push(e); + else events.set(name, [e]); + return e; + } + + /** + Remove _all_ listeners that listen to events with the given `name`. + **/ + public static function remove(name: String) { + events.remove(name); + } + + /** + Remove a specific listener. If the listener is not registered/added, + this function does nothing. + **/ + public static function removeListener(event: TEvent) { + var entries = events.get(event.name); + if (entries != null) { + entries.remove(event); + if (entries.length == 0) { + events.remove(event.name); + } + } + } +} + +/** + Represents an event listener. + + @see `armory.system.Event` +**/ +typedef TEvent = { + /** The name of the events this listener is listening to. **/ + var name: String; + + /** The callback function that is called when a matching event is sent. **/ + var onEvent: Void->Void; + + /** The mask of the events this listener is listening to. **/ + var mask: Int; +} diff --git a/Sources/armory/system/FSM.hx b/Sources/armory/system/FSM.hx new file mode 100644 index 0000000000..f703aea7ce --- /dev/null +++ b/Sources/armory/system/FSM.hx @@ -0,0 +1,74 @@ +package armory.system; + +class FSM { + final transitions = new Array>(); + final tempTransitions = new Array>(); + var state: Null>; + var entered = false; + + public function new() {} + + public function bindTransition(canEnter: Void -> Bool, fromState: State, toState: State) { + final transition = new Transition(canEnter, fromState, toState); + transitions.push(transition); + syncTransitions(); + } + + public function setInitState(state: State) { + this.state = state; + syncTransitions(); + } + + public function update() { + if (!entered) { + state.onEnter(); + entered = true; + } + + state.onUpdate(); + + for (transition in tempTransitions) { + if (transition.canEnter()) { + state.onExit(); + state = transition.toState; + entered = false; + syncTransitions(); + break; + } + } + } + + public function syncTransitions() { + tempTransitions.resize(0); + + for (transition in transitions) { + if (transition.fromState == state) tempTransitions.push(transition); + } + } +} + +class Transition { + public final canEnter: Void -> Bool; + public final fromState: State; + public final toState: State; + + public function new(canEnter: Void -> Bool, fromState: State, toState: State) { + this.canEnter = canEnter; + this.fromState = fromState; + this.toState = toState; + } +} + +class State { + final owner: T; + + public function new(owner: T) { + this.owner = owner; + } + + public function onEnter() {} + + public function onUpdate() {} + + public function onExit() {} +} \ No newline at end of file diff --git a/Sources/armory/system/InputMap.hx b/Sources/armory/system/InputMap.hx new file mode 100644 index 0000000000..2bd2b4930f --- /dev/null +++ b/Sources/armory/system/InputMap.hx @@ -0,0 +1,226 @@ +package armory.system; + +import kha.FastFloat; +import iron.system.Input; + +class InputMap { + + static var inputMaps = new Map(); + + public var keys(default, null) = new Array(); + public var lastKeyPressed(default, null) = ""; + + public function new() {} + + public static function getInputMap(inputMap: String): Null { + if (inputMaps.exists(inputMap)) { + return inputMaps[inputMap]; + } + + return null; + } + + public static function addInputMap(inputMap: String): InputMap { + return inputMaps[inputMap] = new InputMap(); + } + + 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 null; + } + + public static function removeInputMapKey(inputMap: String, key: String): Bool { + if (inputMaps.exists(inputMap)) { + var i = inputMaps[inputMap]; + + for (k in i.keys) { + if (k.key == key) { + return i.removeKey(k); + } + } + } + + return false; + } + + public function addKeyboard(key: String, scale: FastFloat = 1.0): InputMapKey { + return addKey(new KeyboardKey(key, scale)); + } + + public function addMouse(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey { + return addKey(new MouseKey(key, scale, deadzone)); + } + + public function addGamepad(key: String, scale: FastFloat = 1.0, deadzone: FastFloat = 0.0): InputMapKey { + return addKey(new GamepadKey(key, scale, deadzone)); + } + + 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 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 released(): Bool { + return false; + } + + public function value(): FastFloat { + return 0.0; + } + + public function setIndex(index: Int) {} + + function evalDeadzone(value: FastFloat): FastFloat { + var v = 0.0; + + if (value > deadzone) { + v = value - deadzone; + + } else if (value < -deadzone) { + v = value + deadzone; + } + + return v * scale; + } + + function evalPressure(value: FastFloat): FastFloat { + var v = value - deadzone; + + if (v > 0.0) { + v /= (1.0 - deadzone); + + } else { + v = 0.0; + } + + return v; + } +} + +class KeyboardKey extends InputMapKey { + + var kb = Input.getKeyboard(); + + public inline override function started() { + return kb.started(key); + } + + public inline override function released() { + return kb.released(key); + } + + public inline override function value(): FastFloat { + return kb.down(key) ? scale : 0.0; + } +} + +class MouseKey extends InputMapKey { + + var m = Input.getMouse(); + + public inline override function started() { + return m.started(key); + } + + public inline override function released() { + return m.released(key); + } + + public override function value(): FastFloat { + return switch (key) { + case "movement x": evalDeadzone(m.movementX); + case "movement y": evalDeadzone(m.movementY); + case "wheel": evalDeadzone(m.wheelDelta); + default: m.down(key) ? scale : 0.0; + } + } +} + +class GamepadKey extends InputMapKey { + + var g = Input.getGamepad(); + + public inline override function started() { + return g.started(key); + } + + public inline override function released() { + return g.released(key); + } + + public override function value(): FastFloat { + return switch(key) { + case "ls movement x": evalDeadzone(g.leftStick.movementX); + case "ls movement y": evalDeadzone(g.leftStick.movementY); + case "rs movement x": evalDeadzone(g.rightStick.movementX); + case "rs movement y": evalDeadzone(g.rightStick.movementY); + case "lt pressure": evalDeadzone(evalPressure(g.down("l2"))); + case "rt pressure": evalDeadzone(evalPressure(g.down("r2"))); + default: evalDeadzone(g.down(key)); + } + } + + public override function setIndex(index: Int) { + g = Input.getGamepad(index); + } +} diff --git a/Sources/armory/system/Logic.hx b/Sources/armory/system/Logic.hx new file mode 100644 index 0000000000..e71390faaa --- /dev/null +++ b/Sources/armory/system/Logic.hx @@ -0,0 +1,298 @@ +package armory.system; + +import armory.logicnode.*; + +class Logic { + + static var nodes: Array; + static var links: Array; + + static var parsed_nodes: Array = null; + static var parsed_labels: Map = null; + static var nodeMap: Map; + + public static var packageName = "armory.logicnode"; + + public static function getNode(id: Int): TNode { + for (n in nodes) if (n.id == id) return n; + return null; + } + + public static function getLink(id: Int): TNodeLink { + for (l in links) if (l.id == id) return l; + return null; + } + + public static function getInputLink(inp: TNodeSocket): TNodeLink { + for (l in links) { + if (l.to_id == inp.node_id) { + var node = getNode(inp.node_id); + if (node.inputs.length <= l.to_socket) return null; + if (node.inputs[l.to_socket] == inp) return l; + } + } + return null; + } + + public static function getOutputLinks(out: TNodeSocket): Array { + var res: Array = []; + for (l in links) { + if (l.from_id == out.node_id) { + var node = getNode(out.node_id); + if (node.outputs.length <= l.from_socket) continue; + if (node.outputs[l.from_socket] == out) res.push(l); + } + } + return res; + } + + static function safesrc(s: String): String { + return StringTools.replace(s, " ", ""); + } + + static function node_name(node: TNode): String { + var s = safesrc(node.name) + node.id; + return s; + } + + static var tree: armory.logicnode.LogicTree; + public static function parse(canvas: TNodeCanvas, onAdd = true): armory.logicnode.LogicTree { + + nodes = canvas.nodes; + links = canvas.links; + + parsed_nodes = []; + parsed_labels = new Map(); + nodeMap = new Map(); + var root_nodes = get_root_nodes(canvas); + + tree = new armory.logicnode.LogicTree(); + if (onAdd) { + tree.notifyOnAdd(function() { + for (node in root_nodes) build_node(node); + }); + } + else { + for (node in root_nodes) build_node(node); + } + return tree; + } + + static function build_node(node: TNode): String { + + // Get node name + var name = node_name(node); + + // Check if node already exists + if (parsed_nodes.indexOf(name) != -1) { + return name; + } + + parsed_nodes.push(name); + + // Create node + var v = createClassInstance(node.type, [tree]); + nodeMap.set(name, v); + + #if arm_patch + tree.nodes.set(name, v); + #end + + // Properties + for (i in 0...5) { + for (b in node.buttons) { + if (b.name == "property" + i) { + Reflect.setProperty(v, b.name, b.data[b.default_value]); + } + } + } + + @:privateAccess v.preallocInputs(node.inputs.length); + @:privateAccess v.preallocOutputs(node.outputs.length); + + // Create inputs + var inp_node: armory.logicnode.LogicNode = null; + var inp_from = 0; + var from_type: String; + for (i in 0...node.inputs.length) { + var inp = node.inputs[i]; + // Is linked - find node + var l = getInputLink(inp); + if (l != null) { + var n = getNode(l.from_id); + var socket = n.outputs[l.from_socket]; + inp_node = nodeMap.get(build_node(n)); + for (i in 0...n.outputs.length) { + if (n.outputs[i] == socket) { + inp_from = i; + from_type = socket.type; + break; + } + } + } + else { // Not linked - create node with default values + inp_node = build_default_node(inp); + inp_from = 0; + from_type = inp.type; + } + // Add input + var link = LogicNode.addLink(inp_node, v, inp_from, i); + #if arm_patch + link.fromType = from_type; + link.toType = inp.type; + link.toValue = getSocketDefaultValue(inp); + #end + } + + // Create outputs + for (i in 0...node.outputs.length) { + var out = node.outputs[i]; + var ls = getOutputLinks(out); + + // Linked outputs are already handled after iterating over inputs + // above, so only unconnected outputs are handled here + if (ls == null || ls.length == 0) { + var link = LogicNode.addLink(v, build_default_node(out), i, 0); + + #if arm_patch + link.fromType = out.type; + link.toType = out.type; + link.toValue = getSocketDefaultValue(out); + #end + } + } + + return name; + } + + static function get_root_nodes(node_group: TNodeCanvas): Array { + var roots: Array = []; + for (node in node_group.nodes) { + // if (node.bl_idname == 'NodeUndefined') { + // arm.log.warn('Undefined logic nodes in ' + node_group.name) + // return [] + // } + var linked = false; + for (out in node.outputs) { + var ls = getOutputLinks(out); + if (ls != null && ls.length > 0) { + linked = true; + break; + } + } + if (!linked) { // Assume node with no connected outputs as roots + roots.push(node); + } + } + return roots; + } + + static function build_default_node(inp: TNodeSocket): armory.logicnode.LogicNode { + + var v: armory.logicnode.LogicNode = null; + + if (inp.type == "OBJECT") { + v = createClassInstance("ObjectNode", [tree, inp.default_value]); + } + else if (inp.type == "ANIMACTION") { + v = createClassInstance("StringNode", [tree, inp.default_value]); + } + else if (inp.type == "VECTOR") { + if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO + v = createClassInstance("VectorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2]]); + } + else if (inp.type == "RGBA") { + if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO + v = createClassInstance("ColorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2], inp.default_value[3]]); + } + else if (inp.type == "RGB") { + if (inp.default_value == null) inp.default_value = [0, 0, 0]; // TODO + v = createClassInstance("ColorNode", [tree, inp.default_value[0], inp.default_value[1], inp.default_value[2]]); + } + else if (inp.type == "VALUE") { + v = createClassInstance("FloatNode", [tree, inp.default_value]); + } + else if (inp.type == "INT") { + v = createClassInstance("IntegerNode", [tree, inp.default_value]); + } + else if (inp.type == "BOOLEAN") { + v = createClassInstance("BooleanNode", [tree, inp.default_value]); + } + else if (inp.type == "STRING") { + v = createClassInstance("StringNode", [tree, inp.default_value]); + } + else { // ACTION, ARRAY + v = createClassInstance("NullNode", [tree]); + } + return v; + } + + static function getSocketDefaultValue(socket: TNodeSocket): Any { + + var v: armory.logicnode.LogicNode = null; + + return switch (socket.type) { + case "OBJECT" | "VALUE" | "INT" | "BOOLEAN" | "STRING": + socket.default_value; + case "VECTOR" | "RGB": + socket.default_value == null ? [0, 0, 0] : [socket.default_value[0], socket.default_value[1], socket.default_value[2]]; + case "RGBA": + socket.default_value == null ? [0, 0, 0, 1] : [socket.default_value[0], socket.default_value[1], socket.default_value[2], socket.default_value[3]]; + default: + null; + } + } + + static function createClassInstance(className: String, args: Array): Dynamic { + var cname = Type.resolveClass(packageName + "." + className); + if (cname == null) return null; + return Type.createInstance(cname, args); + } +} + +typedef TNodeCanvas = { + var name: String; + var nodes: Array; + var links: Array; +} + +typedef TNode = { + var id: Int; + var name: String; + var type: String; + var x: Float; + var y: Float; + var inputs: Array; + var outputs: Array; + var buttons: Array; + var color: Int; +} + +typedef TNodeSocket = { + var id: Int; + var node_id: Int; + var name: String; + var type: String; + var color: Int; + var default_value: Dynamic; + @:optional var min: Null; + @:optional var max: Null; +} + +typedef TNodeLink = { + var id: Int; + var from_id: Int; + var from_socket: Int; + var to_id: Int; + var to_socket: Int; +} + +typedef TNodeButton = { + var name: String; + var type: String; + @:optional var output: Null; + @:optional var default_value: Dynamic; + @:optional var data: Dynamic; + @:optional var min: Null; + @:optional var max: Null; +} diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx new file mode 100644 index 0000000000..d97ab0708c --- /dev/null +++ b/Sources/armory/system/Starter.hx @@ -0,0 +1,128 @@ +package armory.system; + +import kha.WindowOptions; + +class Starter { + + #if arm_loadscreen + public static var drawLoading: kha.graphics2.Graphics->Int->Int->Void = null; + public static var numAssets: Int; + #end + + public static function main(scene: String, mode: Int, resize: Bool, min: Bool, max: Bool, w: Int, h: Int, msaa: Int, vsync: Bool, getRenderPath: Void->iron.RenderPath) { + + var tasks = 0; + + function start() { + if (tasks > 0) return; + + if (armory.data.Config.raw == null) armory.data.Config.raw = {}; + var c = armory.data.Config.raw; + + if (c.window_mode == null) c.window_mode = mode; + if (c.window_resizable == null) c.window_resizable = resize; + if (c.window_minimizable == null) c.window_minimizable = min; + if (c.window_maximizable == null) c.window_maximizable = max; + if (c.window_w == null) c.window_w = w; + if (c.window_h == null) c.window_h = h; + if (c.window_scale == null) c.window_scale = 1.0; + if (c.window_msaa == null) c.window_msaa = msaa; + if (c.window_vsync == null) c.window_vsync = vsync; + + armory.object.Uniforms.register(); + + var windowMode = c.window_mode == 0 ? kha.WindowMode.Windowed : kha.WindowMode.Fullscreen; + var windowFeatures = None; + if (c.window_resizable) windowFeatures |= FeatureResizable; + if (c.window_maximizable) windowFeatures |= FeatureMaximizable; + if (c.window_minimizable) windowFeatures |= FeatureMinimizable; + + #if (kha_webgl && (!arm_legacy) && (!kha_node)) + try { + #end + + kha.System.start({title: Main.projectName, width: c.window_w, height: c.window_h, window: {mode: windowMode, windowFeatures: windowFeatures}, framebuffer: {samplesPerPixel: c.window_msaa, verticalSync: c.window_vsync}}, function(window: kha.Window) { + + iron.App.init(function() { + #if arm_loadscreen + function load(g: kha.graphics2.Graphics) { + if (iron.Scene.active != null && iron.Scene.active.ready) iron.App.removeRender2D(load); + else drawLoading(g, iron.data.Data.assetsLoaded, numAssets); + } + iron.App.notifyOnRender2D(load); + #end + iron.Scene.setActive(scene, function(object: iron.object.Object) { + iron.RenderPath.setActive(getRenderPath()); + #if arm_patch + iron.Scene.getRenderPath = getRenderPath; + #end + #if arm_draworder_shader + iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Shader; + #end // else Distance + }); + }); + }); + + #if (kha_webgl && (!arm_legacy) && (!kha_node)) + } + catch (e: Dynamic) { + if (!kha.SystemImpl.gl2) { + trace("This project was not compiled with legacy shaders flag - please use WebGL 2 capable browser."); + } + } + #end + } + + #if (js && arm_bullet) + function loadLibAmmo(name: String) { + kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { + js.Syntax.code("(1,eval)({0})", b.toString()); + #if kha_krom + js.Syntax.code("Ammo({print:function(s){iron.log(s);},instantiateWasm:function(imports,successCallback) { + var wasmbin = Krom.loadBlob('ammo.wasm.wasm'); + var module = new WebAssembly.Module(wasmbin); + var inst = new WebAssembly.Instance(module,imports); + successCallback(inst); + return inst.exports; + }}).then(function(){ tasks--; start();})"); + #else + js.Syntax.code("Ammo({print:function(s){iron.log(s);}}).then(function(){ tasks--; start();})"); + #end + }); + } + #end + + #if (js && arm_navigation) + function loadLib(name: String) { + kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { + js.Syntax.code("(1, eval)({0})", b.toString()); + tasks--; + start(); + }); + } + #end + + tasks = 1; + + #if (js && arm_bullet) + tasks++; + #if kha_krom + loadLibAmmo("ammo.wasm.js"); + #else + loadLibAmmo("ammo.js"); + #end + #end + + #if (js && arm_navigation) + tasks++; + loadLib("recast.js"); + #end + + #if (arm_config) + tasks++; + armory.data.Config.load(function() { tasks--; start(); }); + #end + + tasks--; start(); + } +} diff --git a/Sources/armory/trait/ArcBall.hx b/Sources/armory/trait/ArcBall.hx new file mode 100644 index 0000000000..7f4edad535 --- /dev/null +++ b/Sources/armory/trait/ArcBall.hx @@ -0,0 +1,27 @@ +package armory.trait; + +import iron.Trait; +import iron.system.Input; +import iron.math.Vec4; + +class ArcBall extends Trait { + + @prop + public var axis = new Vec4(0, 0, 1); + + public function new() { + super(); + + notifyOnUpdate(update); + } + + function update() { + if (Input.occupied) return; + + var mouse = Input.getMouse(); + if (mouse.down()) { + object.transform.rotate(axis, -mouse.movementX / 100); + object.transform.rotate(object.transform.world.right(), -mouse.movementY / 100); + } + } +} diff --git a/Sources/armory/trait/Character.hx b/Sources/armory/trait/Character.hx new file mode 100644 index 0000000000..6ad08fb0a1 --- /dev/null +++ b/Sources/armory/trait/Character.hx @@ -0,0 +1,77 @@ +package armory.trait; + +import iron.object.Object; +import iron.object.Animation; +import iron.Trait; +import iron.system.Input; +import iron.math.Vec4; +import iron.math.Quat; + +class Character extends Trait { + + var currentAction: String; + var animation: Animation; + + var speed = 0.0; + var loc: Vec4 = new Vec4(); + var lastLoc: Vec4 = null; + var framesIdle = 0; // Number of frames character did not move + + @:prop + var actionIdle: String = "idle"; + + @:prop + var actionMove: String = "run"; + + public function new() { + super(); + + currentAction = actionIdle; + notifyOnInit(init); + } + + function init() { + animation = object.animation; + + // Try first child if we are running from armature + if (animation == null) { + if (object.children.length > 0) { + animation = object.children[0].animation; + } + } + + if (animation == null) return; + notifyOnUpdate(update); + } + + function update() { + // Get current position + var tr = object.transform; + loc.set(tr.worldx(), tr.worldy(), tr.worldz()); + + // Set previous position to current position if there is no previous position + if (lastLoc == null) lastLoc = new Vec4(loc.x, loc.y, loc.z); + + // Check if character moved compared from last position + speed = Vec4.distance(loc, lastLoc); + + // Update previous position to current position + // in preparation for next check + lastLoc.setFrom(loc); + + if (speed == 0) framesIdle++; + else framesIdle = 0; + + // If state is idle and character is in movement, play move walk animation + if (currentAction == actionIdle && framesIdle == 0) { + currentAction = actionMove; + + if (actionMove != null) animation.play(actionMove); + } + else if (currentAction == actionMove && framesIdle > 2) { // Otherwise if state is walking and character is idle, play idle animation + currentAction = actionIdle; + + if (actionIdle != null) animation.play(actionIdle); + } + } +} diff --git a/Sources/armory/trait/CustomParticle.hx b/Sources/armory/trait/CustomParticle.hx new file mode 100644 index 0000000000..457d344425 --- /dev/null +++ b/Sources/armory/trait/CustomParticle.hx @@ -0,0 +1,40 @@ +package armory.trait; + +import iron.object.MeshObject; + +/** + * Trait to enable GPU instancing of Mesh objects + */ +class CustomParticle extends iron.Trait { + + @prop + var ParticleCount: Int = 100; + + public function new() { + super(); + + notifyOnInit(function() { + var partCount = ParticleCount; + setupSimpleInstanced(partCount); + }); + } + + function setupSimpleInstanced(count: Int){ + if(object.raw.type == 'mesh_object') + { + var meshObjGeom = cast(object, MeshObject).data.geom; + meshObjGeom.instanced = true; + meshObjGeom.instanceCount = count; + } + } + + public function updateParticleCount(count: Int){ + + if(object.raw.type == 'mesh_object') + { + var meshObjGeom = cast(object, MeshObject).data.geom; + meshObjGeom.instanced = true; + meshObjGeom.instanceCount = count; + } + } +} diff --git a/Sources/armory/trait/FirstPersonController.hx b/Sources/armory/trait/FirstPersonController.hx new file mode 100644 index 0000000000..99bc5da18c --- /dev/null +++ b/Sources/armory/trait/FirstPersonController.hx @@ -0,0 +1,87 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.system.Input; +import iron.object.Object; +import iron.object.CameraObject; +import armory.trait.physics.PhysicsWorld; +import armory.trait.internal.CameraController; + +class FirstPersonController extends CameraController { + +#if (!arm_physics) + public function new() { super(); } +#else + + var head: Object; + static inline var rotationSpeed = 2.0; + + public function new() { + super(); + + iron.Scene.active.notifyOnInit(init); + } + + function init() { + head = object.getChildOfType(CameraObject); + + PhysicsWorld.active.notifyOnPreUpdate(preUpdate); + notifyOnUpdate(update); + notifyOnRemove(removed); + } + + var xVec = Vec4.xAxis(); + var zVec = Vec4.zAxis(); + function preUpdate() { + if (Input.occupied || !body.ready) return; + + var mouse = Input.getMouse(); + var kb = Input.getKeyboard(); + + if (mouse.started() && !mouse.locked) mouse.lock(); + else if (kb.started("escape") && mouse.locked) mouse.unlock(); + + if (mouse.locked || mouse.down()) { + head.transform.rotate(xVec, -mouse.movementY / 250 * rotationSpeed); + transform.rotate(zVec, -mouse.movementX / 250 * rotationSpeed); + body.syncTransform(); + } + } + + function removed() { + PhysicsWorld.active.removePreUpdate(preUpdate); + } + + var dir = new Vec4(); + function update() { + if (!body.ready) return; + + if (jump) { + body.applyImpulse(new Vec4(0, 0, 16)); + jump = false; + } + + // Move + dir.set(0, 0, 0); + if (moveForward) dir.add(transform.look()); + if (moveBackward) dir.add(transform.look().mult(-1)); + if (moveLeft) dir.add(transform.right().mult(-1)); + if (moveRight) dir.add(transform.right()); + + // Push down + var btvec = body.getLinearVelocity(); + body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); + + if (moveForward || moveBackward || moveLeft || moveRight) { + var dirN = dir.normalize(); + dirN.mult(6); + body.activate(); + body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0); + } + + // Keep vertical + body.setAngularFactor(0, 0, 0); + camera.buildMatrix(); + } +#end +} diff --git a/Sources/armory/trait/FollowCamera.hx b/Sources/armory/trait/FollowCamera.hx new file mode 100644 index 0000000000..99df3c702e --- /dev/null +++ b/Sources/armory/trait/FollowCamera.hx @@ -0,0 +1,59 @@ +package armory.trait; + +import iron.Scene; +import iron.object.Object; + +/** + This trait is to be used with a camera mounted on a camera boom with offset. + 1. Place the camera as a child to another object, for example an 'Empty'. + 2. Place this trait on the 'Empty' object. + 3. Set the name of the target object to be followed by the camera. +**/ +class FollowCamera extends iron.Trait { + + @prop + var target: String; + + @prop + var lerp: Bool = true; + + @prop + var lerpSpeed: Float = 0.1; + + var targetObj: Object; + var disabled = false; + + public function new() { + super(); + + notifyOnInit(function() { + targetObj = Scene.active.getChild(target); + if (targetObj == null) { + disabled = true; + trace("FollowCamera error, unable to set target object"); + } + + 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."); + } + }); + + notifyOnLateUpdate(function() { + if (!disabled) { + if (targetObj != null) { + if (lerp) { + object.transform.loc.lerp(object.transform.world.getLoc(), targetObj.transform.world.getLoc(), lerpSpeed); + } + else { + object.transform.loc = targetObj.transform.world.getLoc(); + } + object.transform.buildMatrix(); + } + else { + targetObj = Scene.active.getChild(target); + } + } + }); + } +} diff --git a/Sources/armory/trait/NavAgent.hx b/Sources/armory/trait/NavAgent.hx new file mode 100644 index 0000000000..e915c81591 --- /dev/null +++ b/Sources/armory/trait/NavAgent.hx @@ -0,0 +1,81 @@ +package armory.trait; + +import iron.Trait; +import iron.math.Vec4; +import iron.math.Quat; +import iron.system.Tween; + +class NavAgent extends Trait { + + @prop + public var speed: Float = 5; + @prop + public var turnDuration: Float = 0.4; + @prop + public var heightOffset: Float = 0.0; + + var path: Array = null; + var index = 0; + + var rotAnim: TAnim = null; + var locAnim: TAnim = null; + + public var tickPos: Null Void>; + public var tickRot: Null Void>; + + public function new() { + super(); + notifyOnRemove(stopTween); + } + + public function setPath(path: Array) { + stopTween(); + + this.path = path; + index = 1; + notifyOnUpdate(update); + + go(); + } + + function stopTween() { + if (rotAnim != null) Tween.stop(rotAnim); + if (locAnim != null) Tween.stop(locAnim); + } + + public function stop() { + stopTween(); + path = null; + } + + function shortAngle(from: Float, to: Float): Float { + if (from < 0) from += Math.PI * 2; + if (to < 0) to += Math.PI * 2; + var delta = Math.abs(from - to); + if (delta > Math.PI) to = Math.PI * 2 - delta; + return to; + } + + var orient = new Vec4(); + function go() { + if (path == null || index >= path.length) return; + + var p = path[index]; + var dist = Vec4.distance(object.transform.loc, p); + + orient.subvecs(p, object.transform.loc).normalize; + var targetAngle = Math.atan2(orient.y, orient.x) + Math.PI / 2; + locAnim = Tween.to({ target: object.transform.loc, props: { x: p.x, y: p.y, z: p.z + heightOffset }, duration: dist / speed, tick: tickPos, done: function() { + index++; + if (index < path.length) go(); + else removeUpdate(update); + }}); + + var q = new Quat(); + rotAnim = Tween.to({ target: object.transform, props: { rot: q.fromEuler(0, 0, targetAngle) }, tick: tickRot, duration: turnDuration}); + } + + function update() { + object.transform.buildMatrix(); + } +} diff --git a/Sources/armory/trait/NavCrowd.hx b/Sources/armory/trait/NavCrowd.hx new file mode 100644 index 0000000000..b074971e35 --- /dev/null +++ b/Sources/armory/trait/NavCrowd.hx @@ -0,0 +1,10 @@ +package armory.trait; + +import iron.Trait; + +class NavCrowd extends Trait { + + public function new() { + super(); + } +} diff --git a/Sources/armory/trait/NavMesh.hx b/Sources/armory/trait/NavMesh.hx new file mode 100644 index 0000000000..421e5504c6 --- /dev/null +++ b/Sources/armory/trait/NavMesh.hx @@ -0,0 +1,83 @@ +package armory.trait; + +#if arm_navigation +import armory.trait.navigation.Navigation; +import haxerecast.Recast; +#end + +import iron.Trait; +import iron.data.Data; +import iron.math.Vec4; + +class NavMesh extends Trait { + +#if (!arm_navigation) + public function new() { super(); } +#else + + // recast config: + @prop + public var cellSize: Float = 0.3; // voxelization cell size + @prop + public var cellHeight: Float = 0.2; // voxelization cell height + @prop + public var agentHeight: Float = 2.0; // agent capsule height + @prop + public var agentRadius: Float = 0.4; // agent capsule radius + @prop + public var agentMaxClimb: Float = 0.9; // how high steps agents can climb, in voxels + @prop + public var agentMaxSlope: Float = 45.0; // maximum slope angle, in degrees + + var recast: Recast = null; + var ready = false; + + public function new() { + super(); + + notifyOnInit(init); + } + + function init() { + Navigation.active.navMeshes.push(this); + + // Load navmesh + var name = "nav_" + cast(object, iron.object.MeshObject).data.name + ".arm"; + Data.getBlob(name, function(b: kha.Blob) { + + recast = Navigation.active.recast; + recast.OBJDataLoader(b.toString(), function() { + var settings = [ + "cellSize" => cellSize, + "cellHeight" => cellHeight, + "agentHeight" => agentHeight, + "agentRadius" => agentRadius, + "agentMaxClimb" => agentMaxClimb, + "agentMaxSlope" => agentMaxSlope, + ]; + recast.settings(settings); + + recast.buildSolo(); + ready = true; + }); + }); + } + + public function findPath(from: Vec4, to: Vec4, done: Array->Void) { + if (!ready) return; + recast.findPath(from.x, from.z, from.y, to.x, to.z, to.y, 200, function(path: Array) { + var ar: Array = []; + for (p in path) ar.push(new Vec4(p.x, p.z, p.y - cellHeight)); + done(ar); + }); + } + + public function getRandomPoint(done: Vec4->Void) { + if (!ready) return; + recast.getRandomPoint(function(x: Float, y: Float, z: Float) { + done(new Vec4(x, z, -y)); + }); + } + +#end +} diff --git a/Sources/armory/trait/PhysicsBreak.hx b/Sources/armory/trait/PhysicsBreak.hx new file mode 100644 index 0000000000..f0f8a1a830 --- /dev/null +++ b/Sources/armory/trait/PhysicsBreak.hx @@ -0,0 +1,812 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.math.Mat4; +import iron.Trait; +import iron.object.MeshObject; +import iron.data.MeshData; +import iron.data.SceneFormat; +#if arm_bullet +import armory.trait.physics.bullet.RigidBody; +import armory.trait.physics.PhysicsWorld; +#end + +class PhysicsBreak extends Trait { + +#if (!arm_bullet) + public function new() { super(); } +#else + + static var physics: PhysicsWorld = null; + static var breaker: ConvexBreaker = null; + + var body: RigidBody; + + public function new() { + super(); + + if (breaker == null) breaker = new ConvexBreaker(); + notifyOnInit(init); + } + + function init() { + if (physics == null) physics = armory.trait.physics.PhysicsWorld.active; + + body = object.getTrait(RigidBody); + breaker.initBreakableObject(cast object, body.mass, body.friction, new Vec4(), new Vec4(), true); + + notifyOnUpdate(update); + } + + function update() { + var ar = physics.getContactPairs(body); + if (ar != null) { + var maxImpulse = 0.0; + var impactPoint: Vec4 = null; + var impactNormal: Vec4 = null; + for (p in ar) { + if (maxImpulse < p.impulse) { + maxImpulse = p.impulse; + impactPoint = p.posB; + impactNormal = p.normOnB; + } + } + + var fractureImpulse = 4.0; + if (maxImpulse > fractureImpulse) { + var radialIter = 1; + var randIter = 1; + var debris = breaker.subdivideByImpact(cast object, impactPoint, impactNormal, radialIter, randIter); + // var numObjects = debris.length; + for (o in debris) { + var ud = breaker.userDataMap.get(cast o); + var params: RigidBodyParams = { + linearDamping: 0.04, + angularDamping: 0.1, + angularFriction: 0.1, + linearFactorsX: 1.0, + linearFactorsY: 1.0, + linearFactorsZ: 1.0, + angularFactorsX: 1.0, + angularFactorsY: 1.0, + angularFactorsZ: 1.0, + collisionMargin: 0.04, + linearDeactivationThreshold: 0.0, + angularDeactivationThrshold: 0.0, + deactivationTime: 0.0 + }; + o.addTrait(new RigidBody(Shape.ConvexHull, ud.mass, ud.friction, 0, 1, params)); + if (cast(o, MeshObject).data.geom.positions.values.length < 600) { + o.addTrait(new PhysicsBreak()); + } + } + object.remove(); + } + } + } + +#end +} + +// Based on work by yomboprime https://github.com/yomboprime +// This class can be used to subdivide a convex geometry object into pieces +class ConvexBreaker { + + var minSizeForBreak: Float; + var smallDelta: Float; + + var tempLine: Line3; + var tempPlane: Plane; + var tempPlane2: Plane; + var tempCM1: Vec4; + var tempCM2: Vec4; + var tempVec4: Vec4; + var tempVec42: Vec4; + var tempVec43: Vec4; + var tempCutResult: CutResult; + var segments: Array; + + public var userDataMap: Map; + + // minSizeForBreak Min size a debris can have to break + // smallDelta Max distance to consider that a point belongs to a plane + public function new(minSizeForBreak = 1.4, smallDelta = 0.0001) { + this.minSizeForBreak = minSizeForBreak; + this.smallDelta = smallDelta; + tempLine = new Line3(); + tempPlane = new Plane(); + tempPlane2 = new Plane(); + tempCM1 = new Vec4(); + tempCM2 = new Vec4(); + tempVec4 = new Vec4(); + tempVec42 = new Vec4(); + tempVec43 = new Vec4(); + tempCutResult = new CutResult(); + segments = new Array(); + var n = 30 * 30; + for (i in 0...n) segments.push(false); + userDataMap = new Map(); + } + + public function initBreakableObject(object: MeshObject, mass: Float, friction: Float, velocity: Vec4, angularVelocity: Vec4, breakable: Bool) { + var ar = object.data.geom.positions.values; + var scalePos = object.data.scalePos; + // Create vertices mark + var sc = object.transform.scale; + var vertices = new Array(); + for (i in 0...Std.int(ar.length / 4)) { + // Use w component as mark + vertices.push(new Vec4( + ar[i * 4 ] * sc.x * (1 / 32767) * scalePos, + ar[i * 4 + 1] * sc.y * (1 / 32767) * scalePos, + ar[i * 4 + 2] * sc.z * (1 / 32767) * scalePos, + 0 + )); + } + + var ind = object.data.geom.indices[0]; + var faces = new Array(); + for (i in 0...Std.int(ind.length / 3)) { + var a = ind[i * 3]; + var b = ind[i * 3 + 1]; + var c = ind[i * 3 + 2]; + // Merge duplis + for (f in faces) { + if (vertices[a].equals(vertices[f.a])) a = f.a; + else if (vertices[a].equals(vertices[f.b])) a = f.b; + else if (vertices[a].equals(vertices[f.c])) a = f.c; + if (vertices[b].equals(vertices[f.a])) b = f.a; + else if (vertices[b].equals(vertices[f.b])) b = f.b; + else if (vertices[b].equals(vertices[f.c])) b = f.c; + if (vertices[c].equals(vertices[f.a])) c = f.a; + else if (vertices[c].equals(vertices[f.b])) c = f.b; + else if (vertices[c].equals(vertices[f.c])) c = f.c; + } + faces.push(new Face3(a, b, c)); + } + // Reorder vertices + var verts = new Array(); + var map = new Map(); + var i = 0; + function orderVert(fi: Int): Int { + var val = map.get(fi); + if (val == null) { + verts.push(vertices[fi]); + map.set(fi, i); + i++; + return i - 1; + } + else return val; + } + for (f in faces) { + f.a = orderVert(f.a); + f.b = orderVert(f.b); + f.c = orderVert(f.c); + } + + var userData = new UserData(); + userData.mass = mass; + userData.friction = friction; + userData.velocity = velocity.clone(); + userData.angularVelocity = angularVelocity.clone(); + userData.breakable = breakable; + userData.vertices = verts; + userData.faces = faces; + userDataMap.set(object, userData); + } + + // maxRadialIterations Iterations for radial cuts + // maxRandomIterations Max random iterations for not-radial cuts + public function subdivideByImpact(object: MeshObject, pointOfImpact: Vec4, normal: Vec4, maxRadialIterations: Int, maxRandomIterations: Int): Array { + var debris: Array = []; + + tempVec4.addvecs(pointOfImpact, normal); + tempPlane.setFromCoplanarPoints(pointOfImpact, object.transform.loc, tempVec4); + + var maxTotalIterations = maxRandomIterations + maxRadialIterations; + var scope = this; + + function subdivideRadial(subObject: MeshObject, startAngle: Float, endAngle: Float, numIterations: Int) { + + if (Math.random() < numIterations * 0.05 || numIterations > maxTotalIterations) { + debris.push(subObject); + return; + } + + var angle = Math.PI; + if (numIterations == 0) { + tempPlane2.normal.setFrom(tempPlane.normal); + tempPlane2.constant = tempPlane.constant; + } + else { + if (numIterations <= maxRadialIterations) { + angle = (endAngle - startAngle) * (0.2 + 0.6 * Math.random()) + startAngle; + + // Rotate tempPlane2 at impact point around normal axis and the angle + scope.tempVec42.setFrom(object.transform.loc).sub(pointOfImpact).applyAxisAngle(normal, angle).add(pointOfImpact); + tempPlane2.setFromCoplanarPoints(pointOfImpact, scope.tempVec4, scope.tempVec42); + } + else { + angle = ((0.5 * (numIterations & 1)) + 0.2 * (2 - Math.random())) * Math.PI; + + // Rotate tempPlane2 at object position around normal axis and the angle + scope.tempVec42.setFrom(pointOfImpact).sub(subObject.transform.loc).applyAxisAngle(normal, angle).add(subObject.transform.loc); + scope.tempVec43.setFrom(normal).add(subObject.transform.loc); + tempPlane2.setFromCoplanarPoints(subObject.transform.loc, scope.tempVec43, scope.tempVec42); + } + } + + // Perform the cut + scope.cutByPlane(subObject, tempPlane2, scope.tempCutResult); + + var object1 = scope.tempCutResult.object1; + var object2 = scope.tempCutResult.object2; + if (object1 != null) subdivideRadial(object1, startAngle, angle, numIterations + 1); + if (object2 != null) subdivideRadial(object2, angle, endAngle, numIterations + 1); + + // Object was subdivided into debris + iron.Scene.active.meshes.remove(subObject); + } + + subdivideRadial(object, 0, 2 * Math.PI, 0); + return debris; + } + + function transformFreeVector(v: Vec4, m: Mat4): Vec4 { + // Vector interpreted as a free vector + // Mat4 orthogonal matrix (matrix without scale) + var x = v.x, y = v.y, z = v.z; + v.x = m._00 * x + m._10 * y + m._20 * z; + v.y = m._01 * x + m._11 * y + m._21 * z; + v.z = m._02 * x + m._12 * y + m._22 * z; + return v; + } + + function transformFreeVectorInverse(v: Vec4, m: Mat4): Vec4 { + // Vector interpreted as a free vector + // Mat4 orthogonal matrix (matrix without scale) + var x = v.x, y = v.y, z = v.z; + v.x = m._00 * x + m._01 * y + m._02 * z; + v.y = m._10 * x + m._11 * y + m._12 * z; + v.z = m._20 * x + m._21 * y + m._22 * z; + return v; + } + + function transformTiedVectorInverse(v: Vec4, m: Mat4): Vec4 { + // Vector interpreted as a tied (ordinary) vector + // Mat4 orthogonal matrix (matrix without scale) + var x = v.x, y = v.y, z = v.z; + v.x = m._00 * x + m._01 * y + m._02 * z - m._30; + v.y = m._10 * x + m._11 * y + m._12 * z - m._31; + v.z = m._20 * x + m._21 * y + m._22 * z - m._32; + return v; + }; + + function transformPlaneToLocalSpace(plane: Plane, m: Mat4, resultPlane: Plane) { + resultPlane.normal.setFrom(plane.normal); + resultPlane.constant = plane.constant; + + var v1 = new Vec4(); + var referencePoint = transformTiedVectorInverse(plane.coplanarPoint(v1), m); + transformFreeVectorInverse(resultPlane.normal, m); + + // Recalculate constant + resultPlane.constant = -referencePoint.dot(resultPlane.normal); + } + + // Returns breakable objects, the resulting 2 pieces of the cut + // object2 can be null if the plane doesn't cut the object + // object1 can be null only in case of error + // Returned value is number of pieces, 0 for error + function cutByPlane(object: MeshObject, plane: Plane, output: CutResult): Int { + var userData = userDataMap.get(object); + var points: Array = userData.vertices; + var faces: Array = userData.faces; + + var numPoints = points.length; + var points1 = []; + var points2 = []; + var delta = smallDelta; + + // Reset vertices mark + for (i in 0...numPoints) points[i].w = 0; + + // Reset segments mark + var numPointPairs = numPoints * numPoints; + for (i in 0...numPointPairs) this.segments[i] = false; + + // Iterate through the faces to mark edges shared by coplanar faces + for (i in 0...faces.length - 1) { + var face1 = faces[i]; + + for (j in (i + 1)...faces.length) { + var face2 = faces[j]; + var coplanar = 1 - face1.normal.dot(face2.normal) < delta; + + if (coplanar) { + var a1 = face1.a; + var b1 = face1.b; + var c1 = face1.c; + var a2 = face2.a; + var b2 = face2.b; + var c2 = face2.c; + + if (a1 == a2 || a1 == b2 || a1 == c2) { + if (b1 == a2 || b1 == b2 || b1 == c2) { + this.segments[a1 * numPoints + b1] = true; + this.segments[b1 * numPoints + a1] = true; + } + else { + this.segments[c1 * numPoints + a1] = true; + this.segments[a1 * numPoints + c1] = true; + } + } + else if (b1 == a2 || b1 == b2 || b1 == c2) { + this.segments[c1 * numPoints + b1] = true; + this.segments[b1 * numPoints + c1] = true; + } + } + } + } + + // Transform the plane to object local space + var localPlane = this.tempPlane; + object.transform.buildMatrix(); + transformPlaneToLocalSpace(plane, object.transform.world, localPlane); + + // Iterate through the faces adding points to both pieces + for (i in 0...faces.length) { + + var face = faces[i]; + for (segment in 0...3) { + var i0 = segment == 0 ? face.a : (segment == 1 ? face.b : face.c); + var i1 = segment == 0 ? face.b : (segment == 1 ? face.c : face.a); + + var segmentState = this.segments[i0 * numPoints + i1]; + // The segment already has been processed in another face + if (segmentState) continue; + + // Mark segment as processed (also inverted segment) + this.segments[i0 * numPoints + i1] = true; + this.segments[i1 * numPoints + i0] = true; + + var p0 = points[i0]; + var p1 = points[i1]; + + if (p0.w == 0) { + var d = localPlane.distanceToPoint(p0); + + // mark: 1 for negative side, 2 for positive side, 3 for coplanar point + if (d > delta) { + p0.w = 2; + points2.push(p0); + } + else if (d < -delta) { + p0.w = 1; + points1.push(p0); + } + else { + p0.w = 3; + points1.push(p0); + var p02 = p0.clone(); + p02.w = 3; + points2.push(p02); + } + } + + if (p1.w == 0) { + var d = localPlane.distanceToPoint(p1); + + // mark: 1 for negative side, 2 for positive side, 3 for coplanar point + if (d > delta) { + p1.w = 2; + points2.push(p1); + } + else if (d < -delta) { + p1.w = 1; + points1.push(p1); + } + else { + p1.w = 3; + points1.push(p1); + var p1_2 = p1.clone(); + p1_2.w = 3; + points2.push(p1_2); + } + } + + var mark0 = p0.w; + var mark1 = p1.w; + + if ((mark0 == 1 && mark1 == 2 ) || ( mark0 == 2 && mark1 == 1)) { + // Intersection of segment with the plane + tempLine.start.setFrom(p0); + tempLine.end.setFrom(p1); + var intersection = localPlane.intersectLine(tempLine); + if (intersection == null) return 0; + + intersection.w = 1; + points1.push(intersection); + var intersection_2 = intersection.clone(); + intersection_2.w = 2; + points2.push(intersection_2); + } + } + } + + // Calculate debris mass (very fast and imprecise): + var newMass = userData.mass * 0.5; + + // Calculate debris Center of Mass (again fast and imprecise) + tempCM1.set(0, 0, 0); + var radius1 = 0.0; + var numPoints1 = points1.length; + if (numPoints1 > 0) { + for (i in 0...numPoints1) { + tempCM1.add(points1[i]); + } + tempCM1.mult(1.0 / numPoints1); + for (i in 0...numPoints1) { + var p = points1[i]; + p.sub(tempCM1); + radius1 = Math.max(Math.max(radius1, p.x), Math.max(p.y, p.z)); + } + tempCM1.add(object.transform.loc); + } + + tempCM2.set(0, 0, 0); + var radius2 = 0.0; + var numPoints2 = points2.length; + if (numPoints2 > 0) { + for (i in 0...numPoints2) { + tempCM2.add(points2[i]); + } + tempCM2.mult(1.0 / numPoints2); + for (i in 0...numPoints2) { + var p = points2[i]; + p.sub(tempCM2); + radius2 = Math.max(Math.max(radius2, p.x), Math.max(p.y, p.z)); + } + tempCM2.add(object.transform.loc); + } + + var object1 = null; + var object2 = null; + var numObjects = 0; + if (numPoints1 > 4) { + var data1 = makeMeshData(points1); + object1 = new MeshObject(data1, object.materials); + object1.transform.loc.setFrom(tempCM1); + object1.transform.rot.setFrom(object.transform.rot); + object1.transform.buildMatrix(); + initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak); + numObjects++; + } + + if (numPoints2 > 4) { + var data2 = makeMeshData(points2); + object2 = new MeshObject(data2, object.materials); + object2.transform.loc.setFrom(tempCM2); + object2.transform.rot.setFrom(object.transform.rot); + object2.transform.buildMatrix(); + initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak); + numObjects++; + } + + output.object1 = object1; + output.object2 = object2; + return numObjects; + } + + static var meshIndex = 0; + function makeMeshData(points: Array): MeshData { + while (points.length > 50) points.pop(); + var cm = new ConvexHull(points); + + var maxdim = 1.0; + var pa = new Array(); + var na = new Array(); + for (p in cm.vertices) { + pa.push(p.x); + pa.push(p.y); + pa.push(p.z); + na.push(0.0); + na.push(0.0); + na.push(0.0); + + var ax = Math.abs(p.x); + var ay = Math.abs(p.y); + var az = Math.abs(p.z); + if (ax > maxdim) maxdim = ax; + if (ay > maxdim) maxdim = ay; + if (az > maxdim) maxdim = az; + } + maxdim *= 2; + + var ind = new Array(); + function addFlatNormal(normal: Vec4, fi: Int) { + if (na[fi * 3] != 0.0 || na[fi * 3 + 1] != 0.0 || na[fi * 3 + 2] != 0.0) { + pa.push(pa[fi * 3 ]); + pa.push(pa[fi * 3 + 1]); + pa.push(pa[fi * 3 + 2]); + na.push(normal.x); + na.push(normal.y); + na.push(normal.z); + ind.push(Std.int(pa.length / 3 - 1)); + } + else { + na[fi * 3 ] = normal.x; + na[fi * 3 + 1] = normal.y; + na[fi * 3 + 2] = normal.z; + ind.push(fi); + } + } + for (f in cm.face3s) { + // Duplicate vertex for flat normals + addFlatNormal(f.normal, f.a); + addFlatNormal(f.normal, f.b); + addFlatNormal(f.normal, f.c); + } + + // TODO: + var n = Std.int(pa.length / 3); + var paa = new kha.arrays.Int16Array(n * 4); + var naa = new kha.arrays.Int16Array(n * 2); + var invdim = 1 / maxdim; + for (i in 0...n) { + paa.set(i * 4 , Std.int(pa[i * 3 ] * 32767 * invdim)); + paa.set(i * 4 + 1, Std.int(pa[i * 3 + 1] * 32767 * invdim)); + paa.set(i * 4 + 2, Std.int(pa[i * 3 + 2] * 32767 * invdim)); + naa.set(i * 2 , Std.int(na[i * 3 ] * 32767 * invdim)); + naa.set(i * 2 + 1, Std.int(na[i * 3 + 1] * 32767 * invdim)); + paa.set(i * 4 + 3, Std.int(na[i * 3 + 2] * 32767 * invdim)); + } + var inda = new kha.arrays.Uint32Array(ind.length); + for (i in 0...ind.length) inda.set(i, ind[i]); + + var pos: TVertexArray = { attrib: "pos", values: paa, data: "short4norm" }; + var nor: TVertexArray = { attrib: "nor", values: naa, data: "short2norm" }; + var indices: TIndexArray = { material: 0, values: inda }; + + var rawmesh: TMeshData = { + name: "TempMesh" + (meshIndex++), + vertex_arrays: [pos, nor], + index_arrays: [indices], + scale_pos: maxdim + }; + + // Synchronous on Krom + var md = new MeshData(rawmesh, function(d: MeshData) {}); + md.geom.calculateAABB(); + return md; + } +} + +class UserData { + + public var mass: Float; + public var friction: Float; + public var velocity: Vec4; + public var angularVelocity: Vec4; + public var breakable: Bool; + + public var vertices: Array; + public var faces: Array; + + public function new() {} +} + +class CutResult { + + public var object1: MeshObject = null; + public var object2: MeshObject = null; + public function new() {} +} + +class Line3 { + + public var start: Vec4; + public var end: Vec4; + + public function new() { + start = new Vec4(); + end = new Vec4(); + } + + public function delta(result: Vec4): Vec4 { + result.subvecs(end, start); + return result; + } +} + +class Plane { + + public var normal = new Vec4(1.0, 0.0, 0.0); + public var constant = 0.0; + + public function new() {} + + public function distanceToPoint(point: Vec4): Float { + return normal.dot(point) + constant; + } + + public function setFromCoplanarPoints(a: Vec4, b: Vec4, c: Vec4): Plane { + var v1 = new Vec4(); + var v2 = new Vec4(); + var normal = v1.subvecs(c, b).cross(v2.subvecs(a, b)).normalize(); + set(normal, a); + return this; + } + + public function set(normal: Vec4, point: Vec4): Plane { + this.normal.setFrom(normal); + constant = -point.dot(this.normal); + return this; + } + + public function coplanarPoint(result: Vec4): Vec4 { + return result.setFrom(normal).mult(-constant); + } + + public function intersectLine(line: Line3): Vec4 { + var v1 = new Vec4(); + var result = new Vec4(); + var direction = line.delta(v1); + var denominator = normal.dot(direction); + if (denominator == 0) { + // line is coplanar, return origin + if (distanceToPoint(line.start) == 0) { + return result.setFrom(line.start); + } + // Unsure if this is the correct method to handle this case. + return null; + } + + var t = -(line.start.dot(this.normal) + constant) / denominator; + if (t < 0 || t > 1) return null; + return result.setFrom(direction).mult(t).add(line.start); + } +} + +// Based on work by qiao https://github.com/qiao +// This is a convex hull generator using the incremental method +// The complexity is O(n^2) where n is the number of vertices +class ConvexHull { + + var faces = [[0, 1, 2], [0, 2, 1]]; + public var face3s = new Array(); + public var vertices = new Array(); + + public function new(vertices: Array) { + + for (i in 3...vertices.length) addPoint(i, vertices); + + // Push vertices into array, skipping those inside the hull + // Map from old vertex id to new id + var id = 0; + var newId = new Array(); + for (i in 0...vertices.length) newId.push(-1); + + for (i in 0...faces.length) { + var face = faces[i]; + for (j in 0...3) { + if (newId[face[j]] == -1) { + newId[face[j]] = id++; + this.vertices.push(vertices[face[j]]); + } + face[j] = newId[face[j]]; + } + } + + for (i in 0...faces.length) { + face3s.push(new Face3(faces[i][0], faces[i][1], faces[i][2])); + } + + computeFaceNormals(); + } + + var cb = new Vec4(); + var ab = new Vec4(); + function computeFaceNormals() { + for (f in 0...face3s.length) { + var face = face3s[f]; + var va = vertices[face.a]; + var vb = vertices[face.b]; + var vc = vertices[face.c]; + cb.subvecs(vc, vb); + ab.subvecs(va, vb); + cb.cross(ab); + cb.normalize(); + face.normal.setFrom(cb); + } + } + + function addPoint(vertexId: Int, vertices: Array) { + var vertex = vertices[vertexId].clone(); + + var mag = vertex.length(); + vertex.x += mag * randomOffset(); + vertex.y += mag * randomOffset(); + vertex.z += mag * randomOffset(); + + var hole: Array> = []; + var f = 0; + while (f < faces.length) { + var face = faces[f]; + + // For each face, if the vertex can see it, + // then we try to add the face's edges into the hole + if (visible(face, vertex, vertices)) { + for (e in 0...3) { + var edge = [face[e], face[(e + 1) % 3]]; + var boundary = true; + + // Remove duplicated edges + for (h in 0...hole.length) { + if (equalEdge(hole[h], edge)) { + hole[h] = hole[hole.length - 1]; + hole.pop(); + boundary = false; + break; + } + } + if (boundary) hole.push(edge); + } + + faces[f] = faces[faces.length - 1]; + faces.pop(); + } + else { + f++; + } + } + + // Construct the new faces formed by the edges of the hole and the vertex + for (h in 0...hole.length) { + faces.push([hole[h][0], hole[h][1], vertexId]); + } + } + + // Whether the face is visible from the vertex + function visible(face: Array, vertex: Vec4, vertices: Array): Bool { + var va = vertices[face[0]]; + var vb = vertices[face[1]]; + var vc = vertices[face[2]]; + var n = normal(va, vb, vc); + var dist = n.dot(va); // Distance from face to origin + return n.dot(vertex) >= dist; + } + + function normal(va: Vec4, vb: Vec4, vc: Vec4): Vec4 { + var cb = new Vec4(); + var ab = new Vec4(); + cb.subvecs(vc, vb); + ab.subvecs(va, vb); + cb.cross(ab); + cb.normalize(); + return cb; + } + + function equalEdge(ea: Array, eb: Array): Bool { + return ea[0] == eb[1] && ea[1] == eb[0]; + } + + function randomOffset(): Float { + return (Math.random() - 0.5) * 2 * 1e-6; + } +} + +class Face3 { + + public var a: Int; + public var b: Int; + public var c: Int; + public var normal: Vec4; + + public function new(a: Int, b: Int, c: Int) { + this.a = a; + this.b = b; + this.c = c; + normal = new Vec4(); + } +} diff --git a/Sources/armory/trait/PhysicsDrag.hx b/Sources/armory/trait/PhysicsDrag.hx new file mode 100644 index 0000000000..111a440344 --- /dev/null +++ b/Sources/armory/trait/PhysicsDrag.hx @@ -0,0 +1,132 @@ +package armory.trait; + +import iron.Trait; +import iron.system.Input; +import iron.math.Vec3; +import iron.math.Vec4; +import iron.math.Mat4; +import iron.math.RayCaster; +import armory.trait.physics.RigidBody; +import armory.trait.physics.PhysicsWorld; + +class PhysicsDrag extends Trait { + +#if (!arm_bullet) + public function new() { super(); } +#else + + @prop public var linearLowerLimit = new Vec3(0,0,0); + @prop public var linearUpperLimit = new Vec3(0,0,0); + @prop public var angularLowerLimit = new Vec3(-10,-10,-10); + @prop public var angularUpperLimit = new Vec3(10,10,10); + + var pickConstraint: bullet.Bt.Generic6DofConstraint = null; + var pickDist: Float; + var pickedBody: RigidBody = null; + + var rayFrom: bullet.Bt.Vector3; + var rayTo: bullet.Bt.Vector3; + + static var v = new Vec4(); + static var m = Mat4.identity(); + static var first = true; + + public function new() { + super(); + if (first) { + first = false; + notifyOnUpdate(update); + } + } + + function update() { + var physics = PhysicsWorld.active; + if (pickedBody != null) pickedBody.activate(); + + var mouse = Input.getMouse(); + if (mouse.started()) { + + var b = physics.pickClosest(mouse.x, mouse.y); + if (b != null && b.mass > 0 && !b.body.isKinematicObject() && b.object.getTrait(PhysicsDrag) != null) { + + setRays(); + pickedBody = b; + + m.getInverse(b.object.transform.world); + var hit = physics.hitPointWorld; + v.setFrom(hit); + v.applymat4(m); + var localPivot = new bullet.Bt.Vector3(v.x, v.y, v.z); + var tr = new bullet.Bt.Transform(); + tr.setIdentity(); + tr.setOrigin(localPivot); + + pickConstraint = new bullet.Bt.Generic6DofConstraint(b.body, tr, false); + pickConstraint.setLinearLowerLimit(new bullet.Bt.Vector3(linearLowerLimit.x, linearLowerLimit.y, linearLowerLimit.z)); + pickConstraint.setLinearUpperLimit(new bullet.Bt.Vector3(linearUpperLimit.x, linearUpperLimit.y, linearUpperLimit.z)); + pickConstraint.setAngularLowerLimit(new bullet.Bt.Vector3(angularLowerLimit.x, angularLowerLimit.y, angularLowerLimit.z)); + pickConstraint.setAngularUpperLimit(new bullet.Bt.Vector3(angularUpperLimit.x, angularUpperLimit.y, angularUpperLimit.z)); + physics.world.addConstraint(pickConstraint, false); + + /*pickConstraint.setParam(4, 0.8, 0); + pickConstraint.setParam(4, 0.8, 1); + pickConstraint.setParam(4, 0.8, 2); + pickConstraint.setParam(4, 0.8, 3); + pickConstraint.setParam(4, 0.8, 4); + pickConstraint.setParam(4, 0.8, 5); + + pickConstraint.setParam(1, 0.1, 0); + pickConstraint.setParam(1, 0.1, 1); + pickConstraint.setParam(1, 0.1, 2); + pickConstraint.setParam(1, 0.1, 3); + pickConstraint.setParam(1, 0.1, 4); + pickConstraint.setParam(1, 0.1, 5);*/ + + pickDist = v.set(hit.x - rayFrom.x(), hit.y - rayFrom.y(), hit.z - rayFrom.z()).length(); + + Input.occupied = true; + } + } + + else if (mouse.released()) { + if (pickConstraint != null) { + physics.world.removeConstraint(pickConstraint); + pickConstraint = null; + pickedBody = null; + } + Input.occupied = false; + } + + else if (mouse.down()) { + if (pickConstraint != null) { + setRays(); + + // Keep it at the same picking distance + var dir = new bullet.Bt.Vector3(rayTo.x() - rayFrom.x(), rayTo.y() - rayFrom.y(), rayTo.z() - rayFrom.z()); + dir.normalize(); + dir.setX(dir.x() * pickDist); + dir.setY(dir.y() * pickDist); + dir.setZ(dir.z() * pickDist); + var newPivotB = new bullet.Bt.Vector3(rayFrom.x() + dir.x(), rayFrom.y() + dir.y(), rayFrom.z() + dir.z()); + + #if (js || hl) + pickConstraint.getFrameOffsetA().setOrigin(newPivotB); + #elseif cpp + pickConstraint.setFrameOffsetAOrigin(newPivotB); + #end + } + } + } + + static var start = new Vec4(); + static var end = new Vec4(); + inline function setRays() { + var mouse = Input.getMouse(); + var camera = iron.Scene.active.camera; + var v = camera.transform.world.getLoc(); + rayFrom = new bullet.Bt.Vector3(v.x, v.y, v.z); + RayCaster.getDirection(start, end, mouse.x, mouse.y, camera); + rayTo = new bullet.Bt.Vector3(end.x, end.y, end.z); + } +#end +} diff --git a/Sources/armory/trait/SimpleMoveObject.hx b/Sources/armory/trait/SimpleMoveObject.hx new file mode 100644 index 0000000000..2479cb036d --- /dev/null +++ b/Sources/armory/trait/SimpleMoveObject.hx @@ -0,0 +1,73 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.system.Input; +import armory.trait.physics.RigidBody; + +/** + Simple script to move an object around using the keyboard with WSAD+QE. + Can be used for testing and debuging. +**/ +class SimpleMoveObject extends iron.Trait { + + @prop + var speed: Float = 0.1; + + var keyboard: Keyboard; + var rb: RigidBody; + + public function new() { + super(); + + notifyOnInit(function() { + rb = object.getTrait(RigidBody); + keyboard = Input.getKeyboard(); + }); + + notifyOnUpdate(function() { + var move = new Vec4(0, 0, 0); + + if (keyboard.down("d")) { + move.x += speed; + } + + if (keyboard.down("a")) { + move.x -= speed; + } + + if (keyboard.down("w")) { + move.y += speed; + } + + if (keyboard.down("s")) { + move.y -= speed; + } + + if (keyboard.down("q")) { + move.z += speed; + } + + if (keyboard.down("e")) { + move.z -= speed; + } + + if (!move.equals(new Vec4(0, 0, 0))) { + moveObject(move); + } + }); + } + + function moveObject(vec: Vec4){ + if (rb != null) { + #if arm_physics + rb.setLinearVelocity(0, 0, 0); + rb.setAngularVelocity(0, 0, 0); + rb.transform.translate(vec.x, vec.y, vec.z); + rb.syncTransform(); + #end + } + else { + object.transform.translate(vec.x, vec.y, vec.z); + } + } +} diff --git a/Sources/armory/trait/SimpleRotateObject.hx b/Sources/armory/trait/SimpleRotateObject.hx new file mode 100644 index 0000000000..dd2238219b --- /dev/null +++ b/Sources/armory/trait/SimpleRotateObject.hx @@ -0,0 +1,72 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.system.Input; +import armory.trait.physics.RigidBody; + +/** + Simple script to rotate an object around using the keyboard with RT(x), FG(y), VB(z). + Can be used for testing and debuging. +**/ +class SimpleRotateObject extends iron.Trait { + + @prop + var speed: Float = 0.01; + + var keyboard: Keyboard; + var rb: RigidBody; + + public function new() { + super(); + + notifyOnInit(function() { + rb = object.getTrait(RigidBody); + keyboard = Input.getKeyboard(); + }); + + notifyOnUpdate(function() { + var rotate = new Vec4(0, 0, 0); + + if (keyboard.down("r")) { + rotate.x += 1; + } + + if (keyboard.down("t")) { + rotate.x -= 1; + } + + if (keyboard.down("f")) { + rotate.y += 1; + } + + if (keyboard.down("g")) { + rotate.y -= 1; + } + + if (keyboard.down("v")) { + rotate.z += 1; + } + + if (keyboard.down("b")) { + rotate.z -= 1; + } + + if (!rotate.equals(new Vec4(0, 0, 0))) { + rotateObject(rotate); + } + }); + } + + function rotateObject(vec: Vec4){ + if (rb != null) { + #if arm_physics + rb.setAngularVelocity(0, 0, 0); + rb.transform.rotate(vec, speed); + rb.syncTransform(); + #end + } + else { + object.transform.rotate(vec, speed); + } + } +} diff --git a/Sources/armory/trait/SimpleScaleObject.hx b/Sources/armory/trait/SimpleScaleObject.hx new file mode 100644 index 0000000000..646beb220c --- /dev/null +++ b/Sources/armory/trait/SimpleScaleObject.hx @@ -0,0 +1,92 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.system.Input; +import armory.trait.physics.RigidBody; +import armory.trait.physics.KinematicCharacterController; + +/** + Simple script to scale an object around using the keyboard. + All axis can be scaled at once using the Z and X keys. + Individual axis can be scaled using YU(x), HJ(y), NM(z). + Can be used for testing and debuging. +**/ +class SimpleScaleObject extends iron.Trait { + + @prop + var speed: Float = 0.1; + + var keyboard: Keyboard; + var rb: RigidBody; + var character: KinematicCharacterController; + + public function new() { + super(); + + notifyOnInit(function() { + rb = object.getTrait(RigidBody); + character = object.getTrait(KinematicCharacterController); + keyboard = Input.getKeyboard(); + }); + + notifyOnUpdate(function() { + var scale = new Vec4(0, 0, 0); + + if (keyboard.down("y")) { + scale.x += speed; + } + + if (keyboard.down("u")) { + scale.x -= speed; + } + + if (keyboard.down("h")) { + scale.y += speed; + } + + if (keyboard.down("j")) { + scale.y -= speed; + } + + if (keyboard.down("n")) { + scale.z += speed; + } + + if (keyboard.down("m")) { + scale.z -= speed; + } + + if (keyboard.down("z")) { + scale.set(speed, speed, speed); + } + + if (keyboard.down("x")) { + scale.set(-speed, -speed, -speed); + } + + if (!scale.equals(new Vec4(0, 0, 0))) { + scaleObject(scale); + } + }); + } + + function scaleObject(vec: Vec4){ + var s = object.transform.scale; + if (rb != null) { + #if arm_physics + rb.transform.scale.set(s.x + vec.x, s.y + vec.y, s.z + vec.z); + rb.syncTransform(); + #end + } + else if (character != null) { + #if arm_physics + character.transform.scale.set(s.x + vec.x, s.y + vec.y, s.z + vec.z); + character.syncTransform(); + #end + } + else { + object.transform.scale.set(s.x + vec.x, s.y + vec.y, s.z + vec.z); + object.transform.buildMatrix(); + } + } +} diff --git a/Sources/armory/trait/ThirdPersonController.hx b/Sources/armory/trait/ThirdPersonController.hx new file mode 100644 index 0000000000..c834f786a6 --- /dev/null +++ b/Sources/armory/trait/ThirdPersonController.hx @@ -0,0 +1,108 @@ +package armory.trait; + +import iron.math.Vec4; +import iron.system.Input; +import iron.object.Object; +import armory.trait.physics.PhysicsWorld; +import armory.trait.internal.CameraController; + +class ThirdPersonController extends CameraController { + +#if (!arm_physics) + public function new() { super(); } +#else + + static inline var rotationSpeed = 1.0; + + var animObject: String; + var idleAction: String; + var runAction: String; + var currentAction: String; + var arm: Object; + + public function new(animObject = "", idle = "idle", run = "run") { + super(); + + this.animObject = animObject; + this.idleAction = idle; + this.runAction = run; + currentAction = idleAction; + + iron.Scene.active.notifyOnInit(init); + } + + function findAnimation(o: Object): Object { + if (o.animation != null) return o; + for (c in o.children) { + var co = findAnimation(c); + if (co != null) return co; + } + return null; + } + + function init() { + if (animObject == "") arm = findAnimation(object); + else arm = object.getChild(animObject); + + PhysicsWorld.active.notifyOnPreUpdate(preUpdate); + notifyOnUpdate(update); + notifyOnRemove(removed); + } + + var xVec = Vec4.xAxis(); + var zVec = Vec4.zAxis(); + function preUpdate() { + if (Input.occupied || !body.ready) return; + + var mouse = Input.getMouse(); + if (mouse.down()) { + camera.transform.rotate(xVec, mouse.movementY / 250 * rotationSpeed); + transform.rotate(zVec, -mouse.movementX / 250 * rotationSpeed); + camera.buildMatrix(); + body.syncTransform(); + } + } + + function removed() { + PhysicsWorld.active.removePreUpdate(preUpdate); + } + + var dir = new Vec4(); + function update() { + if (!body.ready) return; + + if (jump) body.applyImpulse(new Vec4(0, 0, 20)); + + // Move + dir.set(0, 0, 0); + if (moveForward) dir.add(transform.look()); + if (moveBackward) dir.add(transform.look().mult(-1)); + if (moveLeft) dir.add(transform.right().mult(-1)); + if (moveRight) dir.add(transform.right()); + + // Push down + var btvec = body.getLinearVelocity(); + body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); + + if (moveForward || moveBackward || moveLeft || moveRight) { + if (currentAction != runAction) { + arm.animation.play(runAction, null, 0.2); + currentAction = runAction; + } + dir.mult(-4 * 0.7); + body.activate(); + body.setLinearVelocity(dir.x, dir.y, btvec.z - 1.0); + } + else { + if (currentAction != idleAction) { + arm.animation.play(idleAction, null, 0.2); + currentAction = idleAction; + } + } + + // Keep vertical + body.setAngularFactor(0, 0, 0); + camera.buildMatrix(); + } +#end +} diff --git a/Sources/armory/trait/VehicleBody.hx b/Sources/armory/trait/VehicleBody.hx new file mode 100644 index 0000000000..a343629d73 --- /dev/null +++ b/Sources/armory/trait/VehicleBody.hx @@ -0,0 +1,265 @@ +package armory.trait; + +import iron.Trait; +import iron.object.Object; +import iron.object.CameraObject; +import iron.object.Transform; +import iron.system.Time; +import armory.trait.physics.PhysicsWorld; + +class VehicleBody extends Trait { + +#if (!arm_bullet) + public function new() { super(); } +#else + + @prop var wheel0Name: String = "Wheel0"; + @prop var wheel1Name: String = "Wheel1"; + @prop var wheel2Name: String = "Wheel2"; + @prop var wheel3Name: String = "Wheel3"; + + var physics: PhysicsWorld; + var transform: Transform; + var camera: CameraObject; + + var wheels: Array = []; + var vehicle: bullet.Bt.RaycastVehicle = null; + var carChassis: bullet.Bt.RigidBody; + + var chassis_mass = 600.0; + var wheelFriction = 1000; + var suspensionStiffness = 20.0; + var suspensionDamping = 2.3; + var suspensionCompression = 4.4; + var suspensionRestLength = 0.3; + var rollInfluence = 0.1; + + var maxEngineForce = 3000.0; + var maxBreakingForce = 500.0; + + var engineForce = 0.0; + var breakingForce = 0.0; + var vehicleSteering = 0.0; + + public function new() { + super(); + iron.Scene.active.notifyOnInit(init); + } + + function init() { + physics = armory.trait.physics.PhysicsWorld.active; + transform = object.transform; + camera = iron.Scene.active.camera; + + for (n in [wheel0Name, wheel1Name, wheel2Name, wheel3Name]) { + wheels.push(iron.Scene.active.root.getChild(n)); + } + + var wheelDirectionCS0 = new bullet.Bt.Vector3(0, 0, -1); + var wheelAxleCS = new bullet.Bt.Vector3(1, 0, 0); + + var chassisShape = new bullet.Bt.BoxShape(new bullet.Bt.Vector3( + transform.dim.x / 2, + transform.dim.y / 2, + transform.dim.z / 2)); + + var compound = new bullet.Bt.CompoundShape(); + + var localTrans = new bullet.Bt.Transform(); + localTrans.setIdentity(); + localTrans.setOrigin(new bullet.Bt.Vector3(0, 0, 1)); + + compound.addChildShape(localTrans, chassisShape); + + carChassis = createRigidBody(chassis_mass, compound); + + // Create vehicle + var tuning = new bullet.Bt.VehicleTuning(); + var vehicleRayCaster = new bullet.Bt.DefaultVehicleRaycaster(physics.world); + vehicle = new bullet.Bt.RaycastVehicle(tuning, carChassis, vehicleRayCaster); + + // Never deactivate the vehicle + carChassis.setActivationState(bullet.Bt.CollisionObjectActivationState.DISABLE_DEACTIVATION); + + // Choose coordinate system + var rightIndex = 0; + var upIndex = 2; + var forwardIndex = 1; + vehicle.setCoordinateSystem(rightIndex, upIndex, forwardIndex); + + // Add wheels + for (i in 0...wheels.length) { + var vehicleWheel = new VehicleWheel(i, wheels[i].transform, object.transform); + vehicle.addWheel( + vehicleWheel.getConnectionPoint(), + wheelDirectionCS0, + wheelAxleCS, + suspensionRestLength, + vehicleWheel.wheelRadius, + tuning, + vehicleWheel.isFrontWheel); + } + + // Setup wheels + for (i in 0...vehicle.getNumWheels()){ + var wheel = vehicle.getWheelInfo(i); + wheel.m_suspensionStiffness = suspensionStiffness; + wheel.m_wheelsDampingRelaxation = suspensionDamping; + wheel.m_wheelsDampingCompression = suspensionCompression; + wheel.m_frictionSlip = wheelFriction; + wheel.m_rollInfluence = rollInfluence; + } + + physics.world.addAction(vehicle); + + notifyOnUpdate(update); + } + + function update() { + if (vehicle == null) return; + + var keyboard = iron.system.Input.getKeyboard(); + var forward = keyboard.down(keyUp); + var backward = keyboard.down(keyDown); + var left = keyboard.down(keyLeft); + var right = keyboard.down(keyRight); + var brake = keyboard.down("space"); + + if (forward) { + engineForce = maxEngineForce; + } + else if (backward) { + engineForce = -maxEngineForce; + } + else if (brake) { + breakingForce = 100; + } + else { + engineForce = 0; + breakingForce = 20; + } + + if (left) { + if (vehicleSteering < 0.3) vehicleSteering += Time.step; + } + else if (right) { + if (vehicleSteering > -0.3) vehicleSteering -= Time.step; + } + else if (vehicleSteering != 0) { + var step = Math.abs(vehicleSteering) < Time.step ? Math.abs(vehicleSteering) : Time.step; + if (vehicleSteering > 0) vehicleSteering -= step; + else vehicleSteering += step; + } + + vehicle.applyEngineForce(engineForce, 2); + vehicle.setBrake(breakingForce, 2); + vehicle.applyEngineForce(engineForce, 3); + vehicle.setBrake(breakingForce, 3); + vehicle.setSteeringValue(vehicleSteering, 0); + vehicle.setSteeringValue(vehicleSteering, 1); + + for (i in 0...vehicle.getNumWheels()) { + // Synchronize the wheels with the chassis worldtransform + vehicle.updateWheelTransform(i, true); + + // Update wheels transforms + var trans = vehicle.getWheelTransformWS(i); + var p = trans.getOrigin(); + var q = trans.getRotation(); + wheels[i].transform.localOnly = true; + wheels[i].transform.loc.set(p.x(), p.y(), p.z()); + wheels[i].transform.rot.set(q.x(), q.y(), q.z(), q.w()); + wheels[i].transform.dirty = true; + } + + var trans = carChassis.getWorldTransform(); + var p = trans.getOrigin(); + var q = trans.getRotation(); + transform.loc.set(p.x(), p.y(), p.z()); + transform.rot.set(q.x(), q.y(), q.z(), q.w()); + var up = transform.world.up(); + transform.loc.add(up); + transform.dirty = true; + + // TODO: fix parent matrix update + if (camera.parent != null) camera.parent.transform.buildMatrix(); + camera.buildMatrix(); + } + + function createRigidBody(mass: Float, shape: bullet.Bt.CompoundShape): bullet.Bt.RigidBody { + + var localInertia = new bullet.Bt.Vector3(0, 0, 0); + shape.calculateLocalInertia(mass, localInertia); + + var centerOfMassOffset = new bullet.Bt.Transform(); + centerOfMassOffset.setIdentity(); + + var startTransform = new bullet.Bt.Transform(); + startTransform.setIdentity(); + startTransform.setOrigin(new bullet.Bt.Vector3( + transform.loc.x, + transform.loc.y, + transform.loc.z)); + startTransform.setRotation(new bullet.Bt.Quaternion( + transform.rot.x, + transform.rot.y, + transform.rot.z, + transform.rot.w)); + + var myMotionState = new bullet.Bt.DefaultMotionState(startTransform, centerOfMassOffset); + var cInfo = new bullet.Bt.RigidBodyConstructionInfo(mass, myMotionState, shape, localInertia); + + var body = new bullet.Bt.RigidBody(cInfo); + body.setLinearVelocity(new bullet.Bt.Vector3(0, 0, 0)); + body.setAngularVelocity(new bullet.Bt.Vector3(0, 0, 0)); + physics.world.addRigidBody(body); + + return body; + } + + #if arm_azerty + static inline var keyUp = "z"; + static inline var keyDown = "s"; + static inline var keyLeft = "q"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "a"; + #else + static inline var keyUp = "w"; + static inline var keyDown = "s"; + static inline var keyLeft = "a"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "q"; + #end +#end +} + +class VehicleWheel { + +#if (!arm_bullet) + public function new() {} +#else + + public var isFrontWheel: Bool; + public var wheelRadius: Float; + public var wheelWidth: Float; + + var locX: Float; + var locY: Float; + var locZ: Float; + + public function new(id: Int, transform: Transform, vehicleTransform: Transform) { + wheelRadius = transform.dim.z / 2; + wheelWidth = transform.dim.x > transform.dim.y ? transform.dim.y : transform.dim.x; + + locX = transform.loc.x; + locY = transform.loc.y; + locZ = vehicleTransform.dim.z / 2 + transform.loc.z; + } + + public function getConnectionPoint(): bullet.Bt.Vector3 { + return new bullet.Bt.Vector3(locX, locY, locZ); + } +#end +} diff --git a/Sources/armory/trait/VirtualGamepad.hx b/Sources/armory/trait/VirtualGamepad.hx new file mode 100644 index 0000000000..2b6319e44c --- /dev/null +++ b/Sources/armory/trait/VirtualGamepad.hx @@ -0,0 +1,139 @@ +package armory.trait; + +import iron.Trait; +import iron.system.Input; +import iron.math.Vec2; + +@:access(iron.system.Input) +@:access(iron.system.Gamepad) +class VirtualGamepad extends Trait { + + var gamepad: Gamepad; + + var leftPadX = 0; + var leftPadY = 0; + var rightPadX = 0; + var rightPadY = 0; + + var leftStickX = 0; + var leftStickXLast = 0; + var leftStickY = 0; + var leftStickYLast = 0; + var rightStickX = 0; + var rightStickXLast = 0; + var rightStickY = 0; + var rightStickYLast = 0; + + var leftLocked = false; + var rightLocked = false; + + @prop + public var radius = 100; // Radius + @prop + public var offset = 40; // Offset + + public function new() { + super(); + + notifyOnInit(function() { + + gamepad = new Gamepad(0, true); + Input.gamepads.push(gamepad); + + notifyOnUpdate(update); + notifyOnRender2D(render2D); + }); + } + + function update() { + var r = radius; + var o = offset; + + leftPadX = r + o; + rightPadX = iron.App.w() - r - o; + leftPadY = rightPadY = iron.App.h() - r - o; + + var mouse = Input.getMouse(); + if (mouse.started() && Vec2.distancef(mouse.x, mouse.y, leftPadX, leftPadY) <= r) { + leftLocked = true; + } + else if (mouse.released()) { + leftLocked = false; + } + + if (mouse.started() && Vec2.distancef(mouse.x, mouse.y, rightPadX, rightPadY) <= r) { + rightLocked = true; + } + else if (mouse.released()) { + rightLocked = false; + } + + if (leftLocked) { + leftStickX = Std.int(mouse.x - leftPadX); + leftStickY = Std.int(mouse.y - leftPadY); + + var l = Math.sqrt(leftStickX * leftStickX + leftStickY * leftStickY); + if (l > r) { + var x = r * (leftStickX / Math.sqrt(leftStickX * leftStickX + leftStickY * leftStickY)); + var y = r * (leftStickY / Math.sqrt(leftStickX * leftStickX + leftStickY * leftStickY)); + leftStickX = Std.int(x); + leftStickY = Std.int(y); + } + } + else { + leftStickX = 0; + leftStickY = 0; + } + + if (rightLocked) { + rightStickX = Std.int(mouse.x - rightPadX); + rightStickY = Std.int(mouse.y - rightPadY); + + var l = Math.sqrt(rightStickX * rightStickX + rightStickY * rightStickY); + if (l > r) { + var x = r * (rightStickX / Math.sqrt(rightStickX * rightStickX + rightStickY * rightStickY)); + var y = r * (rightStickY / Math.sqrt(rightStickX * rightStickX + rightStickY * rightStickY)); + rightStickX = Std.int(x); + rightStickY = Std.int(y); + } + } + else { + rightStickX = 0; + rightStickY = 0; + } + + if (leftStickX != leftStickXLast) { + gamepad.axisListener(0, leftStickX / r); + } + if (leftStickY != leftStickYLast) { + gamepad.axisListener(1, leftStickY / r); + } + if (rightStickX != rightStickXLast) { + gamepad.axisListener(2, rightStickX / r); + } + if (rightStickY != rightStickYLast) { + gamepad.axisListener(3, rightStickY / r); + } + + leftStickXLast = leftStickX; + leftStickYLast = leftStickY; + rightStickXLast = rightStickX; + rightStickYLast = rightStickY; + } + + function render2D(g: kha.graphics2.Graphics) { + var r = radius; + + g.color = 0xffaaaaaa; + + zui.GraphicsExtension.fillCircle(g, leftPadX, leftPadY, r); + zui.GraphicsExtension.fillCircle(g, rightPadX, rightPadY, r); + + var r2 = Std.int(r / 2.2); + g.color = 0xffffff44; + zui.GraphicsExtension.fillCircle(g, leftPadX + leftStickX, leftPadY + leftStickY, r2); + zui.GraphicsExtension.fillCircle(g, rightPadX + rightStickX, rightPadY + rightStickY, r2); + + g.color = 0xffffffff; + } +} diff --git a/Sources/armory/trait/WalkNavigation.hx b/Sources/armory/trait/WalkNavigation.hx new file mode 100644 index 0000000000..6bfc50d54d --- /dev/null +++ b/Sources/armory/trait/WalkNavigation.hx @@ -0,0 +1,149 @@ +package armory.trait; + +import iron.Trait; +import iron.system.Input; +import iron.system.Time; +import iron.object.CameraObject; +import iron.math.Vec4; + +class WalkNavigation extends Trait { + + public static var enabled = true; + var speed = 5.0; + var dir = new Vec4(); + var xvec = new Vec4(); + var yvec = new Vec4(); + var ease = 1.0; + + var camera: CameraObject; + + var keyboard: Keyboard; + var gamepad: Gamepad; + var mouse: Mouse; + + public function new() { + super(); + notifyOnInit(init); + } + + function init() { + keyboard = Input.getKeyboard(); + gamepad = Input.getGamepad(); + mouse = Input.getMouse(); + + try { + camera = cast(object, CameraObject); + } + catch (msg: String) { + trace("Error occurred: " + msg + "\nWalkNavigation trait should be used with a camera object."); + } + + if (camera != null){ + notifyOnUpdate(update); + } + } + + function update() { + if (!enabled || Input.occupied) return; + + var moveForward = keyboard.down(keyUp) || keyboard.down("up"); + var moveBackward = keyboard.down(keyDown) || keyboard.down("down"); + var strafeLeft = keyboard.down(keyLeft) || keyboard.down("left"); + var strafeRight = keyboard.down(keyRight) || keyboard.down("right"); + var strafeUp = keyboard.down(keyStrafeUp); + var strafeDown = keyboard.down(keyStrafeDown); + var fast = keyboard.down("shift") ? 2.0 : (keyboard.down("alt") ? 0.5 : 1.0); + + if (gamepad != null) { + var leftStickY = Math.abs(gamepad.leftStick.y) > 0.05; + var leftStickX = Math.abs(gamepad.leftStick.x) > 0.05; + var r1 = gamepad.down("r1") > 0.0; + var l1 = gamepad.down("l1") > 0.0; + var rightStickX = Math.abs(gamepad.rightStick.x) > 0.1; + var rightStickY = Math.abs(gamepad.rightStick.y) > 0.1; + + if (leftStickY || leftStickX || r1 || l1 || rightStickX || rightStickY) { + dir.set(0, 0, 0); + + if (leftStickY) { + yvec.setFrom(camera.look()); + yvec.mult(gamepad.leftStick.y); + dir.add(yvec); + } + if (leftStickX) { + xvec.setFrom(camera.right()); + xvec.mult(gamepad.leftStick.x); + dir.add(xvec); + } + if (r1) dir.addf(0, 0, 1); + if (l1) dir.addf(0, 0, -1); + + var d = Time.delta * speed * fast; + camera.transform.move(dir, d); + + if (rightStickX) { + camera.transform.rotate(Vec4.zAxis(), -gamepad.rightStick.x / 15.0); + } + if (rightStickY) { + camera.transform.rotate(camera.right(), gamepad.rightStick.y / 15.0); + } + } + } + + if (moveForward || moveBackward || strafeRight || strafeLeft || strafeUp || strafeDown) { + ease += Time.delta * 15; + if (ease > 1.0) ease = 1.0; + dir.set(0, 0, 0); + if (moveForward) dir.addf(camera.look().x, camera.look().y, camera.look().z); + if (moveBackward) dir.addf(-camera.look().x, -camera.look().y, -camera.look().z); + if (strafeRight) dir.addf(camera.right().x, camera.right().y, camera.right().z); + if (strafeLeft) dir.addf(-camera.right().x, -camera.right().y, -camera.right().z); + #if arm_yaxisup + if (strafeUp) dir.addf(0, 1, 0); + if (strafeDown) dir.addf(0, -1, 0); + #else + if (strafeUp) dir.addf(0, 0, 1); + if (strafeDown) dir.addf(0, 0, -1); + #end + } + else { + ease -= Time.delta * 20.0 * ease; + if (ease < 0.0) ease = 0.0; + } + + if (mouse.wheelDelta < 0) { + speed *= 1.1; + } else if (mouse.wheelDelta > 0) { + speed *= 0.9; + if (speed < 0.5) speed = 0.5; + } + + var d = Time.delta * speed * fast * ease; + if (d > 0.0) camera.transform.move(dir, d); + + if (mouse.down()) { + #if arm_yaxisup + camera.transform.rotate(Vec4.yAxis(), -mouse.movementX / 200); + #else + camera.transform.rotate(Vec4.zAxis(), -mouse.movementX / 200); + #end + camera.transform.rotate(camera.right(), -mouse.movementY / 200); + } + } + + #if arm_azerty + static inline var keyUp = "z"; + static inline var keyDown = "s"; + static inline var keyLeft = "q"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "a"; + #else + static inline var keyUp = "w"; + static inline var keyDown = "s"; + static inline var keyLeft = "a"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "q"; + #end +} diff --git a/Sources/armory/trait/internal/Bridge.hx b/Sources/armory/trait/internal/Bridge.hx new file mode 100644 index 0000000000..bded6a7e5a --- /dev/null +++ b/Sources/armory/trait/internal/Bridge.hx @@ -0,0 +1,19 @@ +package armory.trait.internal; + +#if js + +@:expose("iron") +class Bridge { + + public static var App = iron.App; + public static var Scene = iron.Scene; + public static var Time = iron.system.Time; + public static var Input = iron.system.Input; + public static var Object = iron.object.Object; + public static var Data = iron.data.Data; + public static var Vec4 = iron.math.Vec4; + public static var Quat = iron.math.Quat; + public static function log(s: String) { trace(s); }; +} + +#end diff --git a/Sources/armory/trait/internal/CameraController.hx b/Sources/armory/trait/internal/CameraController.hx new file mode 100644 index 0000000000..995d79dfef --- /dev/null +++ b/Sources/armory/trait/internal/CameraController.hx @@ -0,0 +1,60 @@ +package armory.trait.internal; + +import iron.Trait; +import iron.system.Input; +import iron.object.Transform; +import iron.object.CameraObject; +import armory.trait.physics.RigidBody; + +class CameraController extends Trait { + +#if (!arm_physics) + public function new() { super(); } +#else + + var transform: Transform; + var body: RigidBody; + var camera: CameraObject; + + var moveForward = false; + var moveBackward = false; + var moveLeft = false; + var moveRight = false; + var jump = false; + + public function new() { + super(); + + iron.Scene.active.notifyOnInit(function() { + transform = object.transform; + body = object.getTrait(RigidBody); + camera = cast(object.getChildOfType(CameraObject), CameraObject); + }); + + notifyOnUpdate(function() { + var keyboard = Input.getKeyboard(); + moveForward = keyboard.down(keyUp); + moveRight = keyboard.down(keyRight); + moveBackward = keyboard.down(keyDown); + moveLeft = keyboard.down(keyLeft); + jump = keyboard.started("space"); + }); + } + + #if arm_azerty + static inline var keyUp = "z"; + static inline var keyDown = "s"; + static inline var keyLeft = "q"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "a"; + #else + static inline var keyUp = "w"; + static inline var keyDown = "s"; + static inline var keyLeft = "a"; + static inline var keyRight = "d"; + static inline var keyStrafeUp = "e"; + static inline var keyStrafeDown = "q"; + #end +#end +} diff --git a/Sources/armory/trait/internal/CanvasScript.hx b/Sources/armory/trait/internal/CanvasScript.hx new file mode 100644 index 0000000000..a25461619a --- /dev/null +++ b/Sources/armory/trait/internal/CanvasScript.hx @@ -0,0 +1,203 @@ +package armory.trait.internal; + +import iron.Trait; +#if arm_ui +import iron.Scene; +import zui.Zui; +import armory.ui.Canvas; +#end + +class CanvasScript extends Trait { + + public var cnvName: String; +#if arm_ui + + var cui: Zui; + var canvas: TCanvas = null; + + public var ready(get, null): Bool; + function get_ready(): Bool { return canvas != null; } + + var onReadyFuncs: ArrayVoid> = null; + + /** + Create new CanvasScript from canvas + @param canvasName Name of the canvas + @param font font file (Optional) + **/ + public function new(canvasName: String, font: String = Canvas.defaultFontName) { + super(); + cnvName = canvasName; + + iron.data.Data.getBlob(canvasName + ".json", function(blob: kha.Blob) { + + iron.data.Data.getBlob("_themes.json", function(tBlob: kha.Blob) { + if (@:privateAccess tBlob.get_length() != 0) { + Canvas.themes = haxe.Json.parse(tBlob.toString()); + } + else { + trace("\"_themes.json\" is empty! Using default theme instead."); + } + + if (Canvas.themes.length == 0) { + Canvas.themes.push(armory.ui.Themes.light); + } + + iron.data.Data.getFont(font, function(defaultFont: kha.Font) { + var c: TCanvas = haxe.Json.parse(blob.toString()); + if (c.theme == null) c.theme = Canvas.themes[0].NAME; + cui = new Zui({font: defaultFont, theme: Canvas.getTheme(c.theme)}); + + if (c.assets == null || c.assets.length == 0) canvas = c; + else { // Load canvas assets + var loaded = 0; + for (asset in c.assets) { + var file = asset.name; + if (Canvas.isFontAsset(file)) { + iron.data.Data.getFont(file, function(f: kha.Font) { + Canvas.assetMap.set(asset.id, f); + if (++loaded >= c.assets.length) canvas = c; + }); + } else { + iron.data.Data.getImage(file, function(image: kha.Image) { + Canvas.assetMap.set(asset.id, image); + if (++loaded >= c.assets.length) canvas = c; + }); + } + } + } + }); + }); + }); + + notifyOnRender2D(function(g: kha.graphics2.Graphics) { + if (canvas == null) return; + + setCanvasDimensions(kha.System.windowWidth(), kha.System.windowHeight()); + var events = Canvas.draw(cui, canvas, g); + + for (e in events) { + var all = armory.system.Event.get(e); + if (all != null) for (entry in all) entry.onEvent(); + } + + if (onReadyFuncs != null) { + for (f in onReadyFuncs) { + f(); + } + onReadyFuncs.resize(0); + } + }); + } + + /** + Run the given callback function `f` when the canvas is loaded and ready. + + @see https://github.com/armory3d/armory/wiki/traits#canvas-trait-events + **/ + public function notifyOnReady(f: Void->Void) { + if (onReadyFuncs == null) onReadyFuncs = []; + onReadyFuncs.push(f); + } + + /** + Returns an element of the canvas. + @param name The name of the element + @return TElement + **/ + public function getElement(name: String): TElement { + for (e in canvas.elements) if (e.name == name) return e; + return null; + } + + /** + Returns an array of the elements of the canvas. + @return Array + **/ + public function getElements(): Array { + return canvas.elements; + } + + /** + Returns the canvas object of this trait. + @return TCanvas + **/ + public function getCanvas(): Null { + return canvas; + } + + /** + Set the UI scale factor. + **/ + public inline function setUiScale(factor: Float) { + cui.setScale(factor); + } + + /** + Get the UI scale factor. + **/ + public inline function getUiScale(): Float { + return cui.ops.scaleFactor; + } + + /** + Set visibility of canvas + @param visible Whether canvas should be visible or not + **/ + public function setCanvasVisibility(visible: Bool){ + for (e in canvas.elements) e.visible = visible; + } + + /** + Set dimensions of canvas + @param x Width + @param y Height + **/ + public function setCanvasDimensions(x: Int, y: Int){ + canvas.width = x; + canvas.height = y; + } + /** + Set font size of the canvas + @param fontSize Size of font to be setted + **/ + public function setCanvasFontSize(fontSize: Int) { + cui.t.FONT_SIZE = fontSize; + cui.setScale(cui.ops.scaleFactor); + } + + public function getCanvasFontSize(): Int { + return cui.t.FONT_SIZE; + } + + public function setCanvasInputTextFocus(e: Handle, focus: Bool) { + if (focus == true){ + @:privateAccess cui.startTextEdit(e); + } else { + @:privateAccess cui.deselectText(); + } + } + + // Contains data + @:access(armory.ui.Canvas) + @:access(zui.Handle) + public function getHandle(name: String): Handle { + // Consider this a temporary solution + return Canvas.h.children[getElement(name).id]; + } + + public static function getActiveCanvas(): CanvasScript { + var activeCanvas = Scene.active.getTrait(CanvasScript); + if (activeCanvas == null) activeCanvas = Scene.active.camera.getTrait(CanvasScript); + + assert(Error, activeCanvas != null, "Could not find a canvas trait on the active scene or camera"); + + return activeCanvas; + } + +#else + + public function new(canvasName: String) { super(); cnvName = canvasName; } + +#end +} diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx new file mode 100644 index 0000000000..1574dd04af --- /dev/null +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -0,0 +1,1016 @@ +package armory.trait.internal; + +import iron.Trait; +#if arm_debug +import kha.Scheduler; +import armory.ui.Canvas; +import iron.object.CameraObject; +import iron.object.MeshObject; +import zui.Zui; +import zui.Id; +using armory.object.TransformExtension; +#if arm_shadowmap_atlas +import armory.renderpath.Inc.ShadowMapTile; +import armory.renderpath.Inc.ShadowMapAtlas; +#end +#end + +#if arm_debug +@:access(zui.Zui) +@:access(armory.logicnode.LogicNode) +#end +class DebugConsole extends Trait { + +#if (!arm_debug) + public function new() { super(); } +#else + + public static var visible = true; + public static var traceWithPosition = true; + public static var fpsAvg = 0.0; + + static var ui: Zui; + var scaleFactor = 1.0; + + var lastTime = 0.0; + var frameTime = 0.0; + var totalTime = 0.0; + var frames = 0; + + var frameTimeAvg = 0.0; + var frameTimeAvgMin = 0.0; + var frameTimeAvgMax = 0.0; + var renderPathTime = 0.0; + var renderPathTimeAvg = 0.0; + var updateTime = 0.0; + var updateTimeAvg = 0.0; + var animTime = 0.0; + var animTimeAvg = 0.0; + var physTime = 0.0; + var physTimeAvg = 0.0; + var graph: kha.Image = null; + var graphA: kha.Image = null; + var graphB: kha.Image = null; + var benchmark = false; + var benchFrames = 0; + var benchTime = 0.0; + + var selectedObject: iron.object.Object; + var selectedType = ""; + var selectedTraits = new Array(); + static var lrow = [1 / 2, 1 / 2]; + static var row4 = [1 / 4, 1 / 4, 1 / 4, 1 / 4]; + + public static var debugFloat = 1.0; + public static var watchNodes: Array = []; + + static var positionConsole: PositionStateEnum = PositionStateEnum.Right; + var shortcutVisible = kha.input.KeyCode.Tilde; + var shortcutScaleIn = kha.input.KeyCode.OpenBracket; + var shortcutScaleOut = kha.input.KeyCode.CloseBracket; + + #if arm_shadowmap_atlas + var lightColorMap: Map = new Map(); + var lightColorMapCount = 0; + var smaLogicTime = 0.0; + var smaLogicTimeAvg = 0.0; + var smaRenderTime = 0.0; + var smaRenderTimeAvg = 0.0; + #end + + public function new(scaleFactor = 1.0, scaleDebugConsole = 1.0, positionDebugConsole = 2, visibleDebugConsole = 1, + traceWithPosition = 1, keyCodeVisible = kha.input.KeyCode.Tilde, keyCodeScaleIn = kha.input.KeyCode.OpenBracket, + keyCodeScaleOut = kha.input.KeyCode.CloseBracket) { + super(); + this.scaleFactor = scaleFactor; + DebugConsole.traceWithPosition = traceWithPosition == 1; + + iron.data.Data.getFont(Canvas.defaultFontName, function(font: kha.Font) { + ui = new Zui({scaleFactor: scaleFactor, font: font}); + // Set settings + setScale(scaleDebugConsole); + setVisible(visibleDebugConsole == 1); + setPosition(positionDebugConsole); + shortcutVisible = keyCodeVisible; + shortcutScaleIn = keyCodeScaleIn; + shortcutScaleOut = keyCodeScaleOut; + + notifyOnRender2D(render2D); + notifyOnUpdate(update); + if (haxeTrace == null) { + haxeTrace = haxe.Log.trace; + haxe.Log.trace = consoleTrace; + } + // Toggle console + kha.input.Keyboard.get().notify(null, function(key: kha.input.KeyCode) { + // DebugFloat + if (key == kha.input.KeyCode.OpenBracket) { + debugFloat -= 0.1; + trace("debugFloat = " + debugFloat); + } + else if (key == kha.input.KeyCode.CloseBracket){ + debugFloat += 0.1; + trace("debugFloat = " + debugFloat); + } + // Shortcut - Visible + if (key == shortcutVisible) visible = !visible; + // Scale In + else if (key == shortcutScaleIn) { + var debugScale = getScale() - 0.1; + if (debugScale > 0.3) { + setScale(debugScale); + } + } + // Scale Out + else if (key == shortcutScaleOut) { + var debugScale = getScale() + 0.1; + if (debugScale < 10.0) { + setScale(debugScale); + } + } + }, null); + }); + } + + var debugDrawSet = false; + + function selectObject(o: iron.object.Object) { + selectedObject = o; + + if (!debugDrawSet) { + debugDrawSet = true; + armory.trait.internal.DebugDraw.notifyOnRender(function(draw: armory.trait.internal.DebugDraw) { + if (selectedObject != null) draw.bounds(selectedObject.transform); + }); + } + } + + function updateGraph() { + if (graph == null) { + graphA = kha.Image.createRenderTarget(280, 33); + graphB = kha.Image.createRenderTarget(280, 33); + graph = graphA; + } + else graph = graph == graphA ? graphB : graphA; + var graphPrev = graph == graphA ? graphB : graphA; + + graph.g2.begin(true, 0x00000000); + graph.g2.color = 0xffffffff; + graph.g2.drawImage(graphPrev, -3, 0); + + var avg = Math.round(frameTimeAvg * 1000); + var miss = avg > 16.7 ? (avg - 16.7) / 16.7 : 0.0; + graph.g2.color = kha.Color.fromFloats(miss, 1 - miss, 0, 1.0); + graph.g2.fillRect(280 - 3, 33 - avg, 3, avg); + + graph.g2.color = 0xff000000; + graph.g2.fillRect(280 - 3, 33 - 17, 3, 1); + + graph.g2.end(); + } + + static var haxeTrace: Dynamic->haxe.PosInfos->Void = null; + static var lastTraces: Array = [""]; + static function consoleTrace(v: Dynamic, ?inf: haxe.PosInfos) { + lastTraces.unshift(haxe.Log.formatOutput(v, traceWithPosition ? inf : null)); + if (lastTraces.length > 10) lastTraces.pop(); + haxeTrace(v, inf); + } + + function render2D(g: kha.graphics2.Graphics) { + var hwin = Id.handle(); + var htab = Id.handle({position: 0}); + + var avg = Math.round(frameTimeAvg * 10000) / 10; + fpsAvg = avg > 0 ? Math.round(1000 / avg) : 0; + + totalTime += frameTime; + renderPathTime += iron.App.renderPathTime; + frames++; + if (totalTime > 1.0) { + hwin.redraws = 1; + var t = totalTime / frames; + // Second frame + if (frameTimeAvg > 0) { + if (t < frameTimeAvgMin || frameTimeAvgMin == 0) frameTimeAvgMin = t; + if (t > frameTimeAvgMax || frameTimeAvgMax == 0) frameTimeAvgMax = t; + } + + frameTimeAvg = t; + + if (benchmark) { + benchFrames++; + if (benchFrames > 10) benchTime += t; + if (benchFrames == 20) trace(Std.int((benchTime / 10) * 1000000) / 1000); // ms + } + + renderPathTimeAvg = renderPathTime / frames; + updateTimeAvg = updateTime / frames; + animTimeAvg = animTime / frames; + physTimeAvg = physTime / frames; + + #if arm_shadowmap_atlas + smaLogicTimeAvg = smaLogicTime / frames; + smaLogicTime = 0; + + smaRenderTimeAvg = smaRenderTime / frames; + smaRenderTime = 0; + #end + + totalTime = 0; + renderPathTime = 0; + updateTime = 0; + animTime = 0; + physTime = 0; + frames = 0; + + if (htab.position == 2) { + g.end(); + updateGraph(); // Profile tab selected + g.begin(false); + } + } + frameTime = Scheduler.realTime() - lastTime; + lastTime = Scheduler.realTime(); + + if (!visible) return; + var ww = Std.int(280 * scaleFactor * getScale()); + // RIGHT + var wx = iron.App.w() - ww; + var wy = 0; + var wh = iron.App.h(); + // Check position + switch (positionConsole) { + case PositionStateEnum.Left: wx = 0; + case PositionStateEnum.Center: wx = Math.round(iron.App.w() / 2 - ww / 2); + case PositionStateEnum.Right: wx = iron.App.w() - ww; + } + + // var bindG = ui.windowDirty(hwin, wx, wy, ww, wh) || hwin.redraws > 0; + var bindG = true; + if (bindG) g.end(); + + ui.begin(g); + if (ui.window(hwin, wx, wy, ww, wh, true)) { + + if (ui.tab(htab, "")) {} + + if (ui.tab(htab, "Scene")) { + + if (ui.panel(Id.handle({selected: true}), "Outliner")) { + ui.indent(); + ui._y -= ui.ELEMENT_OFFSET(); + + var listX = ui._x; + var listW = ui._w; + + function drawObjectNameInList(object: iron.object.Object, selected: Bool) { + var _y = ui._y; + ui.text(object.name); + + if (object == iron.Scene.active.camera) { + var tagWidth = 100; + var offsetX = listW - tagWidth; + + var prevX = ui._x; + var prevY = ui._y; + var prevW = ui._w; + ui._x = listX + offsetX; + ui._y = _y; + ui._w = tagWidth; + ui.g.color = selected ? kha.Color.White : kha.Color.fromFloats(0.941, 0.914, 0.329, 1.0); + ui.drawString(ui.g, "Active Camera", null, 0, Right); + ui._x = prevX; + ui._y = prevY; + ui._w = prevW; + } + } + + var lineCounter = 0; + function drawList(listHandle: zui.Zui.Handle, currentObject: iron.object.Object) { + if (currentObject.name.charAt(0) == ".") return; // Hidden + var b = false; + + var isLineSelected = currentObject == selectedObject; + + // Highlight every other line + if (lineCounter % 2 == 0) { + ui.g.color = ui.t.SEPARATOR_COL; + ui.g.fillRect(0, ui._y, ui._windowW, ui.ELEMENT_H()); + ui.g.color = 0xffffffff; + } + + // Highlight selected line + if (isLineSelected) { + ui.g.color = 0xff205d9c; + ui.g.fillRect(0, ui._y, ui._windowW, ui.ELEMENT_H()); + ui.g.color = 0xffffffff; + } + + if (currentObject.children.length > 0) { + ui.row([1 / 13, 12 / 13]); + b = ui.panel(listHandle.nest(lineCounter, {selected: true}), "", true, false, false); + drawObjectNameInList(currentObject, isLineSelected); + } + else { + ui._x += 18; // Sign offset + + // Draw line that shows parent relations + ui.g.color = ui.t.ACCENT_COL; + ui.g.drawLine(ui._x - 10, ui._y + ui.ELEMENT_H() / 2, ui._x, ui._y + ui.ELEMENT_H() / 2); + ui.g.color = 0xffffffff; + + drawObjectNameInList(currentObject, isLineSelected); + ui._x -= 18; + } + + lineCounter++; + // Undo applied offset for row drawing caused by endElement() in Zui.hx + ui._y -= ui.ELEMENT_OFFSET(); + + if (ui.isReleased) { + selectObject(currentObject); + } + + if (b) { + var currentY = ui._y; + for (child in currentObject.children) { + ui.indent(); + drawList(listHandle, child); + ui.unindent(); + } + + // Draw line that shows parent relations + ui.g.color = ui.t.ACCENT_COL; + ui.g.drawLine(ui._x + 14, currentY, ui._x + 14, ui._y - ui.ELEMENT_H() / 2); + ui.g.color = 0xffffffff; + } + } + for (c in iron.Scene.active.root.children) { + drawList(Id.handle(), c); + } + + ui.unindent(); + } + + if (selectedObject == null) selectedType = ""; + + if (ui.panel(Id.handle({selected: true}), 'Properties $selectedType')) { + ui.indent(); + + if (selectedObject != null) { + if (Std.isOfType(selectedObject, iron.object.CameraObject) && selectedObject != iron.Scene.active.camera) { + ui.row([1/2, 1/2]); + } + + var h = Id.handle(); + h.selected = selectedObject.visible; + selectedObject.visible = ui.check(h, "Visible"); + + if (Std.isOfType(selectedObject, iron.object.CameraObject) && selectedObject != iron.Scene.active.camera) { + if (ui.button("Set Active Camera")) { + iron.Scene.active.camera = cast selectedObject; + } + } + + var localPos = selectedObject.transform.loc; + var worldPos = selectedObject.transform.getWorldPosition(); + var scale = selectedObject.transform.scale; + var rot = selectedObject.transform.rot.getEuler(); + var dim = selectedObject.transform.dim; + rot.mult(180 / 3.141592); + var f = 0.0; + + ui.text("Transforms"); + ui.indent(); + + ui.row(row4); + ui.text("World Loc"); + // Read-only currently + ui.enabled = false; + h = Id.handle(); + h.text = roundfp(worldPos.x) + ""; + f = Std.parseFloat(ui.textInput(h, "X")); + h = Id.handle(); + h.text = roundfp(worldPos.y) + ""; + f = Std.parseFloat(ui.textInput(h, "Y")); + h = Id.handle(); + h.text = roundfp(worldPos.z) + ""; + f = Std.parseFloat(ui.textInput(h, "Z")); + ui.enabled = true; + + ui.row(row4); + ui.text("Local Loc"); + + h = Id.handle(); + h.text = roundfp(localPos.x) + ""; + f = Std.parseFloat(ui.textInput(h, "X")); + if (h.changed) localPos.x = f; + + h = Id.handle(); + h.text = roundfp(localPos.y) + ""; + f = Std.parseFloat(ui.textInput(h, "Y")); + if (h.changed) localPos.y = f; + + h = Id.handle(); + h.text = roundfp(localPos.z) + ""; + f = Std.parseFloat(ui.textInput(h, "Z")); + if (h.changed) localPos.z = f; + + ui.row(row4); + ui.text("Rotation"); + + h = Id.handle(); + h.text = roundfp(rot.x) + ""; + f = Std.parseFloat(ui.textInput(h, "X")); + var changed = false; + if (h.changed) { changed = true; rot.x = f; } + + h = Id.handle(); + h.text = roundfp(rot.y) + ""; + f = Std.parseFloat(ui.textInput(h, "Y")); + if (h.changed) { changed = true; rot.y = f; } + + h = Id.handle(); + h.text = roundfp(rot.z) + ""; + f = Std.parseFloat(ui.textInput(h, "Z")); + if (h.changed) { changed = true; rot.z = f; } + + if (changed && selectedObject.name != "Scene") { + rot.mult(3.141592 / 180); + selectedObject.transform.rot.fromEuler(rot.x, rot.y, rot.z); + selectedObject.transform.buildMatrix(); + #if arm_physics + var rb = selectedObject.getTrait(armory.trait.physics.RigidBody); + if (rb != null) rb.syncTransform(); + #end + } + + ui.row(row4); + ui.text("Scale"); + + h = Id.handle(); + h.text = roundfp(scale.x) + ""; + f = Std.parseFloat(ui.textInput(h, "X")); + if (h.changed) scale.x = f; + + h = Id.handle(); + h.text = roundfp(scale.y) + ""; + f = Std.parseFloat(ui.textInput(h, "Y")); + if (h.changed) scale.y = f; + + h = Id.handle(); + h.text = roundfp(scale.z) + ""; + f = Std.parseFloat(ui.textInput(h, "Z")); + if (h.changed) scale.z = f; + + ui.row(row4); + ui.text("Dimensions"); + + h = Id.handle(); + h.text = roundfp(dim.x) + ""; + f = Std.parseFloat(ui.textInput(h, "X")); + if (h.changed) dim.x = f; + + h = Id.handle(); + h.text = roundfp(dim.y) + ""; + f = Std.parseFloat(ui.textInput(h, "Y")); + if (h.changed) dim.y = f; + + h = Id.handle(); + h.text = roundfp(dim.z) + ""; + f = Std.parseFloat(ui.textInput(h, "Z")); + if (h.changed) dim.z = f; + + selectedObject.transform.dirty = true; + ui.unindent(); + + if (selectedObject.traits.length > 0) { + ui.text("Traits:"); + ui.indent(); + for (t in selectedObject.traits) { + ui.row([3/4, 1/4]); + ui.text(Type.getClassName(Type.getClass(t))); + + if (ui.button("Details")) { + if (selectedTraits.indexOf(t) == -1) { + selectedTraits.push(t); + } + } + if (ui.isHovered) ui.tooltip("Open details about the trait in another window"); + } + ui.unindent(); + } + + if (selectedObject.name == "Scene") { + selectedType = "(Scene)"; + if (iron.Scene.active.world != null) { + var p = iron.Scene.active.world.probe; + p.raw.strength = ui.slider(Id.handle({value: p.raw.strength}), "Env Strength", 0.0, 5.0, true); + } + else { + ui.text("This scene has no world data to edit."); + } + } + else if (Std.isOfType(selectedObject, iron.object.LightObject)) { + selectedType = "(Light)"; + var light = cast(selectedObject, iron.object.LightObject); + var lightHandle = Id.handle(); + lightHandle.value = light.data.raw.strength / 10; + light.data.raw.strength = ui.slider(lightHandle, "Strength", 0.0, 5.0, true) * 10; + #if arm_shadowmap_atlas + ui.text("status: " + (light.culledLight ? "culled" : "rendered")); + ui.text("shadow map size: " + light.data.raw.shadowmap_size); + #end + } + else if (Std.isOfType(selectedObject, iron.object.CameraObject)) { + selectedType = "(Camera)"; + var cam = cast(selectedObject, iron.object.CameraObject); + var fovHandle = Id.handle(); + fovHandle.value = Std.int(cam.data.raw.fov * 100) / 100; + cam.data.raw.fov = ui.slider(fovHandle, "Field of View", 0.3, 2.0, true); + if (fovHandle.changed) { + cam.buildProjection(); + } + } + else { + selectedType = "(Object)"; + + } + } + + ui.unindent(); + } + } + + if (ui.tab(htab, '$avg ms')) { + + if (ui.panel(Id.handle({selected: true}), "Performance")) { + if (graph != null) ui.image(graph); + ui.indent(); + + ui.row(lrow); + ui.text("Frame"); + ui.text('$avg ms / $fpsAvg fps', Align.Right); + + ui.row(lrow); + ui.text("Render-path"); + ui.text(Math.round(renderPathTimeAvg * 10000) / 10 + " ms", Align.Right); + + ui.row(lrow); + ui.text("Script"); + ui.text(Math.round((updateTimeAvg - physTimeAvg - animTimeAvg) * 10000) / 10 + " ms", Align.Right); + + ui.row(lrow); + ui.text("Animation"); + ui.text(Math.round(animTimeAvg * 10000) / 10 + " ms", Align.Right); + + ui.row(lrow); + ui.text("Physics"); + ui.text(Math.round(physTimeAvg * 10000) / 10 + " ms", Align.Right); + + #if arm_shadowmap_atlas + ui.row(lrow); + ui.text("Shadow Map Atlas (Logic)"); + ui.text(Math.round(smaLogicTimeAvg * 10000) / 10 + " ms", Align.Right); + + ui.row(lrow); + ui.text("Shadow Map Atlas (Render)"); + ui.text(Math.round(smaRenderTimeAvg * 10000) / 10 + " ms", Align.Right); + #end + + ui.unindent(); + } + + if (ui.panel(Id.handle({selected: false}), "Draw")) { + ui.indent(); + + ui.row(lrow); + var numMeshes = iron.Scene.active.meshes.length; + ui.text("Meshes"); + ui.text(numMeshes + "", Align.Right); + + ui.row(lrow); + ui.text("Draw calls"); + ui.text(iron.RenderPath.drawCalls + "", Align.Right); + + ui.row(lrow); + ui.text("Tris mesh"); + ui.text(iron.RenderPath.numTrisMesh + "", Align.Right); + + ui.row(lrow); + ui.text("Tris shadow"); + ui.text(iron.RenderPath.numTrisShadow + "", Align.Right); + + #if arm_batch + ui.row(lrow); + ui.text("Batch calls"); + ui.text(iron.RenderPath.batchCalls + "", Align.Right); + + ui.row(lrow); + ui.text("Batch buckets"); + ui.text(iron.RenderPath.batchBuckets + "", Align.Right); + #end + + ui.row(lrow); + ui.text("Culled"); // Assumes shadow context for all meshes + ui.text(iron.RenderPath.culled + " / " + numMeshes * 2, Align.Right); + + #if arm_stream + ui.row(lrow); + var total = iron.Scene.active.sceneStream.sceneTotal(); + ui.text("Streamed"); + ui.text('$numMeshes / $total', Align.Right); + #end + + ui.unindent(); + } + + #if arm_shadowmap_atlas + if (ui.panel(Id.handle({selected: false}), "Shadow Map Atlases")) { + inline function highLightNext(color: kha.Color = null) { + ui.g.color = color != null ? color : -13882324; + ui.g.fillRect(ui._x, ui._y, ui._windowW, ui.ELEMENT_H()); + ui.g.color = 0xffffffff; + } + + inline function drawScale(text: String, y: Float, fromX: Float, toX: Float, bottom = false) { + var _off = bottom ? -4 : 4; + ui.g.drawLine(fromX, y, toX, y); + ui.g.drawLine(fromX, y, fromX, y + _off); + ui.g.drawLine(toX, y, toX, y + _off); + + var _w = ui._w; + ui._w = Std.int(Math.abs(toX - fromX)); + ui.text(text, Align.Center); + ui._w = _w; + } + + /** + * create a kha Color from HSV (Hue, Saturation, Value) + * @param h expected Hue from [0, 1]. + * @param s expected Saturation from [0, 1]. + * @param v expected Value from [0, 1]. + * @return kha.Color + */ + function colorFromHSV(h: Float, s: Float, v: Float): kha.Color { + // https://stackoverflow.com/a/17243070 + var r = 0.0; var g = 0.0; var b = 0.0; + + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: { r = v; g = t; b = p; } + case 1: { r = q; g = v; b = p; } + case 2: { r = p; g = v; b = t; } + case 3: { r = p; g = q; b = v; } + case 4: { r = t; g = p; b = v; } + case 5: { r = v; g = p; b = q; } + } + + return kha.Color.fromFloats(r, g, b); + } + + function drawTiles(tile: ShadowMapTile, atlas: ShadowMapAtlas, atlasVisualSize: Float) { + var color: Null = kha.Color.fromFloats(0.1, 0.1, 0.1); + var borderColor = color; + var tileScale = (tile.size / atlas.sizew) * atlasVisualSize; //* 0.95; + var x = (tile.coordsX / atlas.sizew) * atlasVisualSize; + var y = (tile.coordsY / atlas.sizew) * atlasVisualSize; + + if (tile.light != null) { + color = lightColorMap.get(tile.light.name); + if (color == null) { + color = colorFromHSV(Math.random(), 0.7, Math.random() * 0.5 + 0.5); + + lightColorMap.set(tile.light.name, color); + lightColorMapCount++; + } + ui.fill(x + tileScale * 0.019, y + tileScale * 0.03, tileScale * 0.96, tileScale * 0.96, color); + } + ui.rect(x, y, tileScale, tileScale, borderColor); + + #if arm_shadowmap_atlas_lod + // draw children tiles + for (t in tile.tiles) + drawTiles(t, atlas, atlasVisualSize); + #end + } + + ui.indent(false); + ui.text("Constants:"); + highLightNext(); + ui.text('Tiles Used Per Point Light: ${ ShadowMapTile.tilesLightType("point") }'); + ui.text('Tiles Used Per Spot Light: ${ ShadowMapTile.tilesLightType("spot") }'); + highLightNext(); + ui.text('Tiles Used For Sun: ${ ShadowMapTile.tilesLightType("sun") }'); + ui.unindent(false); + + ui.indent(false); + var i = 0; + for (atlas in ShadowMapAtlas.shadowMapAtlases) { + if (ui.panel(Id.handle({selected: false}).nest(i), atlas.target )) { + ui.indent(false); + // Draw visual representation of the atlas + var atlasVisualSize = ui._windowW * 0.92; + + drawScale('${atlas.sizew}px', ui._y + ui.ELEMENT_H() * 0.9, ui._x, ui._x + atlasVisualSize); + + // reset light color map when lights are removed + if (lightColorMapCount > iron.Scene.active.lights.length) { + lightColorMap = new Map(); + lightColorMapCount = 0; + } + + for (tile in atlas.tiles) + drawTiles(tile, atlas, atlasVisualSize); + // set vertical space for atlas visual representation + ui._y += atlasVisualSize + 3; + + var tilesRow = atlas.currTileOffset == 0 ? 1 : atlas.currTileOffset; + var tileScale = atlasVisualSize / tilesRow; + drawScale('${atlas.baseTileSizeConst}px', ui._y, ui._x, ui._x + tileScale, true); + + // general atlas information + highLightNext(); + ui.text('Max Atlas Size: ${atlas.maxAtlasSizeConst}, ${atlas.maxAtlasSizeConst} px'); + highLightNext(); + + // show detailed information per light + if (ui.panel(Id.handle({selected: false}).nest(i).nest(0), "Lights in Atlas")) { + ui.indent(false); + var j = 1; + for (tile in atlas.activeTiles) { + var textCol = ui.t.TEXT_COL; + var lightCol = lightColorMap.get(tile.light.name); + if (lightCol != null) + ui.t.TEXT_COL = lightCol; + #if arm_shadowmap_atlas_lod + if (ui.panel(Id.handle({selected: false}).nest(i).nest(j), tile.light.name)) { + ui.t.TEXT_COL = textCol; + ui.indent(false); + ui.text('Shadow Map Size: ${tile.size}, ${tile.size} px'); + ui.unindent(false); + } + #else + ui.indent(false); + ui.text(tile.light.name); + ui.unindent(false); + #end + ui.t.TEXT_COL = textCol; + j++; + } + ui.unindent(false); + } + + // show unused tiles statistics + #if arm_shadowmap_atlas_lod + // WIP + #else + var unusedTiles = atlas.tiles.length; + #if arm_shadowmap_atlas_single_map + for (tile in atlas.activeTiles) + unusedTiles -= ShadowMapTile.tilesLightType(tile.light.data.raw.type); + #else + unusedTiles -= atlas.activeTiles.length * ShadowMapTile.tilesLightType(atlas.lightType); + #end + ui.text('Unused tiles: ${unusedTiles}'); + #end + + var rejectedLightsNames = ""; + if (atlas.rejectedLights.length > 0) { + for (l in atlas.rejectedLights) + rejectedLightsNames += l.name + ", "; + rejectedLightsNames = rejectedLightsNames.substr(0, rejectedLightsNames.length - 2); + highLightNext(kha.Color.fromFloats(0.447, 0.247, 0.188)); + ui.text('Not enough space in atlas for ${atlas.rejectedLights.length} light${atlas.rejectedLights.length > 1 ? "s" : ""}:'); + ui.indent(); + ui.text(${rejectedLightsNames}); + ui.unindent(false); + } + + ui.unindent(false); + } + i++; + } + ui.unindent(false); + ui.unindent(false); + } + #end + + if (ui.panel(Id.handle({selected: false}), "Render Targets")) { + ui.indent(); + #if (kha_opengl || kha_webgl) + ui.imageInvertY = true; + #end + for (rt in iron.RenderPath.active.renderTargets) { + ui.text(rt.raw.name); + if (rt.image != null && !rt.is3D) { + ui.image(rt.image); + } + } + #if (kha_opengl || kha_webgl) + ui.imageInvertY = false; + #end + ui.unindent(); + } + + if (ui.panel(Id.handle({selected: false}), "Cached Materials")) { + ui.indent(); + for (c in iron.data.Data.cachedMaterials) { + ui.text(c.name); + } + ui.unindent(); + } + + if (ui.panel(Id.handle({selected: false}), "Cached Shaders")) { + ui.indent(); + for (c in iron.data.Data.cachedShaders) { + ui.text(c.name); + } + ui.unindent(); + } + + // if (ui.panel(Id.handle({selected: false}), 'Cached Textures')) { + // ui.indent(); + // for (c in iron.data.Data.cachedImages) { + // ui.image(c); + // } + // ui.unindent(); + // } + } + if (ui.tab(htab, lastTraces[0] == "" ? "Console" : lastTraces[0].substr(0, 20))) { + #if js + if (ui.panel(Id.handle({selected: false}), "Script")) { + ui.indent(); + var t = ui.textInput(Id.handle()); + if (ui.button("Run")) { + try { trace("> " + t); js.Lib.eval(t); } + catch (e: Dynamic) { trace(e); } + } + ui.unindent(); + } + #end + if (ui.panel(Id.handle({selected: true}), "Log")) { + ui.indent(); + + final h = Id.handle(); + h.selected = DebugConsole.traceWithPosition; + DebugConsole.traceWithPosition = ui.check(h, "Print With Position"); + if (ui.isHovered) ui.tooltip("Whether to prepend the position of print/trace statements to the printed text"); + + if (ui.button("Clear")) { + lastTraces[0] = ""; + lastTraces.splice(1, lastTraces.length - 1); + } + if (ui.isHovered) ui.tooltip("Clear the log output"); + + final eh = ui.t.ELEMENT_H; + ui.t.ELEMENT_H = ui.fontSize; + for (t in lastTraces) ui.text(t); + ui.t.ELEMENT_H = eh; + + ui.unindent(); + } + } + + if (watchNodes.length > 0 && ui.tab(htab, "Watch")) { + var lineCounter = 0; + for (n in watchNodes) { + if (ui.panel(Id.handle({selected: true}).nest(lineCounter), n.tree.object.name + "." + n.tree.name + "." + n.name + " : ")){ + ui.indent(); + ui.g.color = ui.t.SEPARATOR_COL; + ui.g.fillRect(0, ui._y, ui._windowW, ui.ELEMENT_H()); + ui.g.color = 0xffffffff; + ui.text(Std.string(n.get(0))); + ui.unindent(); + } + lineCounter++; + } + } + + ui.separator(); + } + + // Draw trait debug windows + var handleWinTrait = Id.handle(); + for (trait in selectedTraits) { + var objectID = trait.object.uid; + var traitIndex = trait.object.traits.indexOf(trait); + + var handleWindow = handleWinTrait.nest(objectID).nest(traitIndex); + // This solution is not optimal, dragged windows will change their + // position if the selectedTraits array is changed. + wx = positionConsole == PositionStateEnum.Left ? wx + ww + 8 : wx - ww - 8; + wy = 0; + + handleWindow.redraws = 1; + ui.window(handleWindow, wx, wy, ww, wh, true); + + if (ui.button("Close Trait View")) { + selectedTraits.remove(trait); + handleWinTrait.nest(objectID).unnest(traitIndex); + continue; + } + + ui.row([1/2, 1/2]); + ui.text("Trait:"); + ui.text(Type.getClassName(Type.getClass(trait)), Align.Right); + ui.row([1/2, 1/2]); + ui.text("Extends:"); + ui.text(Type.getClassName(Type.getSuperClass(Type.getClass(trait))), Align.Right); + ui.row([1/2, 1/2]); + ui.text("Object:"); + ui.text(trait.object.name, Align.Right); + ui.separator(); + + if (ui.panel(Id.handle().nest(objectID).nest(traitIndex), "Attributes")) { + ui.indent(); + + for (fieldName in Reflect.fields(trait)) { + ui.row([1/2, 1/2]); + ui.text(fieldName + ""); + + var fieldValue = Reflect.field(trait, fieldName); + var fieldClass = Type.getClass(fieldValue); + + // Treat objects differently (VERY bad performance otherwise) + if (Reflect.isObject(fieldValue) && fieldClass != String) { + + if (fieldClass != null) { + ui.text('<${Type.getClassName(fieldClass)}>', Align.Right); + } else { + // Anonymous data structures for example + ui.text("", Align.Right); + } + } else { + ui.text(Std.string(fieldValue), Align.Right); + } + + } + + ui.unindent(); + } + } + + ui.end(bindG); + if (bindG) g.begin(false); + } + + function update() { + armory.trait.WalkNavigation.enabled = !(ui.isScrolling || ui.dragHandle != null); + updateTime += iron.App.updateTime; + animTime += iron.object.Animation.animationTime; + #if arm_physics + physTime += armory.trait.physics.PhysicsWorld.physTime; + #end + #if arm_shadowmap_atlas + smaLogicTime += armory.renderpath.Inc.shadowsLogicTime; + smaRenderTime += armory.renderpath.Inc.shadowsRenderTime; + #end + } + + static function roundfp(f: Float, precision = 2): Float { + f *= Math.pow(10, precision); + return Math.round(f) / Math.pow(10, precision); + } + + public static function getVisible(): Bool { + return visible; + } + + public static function setVisible(value: Bool) { + visible = value; + } + + public static function getScale(): Float { + return ui.SCALE(); + } + + public static function setScale(value: Float) { + ui.setScale(value); + } + + public static function setPosition(value: PositionStateEnum) { + positionConsole = value; + } + + public static function getPosition(): PositionStateEnum { + return positionConsole; + } + + public static function getFramerate(): Float { + return fpsAvg; + } +#end +} + +enum abstract PositionStateEnum(Int) from Int { + var Left; + var Center; + var Right; +} diff --git a/Sources/armory/trait/internal/DebugDraw.hx b/Sources/armory/trait/internal/DebugDraw.hx new file mode 100644 index 0000000000..6ff3b1bfc6 --- /dev/null +++ b/Sources/armory/trait/internal/DebugDraw.hx @@ -0,0 +1,215 @@ +package armory.trait.internal; + +#if arm_debug + +import kha.graphics4.PipelineState; +import kha.graphics4.VertexStructure; +import kha.graphics4.VertexBuffer; +import kha.graphics4.IndexBuffer; +import kha.graphics4.VertexData; +import kha.graphics4.Usage; +import kha.graphics4.ConstantLocation; +import kha.graphics4.CompareMode; +import kha.graphics4.CullMode; +import iron.math.Vec4; +import iron.math.Mat4; +using armory.object.TransformExtension; + +class DebugDraw { + + static var inst: DebugDraw = null; + + public var color: kha.Color = 0xffff0000; + public var strength = 0.02; + + var vertexBuffer: VertexBuffer; + var indexBuffer: IndexBuffer; + var pipeline: PipelineState; + + var vp: Mat4; + var vpID: ConstantLocation; + + var vbData: kha.arrays.Float32Array; + var ibData: kha.arrays.Uint32Array; + + static inline var maxLines = 300; + static inline var maxVertices = maxLines * 4; + static inline var maxIndices = maxLines * 6; + var lines = 0; + + function new() { + inst = this; + + var structure = new VertexStructure(); + structure.add("pos", VertexData.Float3); + structure.add("col", VertexData.Float3); + pipeline = new PipelineState(); + pipeline.inputLayout = [structure]; + #if arm_deferred + pipeline.fragmentShader = kha.Shaders.line_deferred_frag; + #else + pipeline.fragmentShader = kha.Shaders.line_frag; + #end + pipeline.vertexShader = kha.Shaders.line_vert; + pipeline.depthWrite = true; + pipeline.depthMode = CompareMode.Less; + pipeline.cullMode = CullMode.None; + pipeline.compile(); + vpID = pipeline.getConstantLocation("ViewProjection"); + vp = Mat4.identity(); + + vertexBuffer = new VertexBuffer(maxVertices, structure, Usage.DynamicUsage); + indexBuffer = new IndexBuffer(maxIndices, Usage.DynamicUsage); + } + + static var g: kha.graphics4.Graphics; + + public static function notifyOnRender(f: DebugDraw->Void) { + if (inst == null) inst = new DebugDraw(); + iron.RenderPath.notifyOnContext("mesh", function(g4: kha.graphics4.Graphics, i: Int, len: Int) { + g = g4; + if (i == 0) inst.begin(); + f(inst); + if (i == len - 1) inst.end(); + }); + } + + static var objPosition: Vec4; + static var vx = new Vec4(); + static var vy = new Vec4(); + static var vz = new Vec4(); + public function bounds(transform: iron.object.Transform) { + objPosition = transform.getWorldPosition(); + var dx = transform.dim.x / 2; + var dy = transform.dim.y / 2; + var dz = transform.dim.z / 2; + + var up = transform.world.up(); + var look = transform.world.look(); + var right = transform.world.right(); + up.normalize(); + look.normalize(); + right.normalize(); + + vx.setFrom(right); + vx.mult(dx); + vy.setFrom(look); + vy.mult(dy); + vz.setFrom(up); + vz.mult(dz); + + lineb(-1, -1, -1, 1, -1, -1); + lineb(-1, 1, -1, 1, 1, -1); + lineb(-1, -1, 1, 1, -1, 1); + lineb(-1, 1, 1, 1, 1, 1); + + lineb(-1, -1, -1, -1, 1, -1); + lineb(-1, -1, 1, -1, 1, 1); + lineb( 1, -1, -1, 1, 1, -1); + lineb( 1, -1, 1, 1, 1, 1); + + lineb(-1, -1, -1, -1, -1, 1); + lineb(-1, 1, -1, -1, 1, 1); + lineb( 1, -1, -1, 1, -1, 1); + lineb( 1, 1, -1, 1, 1, 1); + } + + static var v1 = new Vec4(); + static var v2 = new Vec4(); + static var t = new Vec4(); + function lineb(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) { + v1.setFrom(objPosition); + t.setFrom(vx); t.mult(a); v1.add(t); + t.setFrom(vy); t.mult(b); v1.add(t); + t.setFrom(vz); t.mult(c); v1.add(t); + + v2.setFrom(objPosition); + t.setFrom(vx); t.mult(d); v2.add(t); + t.setFrom(vy); t.mult(e); v2.add(t); + t.setFrom(vz); t.mult(f); v2.add(t); + + linev(v1, v2); + } + + public inline function linev(v1: Vec4, v2: Vec4) { + line(v1.x, v1.y, v1.z, v2.x, v2.y, v2.z); + } + + static var midPoint = new Vec4(); + static var midLine = new Vec4(); + static var corner1 = new Vec4(); + static var corner2 = new Vec4(); + static var corner3 = new Vec4(); + static var corner4 = new Vec4(); + static var cameraLook = new Vec4(); + public function line(x1: Float, y1: Float, z1: Float, x2: Float, y2: Float, z2: Float) { + + if (lines >= maxLines) { end(); begin(); } + + midPoint.set(x1 + x2, y1 + y2, z1 + z2); + midPoint.mult(0.5); + + midLine.set(x1, y1, z1); + midLine.sub(midPoint); + + var camera = iron.Scene.active.camera; + cameraLook = camera.transform.getWorldPosition(); + cameraLook.sub(midPoint); + + var lineWidth = cameraLook.cross(midLine); + lineWidth.normalize(); + lineWidth.mult(strength); + + corner1.set(x1, y1, z1).add(lineWidth); + corner2.set(x1, y1, z1).sub(lineWidth); + corner3.set(x2, y2, z2).sub(lineWidth); + corner4.set(x2, y2, z2).add(lineWidth); + + var i = lines * 24; // 4 * 6 (structure len) + addVbData(i, [corner1.x, corner1.y, corner1.z, color.R, color.G, color.B]); + i += 6; + addVbData(i, [corner2.x, corner2.y, corner2.z, color.R, color.G, color.B]); + i += 6; + addVbData(i, [corner3.x, corner3.y, corner3.z, color.R, color.G, color.B]); + i += 6; + addVbData(i, [corner4.x, corner4.y, corner4.z, color.R, color.G, color.B]); + + i = lines * 6; + ibData[i ] = lines * 4; + ibData[i + 1] = lines * 4 + 1; + ibData[i + 2] = lines * 4 + 2; + ibData[i + 3] = lines * 4 + 2; + ibData[i + 4] = lines * 4 + 3; + ibData[i + 5] = lines * 4; + + lines++; + } + + function begin() { + lines = 0; + vbData = vertexBuffer.lock(); + ibData = indexBuffer.lock(); + } + + function end() { + vertexBuffer.unlock(); + indexBuffer.unlock(); + + g.setVertexBuffer(vertexBuffer); + g.setIndexBuffer(indexBuffer); + g.setPipeline(pipeline); + var camera = iron.Scene.active.camera; + vp.setFrom(camera.V); + vp.multmat(camera.P); + g.setMatrix(vpID, vp.self); + g.drawIndexedVertices(0, lines * 6); + } + + inline function addVbData(i: Int, data: Array) { + for (offset in 0...6) { + vbData.set(i + offset, data[offset]); + } + } +} + +#end diff --git a/Sources/armory/trait/internal/LivePatch.hx b/Sources/armory/trait/internal/LivePatch.hx new file mode 100644 index 0000000000..743f66be29 --- /dev/null +++ b/Sources/armory/trait/internal/LivePatch.hx @@ -0,0 +1,195 @@ +package armory.trait.internal; + +import armory.logicnode.LogicNode; +import armory.logicnode.LogicTree; + + +#if arm_patch @:expose("LivePatch") #end +@:access(armory.logicnode.LogicNode) +@:access(armory.logicnode.LogicNodeLink) +class LivePatch extends iron.Trait { + +#if !arm_patch + public function new() { super(); } +#else + + static var patchId = 0; + + public function new() { + super(); + notifyOnUpdate(update); + } + + function update() { + kha.Assets.loadBlobFromPath("krom.patch", function(b: kha.Blob) { + if (b.length == 0) return; + var lines = b.toString().split("\n"); + var id = Std.parseInt(lines[0]); + if (id > patchId) { + patchId = id; + js.Lib.eval(lines[1]); + } + }); + } + + public static function patchCreateNodeLink(treeName: String, fromNodeName: String, toNodeName: String, fromIndex: Int, toIndex: Int) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var fromNode = tree.nodes[fromNodeName]; + var toNode = tree.nodes[toNodeName]; + if (fromNode == null || toNode == null) return; + + LogicNode.addLink(fromNode, toNode, fromIndex, toIndex); + } + } + + public static function patchSetNodeLinks(treeName: String, nodeName: String, inputDatas: Array, outputDatas: Array>) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var node = tree.nodes[nodeName]; + if (node == null) return; + + node.clearInputs(); + node.clearOutputs(); + + for (inputData in inputDatas) { + var fromNode: LogicNode; + var fromIndex: Int; + + if (inputData.isLinked) { + fromNode = tree.nodes[inputData.fromNode]; + if (fromNode == null) continue; + fromIndex = inputData.fromIndex; + } + else { + fromNode = LogicNode.createSocketDefaultNode(node.tree, inputData.socketType, inputData.socketValue); + fromIndex = 0; + } + + LogicNode.addLink(fromNode, node, fromIndex, inputData.toIndex); + } + + for (outputData in outputDatas) { + for (linkData in outputData) { + var toNode: LogicNode; + var toIndex: Int; + + if (linkData.isLinked) { + toNode = tree.nodes[linkData.toNode]; + if (toNode == null) continue; + toIndex = linkData.toIndex; + } + else { + toNode = LogicNode.createSocketDefaultNode(node.tree, linkData.socketType, linkData.socketValue); + toIndex = 0; + } + + LogicNode.addLink(node, toNode, linkData.fromIndex, toIndex); + } + } + } + } + + public static function patchUpdateNodeProp(treeName: String, nodeName: String, propName: String, value: Dynamic) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var node = tree.nodes[nodeName]; + if (node == null) return; + + Reflect.setField(node, propName, value); + } + } + + public static function patchUpdateNodeInputVal(treeName: String, nodeName: String, socketIndex: Int, value: Dynamic) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var node = tree.nodes[nodeName]; + if (node == null) return; + + node.inputs[socketIndex].set(value); + } + } + + public static function patchNodeDelete(treeName: String, nodeName: String) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var node = tree.nodes[nodeName]; + if (node == null) return; + + node.clearOutputs(); + node.clearInputs(); + tree.nodes.remove(nodeName); + } + } + + public static function patchNodeCreate(treeName: String, nodeName: String, nodeType: String, propDatas: Array>, inputDatas: Array>, outputDatas: Array>) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + // No further constructor parameters required here, all variable nodes + // use optional further parameters and all values are set later in this + // function. + var newNode: LogicNode = Type.createInstance(Type.resolveClass(nodeType), [tree]); + newNode.name = nodeName; + tree.nodes[nodeName] = newNode; + + for (propData in propDatas) { + Reflect.setField(newNode, propData[0], propData[1]); + } + + var i = 0; + for (inputData in inputDatas) { + LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++); + } + + i = 0; + for (outputData in outputDatas) { + LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0); + } + } + } + + public static function patchNodeCopy(treeName: String, nodeName: String, newNodeName: String, copyProps: Array, inputDatas: Array>, outputDatas: Array>) { + if (!LogicTree.nodeTrees.exists(treeName)) return; + var trees = LogicTree.nodeTrees[treeName]; + + for (tree in trees) { + var node = tree.nodes[nodeName]; + if (node == null) return; + + // No further constructor parameters required here, all variable nodes + // use optional further parameters and all values are set later in this + // function. + var newNode: LogicNode = Type.createInstance(Type.getClass(node), [tree]); + newNode.name = newNodeName; + tree.nodes[newNodeName] = newNode; + + for (propName in copyProps) { + Reflect.setField(newNode, propName, Reflect.field(node, propName)); + } + + var i = 0; + for (inputData in inputDatas) { + LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++); + } + + i = 0; + for (outputData in outputDatas) { + LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0); + } + } + } + +#end +} diff --git a/Sources/armory/trait/internal/LoadingScreen.hx b/Sources/armory/trait/internal/LoadingScreen.hx new file mode 100644 index 0000000000..953897e410 --- /dev/null +++ b/Sources/armory/trait/internal/LoadingScreen.hx @@ -0,0 +1,11 @@ +package armory.trait.internal; + +// To create a custom loading screen copy this file to blend_root/Sources/arm/LoadingScreen.hx + +class LoadingScreen { + + public static function render(g: kha.graphics2.Graphics, assetsLoaded: Int, assetsTotal: Int) { + g.color = 0xffcf2b43; + g.fillRect(0, iron.App.h() - 6, iron.App.w() / assetsTotal * assetsLoaded, 6); + } +} diff --git a/Sources/armory/trait/internal/MovieTexture.hx b/Sources/armory/trait/internal/MovieTexture.hx new file mode 100644 index 0000000000..3adee41e21 --- /dev/null +++ b/Sources/armory/trait/internal/MovieTexture.hx @@ -0,0 +1,90 @@ +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; + var image: Image; + + var videoName: String; + + function pow(pow: Int): Int { + var ret = 1; + for (i in 0...pow) ret *= 2; + return ret; + } + + function getPower2(i: Int): Int { + var power = 0; + while (true) { + var res = pow(power); + if (res >= i) return res; + power++; + } + } + + public function new(videoName: String) { + super(); + + this.videoName = videoName; + + notifyOnInit(init); + } + + function init() { + iron.data.Data.getVideo(videoName, function(vid: kha.Video) { + video = vid; + video.play(true); + + var w = getPower2(video.width()); + var h = getPower2(video.height()); + + // 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); + }); + } + + function render(g: kha.graphics2.Graphics) { + g.end(); + + var g2 = image.g2; + g2.begin(true, 0xff000000); + g2.color = 0xffffffff; + g2.drawVideo(video, 0, 0, image.width, image.height); + g2.end(); + + g.begin(false); + } +} diff --git a/Sources/armory/trait/internal/TerrainPhysics.hx b/Sources/armory/trait/internal/TerrainPhysics.hx new file mode 100644 index 0000000000..5255d20ac9 --- /dev/null +++ b/Sources/armory/trait/internal/TerrainPhysics.hx @@ -0,0 +1,34 @@ +package armory.trait.internal; + +import iron.Trait; +import armory.trait.physics.RigidBody; + +#if arm_terrain + +class TerrainPhysics extends Trait { + + public function new() { + super(); + notifyOnInit(init); + } + + function init() { + var stream = iron.Scene.active.terrainStream; + stream.notifyOnReady(function() { + for (sector in stream.sectors) { + // Heightmap to bytes + var tex = stream.heightTextures[sector.uid]; + var p = tex.getPixels(); + var b = haxe.io.Bytes.alloc(tex.width * tex.height); + for (i in 0...b.length) b.set(i, p.get(i * 4)); + + // Shape.Terrain, mass + var rb = new RigidBody(7, 0); + rb.heightData = b; + sector.addTrait(rb); + } + }); + } +} + +#end diff --git a/Sources/armory/trait/internal/UniformsManager.hx b/Sources/armory/trait/internal/UniformsManager.hx new file mode 100644 index 0000000000..3be9499b10 --- /dev/null +++ b/Sources/armory/trait/internal/UniformsManager.hx @@ -0,0 +1,372 @@ +package armory.trait.internal; + +import iron.object.DecalObject; +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(); + + notifyOnAdd(init); + notifyOnRemove(removeObject); + + if (!sceneRemoveInitalized) { + Scene.active.notifyOnRemove(removeScene); + } + } + + function init() { + if (Std.isOfType(object, MeshObject)) { + var materials = cast(object, MeshObject).materials; + + for (material in materials) { + + var exists = registerShaderUniforms(material); + if (exists) { + uniformExists = true; + } + } + } + #if rp_decals + if (Std.isOfType(object, DecalObject)) { + var material = cast(object, DecalObject).material; + + var exists = registerShaderUniforms(material); + if (exists) { + uniformExists = true; + } + + } + #end + } + + static function removeScene() { + removeObjectFromAllMaps(Scene.active.root); + } + + function removeObject() { + removeObjectFromAllMaps(object); + } + + // Helper method to register float, vec3 and texture getter functions + static function register(type: UniformType) { + switch (type) { + case Float: + if (!floatsRegistered) { + floatsRegistered = true; + Uniforms.externalFloatLinks.push(floatLink); + } + case Vector: + if (!vectorsRegistered) { + vectorsRegistered = true; + Uniforms.externalVec3Links.push(vec3Link); + } + case Texture: + if (!texturesRegistered) { + texturesRegistered = true; + Uniforms.externalTextureLinks.push(textureLink); + } + } + } + + // Register and map shader uniforms if it is an armory shader parameter + public static function registerShaderUniforms(material: MaterialData) : Bool { + + var uniformExist = false; + + if (!floatsMap.exists(Scene.active.root)) floatsMap.set(Scene.active.root, null); + if (!vectorsMap.exists(Scene.active.root)) vectorsMap.set(Scene.active.root, null); + if (!texturesMap.exists(Scene.active.root)) texturesMap.set(Scene.active.root, null); + + for (context in material.shader.raw.contexts) { // For each context in shader + for (constant in context.constants) { // For each constant in the context + if (constant.is_arm_parameter) { // Check if armory parameter + + uniformExist = true; + var object = Scene.active.root; // Map default uniforms to scene root + + switch (constant.type) { + case "float": + var link = constant.link; + var value = constant.float; + setFloatValue(material, object, link, value); + register(Float); + + case "vec3": + var vec = new Vec4(); + vec.x = constant.vec3.get(0); + vec.y = constant.vec3.get(1); + vec.z = constant.vec3.get(2); + + setVec3Value(material, object, constant.link, vec); + register(Vector); + } + } + } + for (texture in context.texture_units) { + if (texture.is_arm_parameter) { // Check if armory parameter + + uniformExist = true; + var object = Scene.active.root; // Map default texture to scene root + + if (texture.default_image_file == null) { + setTextureValue(material, object, texture.link, null); + + } + else { + iron.data.Data.getImage(texture.default_image_file, function(image: kha.Image) { + setTextureValue(material, object, texture.link, image); + }); + } + register(Texture); + } + } + } + return uniformExist; + } + + // Method to set map Object -> Material -> Link -> FLoat + public static function setFloatValue(material: MaterialData, object: Object, link: String, value: Null) { + + 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 + } + + // Method to get object specific material parameter float value + public static function floatLink(object: Object, mat: MaterialData, link: String): Null { + + if (object == null || mat == null) return null; + + // First check if float exists per object + var res = getObjectFloatLink(object, mat, link); + if (res == null) { + // If not defined per object, use default scene root + res = getObjectFloatLink(Scene.active.root, mat, link); + } + return res; + } + + // Get float link + static function getObjectFloatLink(object: Object, mat: MaterialData, link: String): Null { + + var material = floatsMap.get(object); + if (material == null) return null; + + var entry = material.get(mat); + if (entry == null) return null; + + return entry.get(link); + } + + // Method to get object specific material parameter vector value + public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + + if (object == null || mat == null) return null; + + // First check if vector exists per object + var res = getObjectVec3Link(object, mat, link); + if (res == null) { + // If not defined per object, use default scene root + res = getObjectVec3Link(Scene.active.root, mat, link); + } + return res; + } + + // Get vector link + static function getObjectVec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + + var material = vectorsMap.get(object); + if (material == null) return null; + + var entry = material.get(mat); + if (entry == null) return null; + + return entry.get(link); + } + + // Method to get object specific material parameter texture value + public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { + + if (object == null || mat == null) return null; + + // First check if texture exists per object + var res = getObjectTextureLink(object, mat, link); + if (res == null) { + // If not defined per object, use default scene root + res = getObjectTextureLink(Scene.active.root, mat, link); + } + return res; + } + + // Get texture link + static function getObjectTextureLink(object: Object, mat: MaterialData, link: String): kha.Image { + + 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); + } + } + + public static function removeFloatValue(object: Object, mat:MaterialData, link: String) { + + var material = floatsMap.get(object); + if (material == null) return; + + var entry = material.get(mat); + if (entry == null) return; + + entry.remove(link); + + if (!entry.keys().hasNext()) material.remove(mat); + if (!material.keys().hasNext()) floatsMap.remove(object); + } + + public static function removeVectorValue(object: Object, mat:MaterialData, link: String) { + + var material = vectorsMap.get(object); + if (material == null) return; + + var entry = material.get(mat); + if (entry == null) return; + + entry.remove(link); + + if (!entry.keys().hasNext()) material.remove(mat); + if (!material.keys().hasNext()) vectorsMap.remove(object); + } + + public static function removeTextureValue(object: Object, mat:MaterialData, link: String) { + + var material = texturesMap.get(object); + if (material == null) return; + + var entry = material.get(mat); + if (entry == null) return; + + entry.remove(link); + + if (!entry.keys().hasNext()) material.remove(mat); + if (!material.keys().hasNext()) texturesMap.remove(object); + } +} + +enum abstract UniformType(Int) from Int to Int { + var Float = 0; + var Vector = 1; + var Texture = 2; +} diff --git a/Sources/armory/trait/internal/WasmScript.hx b/Sources/armory/trait/internal/WasmScript.hx new file mode 100644 index 0000000000..cad31ec07e --- /dev/null +++ b/Sources/armory/trait/internal/WasmScript.hx @@ -0,0 +1,91 @@ +package armory.trait.internal; + +#if js + +import iron.data.Data; +import iron.data.Wasm; +import iron.system.Input; +import iron.system.Time; + +class WasmScript extends iron.Trait { + + var wasm: Wasm; + public var wasmName: String; + + var objectMap: Map = new Map(); + + public function new(handle: String) { + super(); + wasmName = handle; + + // Armory API exposed to WebAssembly + // TODO: static + /*static*/ var imports = { + env: { + trace: function(i: Int) { trace(wasm.getString(i)); }, + tracef: function(f: Float) { trace(f); }, + tracei: function(i: Int) { trace(i); }, + + notify_on_update: function(i: Int) { notifyOnUpdate(wasm.exports.update); }, + remove_update: function(i: Int) { removeUpdate(wasm.exports.update); }, + get_object: function(name: Int) { + var s = wasm.getString(name); + var o = iron.Scene.active.getChild(s); + if (o == null) return -1; + objectMap.set(o.uid, o); + return o.uid; + }, + set_transform: function(object: Int, x: Float, y: Float, z: Float, rx: Float, ry: Float, rz: Float, sx: Float, sy: Float, sz: Float) { + var o = objectMap.get(object); + if (o == null) return; + var t = o.transform; + t.loc.set(x, y, z); + t.scale.set(sx, sy, sz); + t.setRotation(rx, ry, rz); + }, + set_location: function(object: Int, x: Float, y: Float, z: Float) { + var o = objectMap.get(object); + if (o == null) return; + var t = o.transform; + t.loc.set(x, y, z); + }, + set_scale: function(object: Int, x: Float, y: Float, z: Float) { + var o = objectMap.get(object); + if (o == null) return; + var t = o.transform; + t.scale.set(x, y, z); + }, + set_rotation: function(object: Int, x: Float, y: Float, z: Float) { + var o = objectMap.get(object); + if (o == null) return; + var t = o.transform; + t.setRotation(x, y, z); + }, + + mouse_x: function() { return Input.getMouse().x; }, + mouse_y: function() { return Input.getMouse().y; }, + mouse_started: function(button: Int) { return Input.getMouse().started(); }, + mouse_down: function(button: Int) { return Input.getMouse().down(); }, + mouse_released: function(button: Int) { return Input.getMouse().released(); }, + key_started: function(key: Int) { return Input.getKeyboard().started(Keyboard.keyCode(cast key)); }, + key_down: function(key: Int) { return Input.getKeyboard().down(Keyboard.keyCode(cast key)); }, + key_released: function(key: Int) { return Input.getKeyboard().released(Keyboard.keyCode(cast key)); }, + + time_real: function() { return Time.time(); }, + time_delta: function() { return Time.delta; }, + + js_eval: function(fn: Int) { js.Lib.eval(wasm.getString(fn)); }, + js_call_object: function(object: Int, fn: Int) { Reflect.callMethod(objectMap.get(object), Reflect.field(objectMap.get(object), wasm.getString(fn)), null); }, + js_call_static: function(path: Int, fn: Int) { var cpath = wasm.getString(path); var ctype = Type.resolveClass(cpath); Reflect.callMethod(ctype, Reflect.field(ctype, wasm.getString(fn)), []); }, + + }, + }; + + Data.getBlob(handle + ".wasm", function(b: kha.Blob) { + wasm = Wasm.instance(b, imports); + wasm.exports.main(); + }); + } +} + +#end diff --git a/Sources/armory/trait/internal/wasm_api.h b/Sources/armory/trait/internal/wasm_api.h new file mode 100644 index 0000000000..2e5a0e01c9 --- /dev/null +++ b/Sources/armory/trait/internal/wasm_api.h @@ -0,0 +1,44 @@ + +/* C API */ + +void trace(const char* s); +void tracef(float f); +void tracei(int i); + +typedef void(*func_update)(void); +void notify_on_update(func_update f); +void remove_update(func_update f); + +int get_object(const char* name); +void set_transform(int object, float x, float y, float z, float rx, float ry, float rz, float sx, float sy, float sz); +void set_location(int object, float x, float y, float z); +void set_scale(int object, float x, float y, float z); +void set_rotation(int object, float x, float y, float z); + +int mouse_x(void); +int mouse_y(void); +int mouse_started(int button); +int mouse_down(int button); +int mouse_released(int button); +int key_started(int key); // kha.input.KeyCode +int key_down(int key); +int key_released(int key); + +float time_real(void); +float time_delta(void); + +void js_eval(const char* fn); +void js_call_object(int object, const char* fn); +void js_call_static(const char* path, const char* fn); + +/* C template +#define WASM_EXPORT __attribute__((visibility("default"))) + +void logs(const char* s); + +WASM_EXPORT +int main() { + logs("Hello, world!"); + return 0; +} +*/ diff --git a/Sources/armory/trait/navigation/Navigation.hx b/Sources/armory/trait/navigation/Navigation.hx new file mode 100644 index 0000000000..99ab5f6ac3 --- /dev/null +++ b/Sources/armory/trait/navigation/Navigation.hx @@ -0,0 +1,26 @@ +package armory.trait.navigation; + +#if arm_navigation +import haxerecast.Recast; +import armory.trait.NavMesh; +#end + +class Navigation extends iron.Trait { + +#if (!arm_navigation) + public function new() { super(); } +#else + + public static var active: Navigation = null; + + public var navMeshes: Array = []; + public var recast: Recast; + + public function new() { + super(); + active = this; + + recast = new Recast(); + } +#end +} diff --git a/Sources/armory/trait/physics/KinematicCharacterController.hx b/Sources/armory/trait/physics/KinematicCharacterController.hx new file mode 100644 index 0000000000..1e7d89ae0b --- /dev/null +++ b/Sources/armory/trait/physics/KinematicCharacterController.hx @@ -0,0 +1,15 @@ +package armory.trait.physics; + +#if (!arm_physics) + +class KinematicCharacterController extends iron.Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef KinematicCharacterController = armory.trait.physics.bullet.KinematicCharacterController; + + #end + +#end diff --git a/Sources/armory/trait/physics/PhysicsConstraint.hx b/Sources/armory/trait/physics/PhysicsConstraint.hx new file mode 100644 index 0000000000..4fe450bb18 --- /dev/null +++ b/Sources/armory/trait/physics/PhysicsConstraint.hx @@ -0,0 +1,19 @@ +package armory.trait.physics; + +#if (!arm_physics) + +class PhysicsConstraint extends iron.Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef PhysicsConstraint = armory.trait.physics.bullet.PhysicsConstraint; + + #else + + typedef PhysicsConstraint = armory.trait.physics.oimo.PhysicsConstraint; + + #end + +#end diff --git a/Sources/armory/trait/physics/PhysicsHook.hx b/Sources/armory/trait/physics/PhysicsHook.hx new file mode 100644 index 0000000000..ccf1c8a867 --- /dev/null +++ b/Sources/armory/trait/physics/PhysicsHook.hx @@ -0,0 +1,19 @@ +package armory.trait.physics; + +#if (!arm_physics) + +class PhysicsHook extends iron.Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef PhysicsHook = armory.trait.physics.bullet.PhysicsHook; + + #else + + typedef PhysicsHook = armory.trait.physics.oimo.PhysicsHook; + + #end + +#end diff --git a/Sources/armory/trait/physics/PhysicsWorld.hx b/Sources/armory/trait/physics/PhysicsWorld.hx new file mode 100644 index 0000000000..921e9d3d2c --- /dev/null +++ b/Sources/armory/trait/physics/PhysicsWorld.hx @@ -0,0 +1,19 @@ +package armory.trait.physics; + +#if (!arm_physics) + +class PhysicsWorld extends iron.Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef PhysicsWorld = armory.trait.physics.bullet.PhysicsWorld; + + #else + + typedef PhysicsWorld = armory.trait.physics.oimo.PhysicsWorld; + + #end + +#end diff --git a/Sources/armory/trait/physics/RigidBody.hx b/Sources/armory/trait/physics/RigidBody.hx new file mode 100644 index 0000000000..8235099933 --- /dev/null +++ b/Sources/armory/trait/physics/RigidBody.hx @@ -0,0 +1,19 @@ +package armory.trait.physics; + +#if (!arm_physics) + +class RigidBody extends iron.Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef RigidBody = armory.trait.physics.bullet.RigidBody; + + #else + + typedef RigidBody = armory.trait.physics.oimo.RigidBody; + + #end + +#end diff --git a/Sources/armory/trait/physics/SoftBody.hx b/Sources/armory/trait/physics/SoftBody.hx new file mode 100644 index 0000000000..de7015d313 --- /dev/null +++ b/Sources/armory/trait/physics/SoftBody.hx @@ -0,0 +1,19 @@ +package armory.trait.physics; + +#if (!arm_physics_soft) + +class SoftBody extends Trait { public function new() { super(); } } + +#else + + #if arm_bullet + + typedef SoftBody = armory.trait.physics.bullet.SoftBody; + + #else + + typedef SoftBody = armory.trait.physics.oimo.SoftBody; + + #end + +#end diff --git a/Sources/armory/trait/physics/bullet/KinematicCharacterController.hx b/Sources/armory/trait/physics/bullet/KinematicCharacterController.hx new file mode 100644 index 0000000000..bc44fea307 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/KinematicCharacterController.hx @@ -0,0 +1,364 @@ +package armory.trait.physics.bullet; + +#if arm_bullet + +import iron.Trait; +import iron.math.Vec4; +import iron.math.Quat; +import iron.object.Transform; +import iron.object.MeshObject; + +class KinematicCharacterController extends Trait { + + var shape: ControllerShape; + var shapeConvex: bullet.Bt.ConvexShape; + var shapeConvexHull: bullet.Bt.ConvexHullShape; + var isConvexHull = false; + + public var physics: PhysicsWorld; + public var transform: Transform = null; + public var mass: Float; + public var friction: Float; + public var restitution: Float; + public var collisionMargin: Float; + public var animated: Bool; + public var group = 1; + var bodyScaleX: Float; // Transform scale at creation time + var bodyScaleY: Float; + var bodyScaleZ: Float; + var currentScaleX: Float; + var currentScaleY: Float; + var currentScaleZ: Float; + var jumpSpeed: Float; + + public var body: bullet.Bt.PairCachingGhostObject = null; + public var character: bullet.Bt.KinematicCharacterController = null; + public var ready = false; + static var nextId = 0; + public var id = 0; + public var onReady: Void->Void = null; + + static var nullvec = true; + static var vec1: bullet.Bt.Vector3; + static var quat1: bullet.Bt.Quaternion; + static var trans1: bullet.Bt.Transform; + static var quat = new Quat(); + + static inline var CF_CHARACTER_OBJECT = 16; + + public function new(mass = 1.0, shape = ControllerShape.Capsule, jumpSpeed = 8.0, friction = 0.5, restitution = 0.0, + collisionMargin = 0.0, animated = false, group = 1) { + super(); + + if (nullvec) { + nullvec = false; + vec1 = new bullet.Bt.Vector3(0, 0, 0); + quat1 = new bullet.Bt.Quaternion(0, 0, 0, 0); + trans1 = new bullet.Bt.Transform(); + } + + this.mass = mass; + this.jumpSpeed = jumpSpeed; + this.shape = shape; + this.friction = friction; + this.restitution = restitution; + this.collisionMargin = collisionMargin; + this.animated = animated; + this.group = group; + + notifyOnAdd(init); + notifyOnLateUpdate(lateUpdate); + notifyOnRemove(removeFromWorld); + } + + inline function withMargin(f: Float): Float { + return f + f * collisionMargin; + } + + public function notifyOnReady(f: Void->Void) { + onReady = f; + if (ready) onReady(); + } + + public function init() { + if (ready) return; + ready = true; + + transform = object.transform; + physics = armory.trait.physics.PhysicsWorld.active; + + shapeConvex = null; + shapeConvexHull = null; + isConvexHull = false; + + if (shape == ControllerShape.Box) { + vec1.setX(withMargin(transform.dim.x / 2)); + vec1.setY(withMargin(transform.dim.y / 2)); + vec1.setZ(withMargin(transform.dim.z / 2)); + shapeConvex = new bullet.Bt.BoxShape(vec1); + } + else if (shape == ControllerShape.Sphere) { + var width = transform.dim.x; + if (transform.dim.y > width) width = transform.dim.y; + if (transform.dim.z > width) width = transform.dim.z; + shapeConvex = new bullet.Bt.SphereShape(withMargin(width / 2)); + } + else if (shape == ControllerShape.ConvexHull && mass > 0) { + shapeConvexHull = new bullet.Bt.ConvexHullShape(); + isConvexHull = true; + addPointsToConvexHull(shapeConvexHull, transform.scale, collisionMargin); + } + else if (shape == ControllerShape.Cone) { + shapeConvex = new bullet.Bt.ConeShapeZ( + withMargin(transform.dim.x / 2), // Radius + withMargin(transform.dim.z)); // Height + } + else if (shape == ControllerShape.Cylinder) { + vec1.setX(withMargin(transform.dim.x / 2)); + vec1.setY(withMargin(transform.dim.y / 2)); + vec1.setZ(withMargin(transform.dim.z / 2)); + shapeConvex = new bullet.Bt.CylinderShapeZ(vec1); + } + else if (shape == ControllerShape.Capsule) { + var r = transform.dim.x / 2; + shapeConvex = new bullet.Bt.CapsuleShapeZ( + withMargin(r), // Radius + withMargin(transform.dim.z - r * 2)); // Height between 2 sphere centers + } + + trans1.setIdentity(); + vec1.setX(transform.worldx()); + vec1.setY(transform.worldy()); + vec1.setZ(transform.worldz()); + trans1.setOrigin(vec1); + + quat.fromMat(transform.world); + quat1.setX(quat.x); + quat1.setY(quat.y); + quat1.setZ(quat.z); + quat1.setW(quat.w); + trans1.setRotation(quat1); + + body = new bullet.Bt.PairCachingGhostObject(); + body.setCollisionShape(isConvexHull ? shapeConvexHull : shapeConvex); + body.setCollisionFlags(CF_CHARACTER_OBJECT); + body.setWorldTransform(trans1); + body.setFriction(friction); + body.setRollingFriction(friction); + body.setRestitution(restitution); + #if js + character = new bullet.Bt.KinematicCharacterController(body, isConvexHull ? shapeConvexHull : shapeConvex, 0.5, 2); + #elseif cpp + character = new bullet.Bt.KinematicCharacterController.create(body, isConvexHull ? shapeConvexHull : shapeConvex, 0.5, bullet.Bt.Vector3(0.0, 0.0, 1.0)); + #end + character.setJumpSpeed(jumpSpeed); + character.setUseGhostSweepTest(true); + + setActivationState(ControllerActivationState.NoDeactivation); + + bodyScaleX = currentScaleX = transform.scale.x; + bodyScaleY = currentScaleY = transform.scale.y; + bodyScaleZ = currentScaleZ = transform.scale.z; + + id = nextId; + nextId++; + + #if js + untyped body.userIndex = id; + #elseif cpp + body.setUserIndex(id); + #end + + // physics.addKinematicCharacterController(this); + + if (onReady != null) onReady(); + } + + function lateUpdate() { + if (!ready) return; + if (object.animation != null || animated) { + syncTransform(); + } + else { + var trans = body.getWorldTransform(); + var p = trans.getOrigin(); + var q = trans.getRotation(); + transform.loc.set(p.x(), p.y(), p.z()); + transform.rot.set(q.x(), q.y(), q.z(), q.w()); + if (object.parent != null) { + var ptransform = object.parent.transform; + transform.loc.x -= ptransform.worldx(); + transform.loc.y -= ptransform.worldy(); + transform.loc.z -= ptransform.worldz(); + } + transform.buildMatrix(); + } + } + + public function canJump(): Bool { + return character.canJump(); + } + + public function onGround(): Bool { + return character.onGround(); + } + + public function setJumpSpeed(jumpSpeed: Float) { + character.setJumpSpeed(jumpSpeed); + } + + public function setFallSpeed(fallSpeed: Float) { + character.setFallSpeed(fallSpeed); + } + + public function setMaxSlope(slopeRadians: Float) { + return character.setMaxSlope(slopeRadians); + } + + public function getMaxSlope(): Float { + return character.getMaxSlope(); + } + + public function setMaxJumpHeight(maxJumpHeight: Float) { + character.setMaxJumpHeight(maxJumpHeight); + } + + public function setWalkDirection(walkDirection: Vec4) { + vec1.setX(walkDirection.x); + vec1.setY(walkDirection.y); + vec1.setZ(walkDirection.z); + character.setWalkDirection(vec1); + } + + public function setUpInterpolate(value: Bool) { + character.setUpInterpolate(value); + } + + #if js + public function jump(): Void{ + character.jump(); + } + #elseif cpp + public function jump(v: Vec4): Void{ + vec1.setX(v.x); + vec1.setY(v.y); + vec1.setZ(v.z); + character.jump(vec1); + } + #end + + public function removeFromWorld() { + // if (physics != null) physics.removeKinematicCharacterController(this); + } + + public function activate() { + body.activate(false); + } + + public function disableGravity() { + #if js + character.setGravity(0.0); + #elseif cpp + vec1.setX(0); + vec1.setY(0); + vec1.setZ(0); + character.setGravity(vec1); + #end + } + + public function enableGravity() { + #if js + character.setGravity(Math.abs(physics.world.getGravity().z()) * 3.0); // 9.8 * 3.0 in cpp source code + #elseif cpp + vec1.setX(physics.world.getGravity().x() * 3.0); + vec1.setY(physics.world.getGravity().y() * 3.0); + vec1.setZ(physics.world.getGravity().z() * 3.0); + character.setGravity(vec1); + #end + } + + #if js + public function setGravity(f: Float) { + character.setGravity(f); + } + #elseif cpp + public function setGravity(v: Vec4) { + vec1.setX(v.x); + vec1.setY(v.y); + vec1.setZ(v.z); + character.setGravity(vec1); + } + #end + + public function setActivationState(newState: Int) { + body.setActivationState(newState); + } + + public function setFriction(f: Float) { + body.setFriction(f); + body.setRollingFriction(f); + this.friction = f; + } + + public function syncTransform() { + var t = transform; + t.buildMatrix(); + vec1.setX(t.worldx()); + vec1.setY(t.worldy()); + vec1.setZ(t.worldz()); + trans1.setOrigin(vec1); + quat.fromMat(t.world); + quat1.setX(quat.x); + quat1.setY(quat.y); + quat1.setZ(quat.z); + quat1.setW(quat.w); + trans1.setRotation(quat1); + //body.setCenterOfMassTransform(trans); // ? + if (currentScaleX != t.scale.x || currentScaleY != t.scale.y || currentScaleZ != t.scale.z) setScale(t.scale); + activate(); + } + + function setScale(v: Vec4) { + currentScaleX = v.x; + currentScaleY = v.y; + currentScaleZ = v.z; + vec1.setX(bodyScaleX * v.x); + vec1.setY(bodyScaleY * v.y); + vec1.setZ(bodyScaleZ * v.z); + if (isConvexHull) shapeConvexHull.setLocalScaling(vec1); + else shapeConvex.setLocalScaling(vec1); + physics.world.updateSingleAabb(body); + } + + function addPointsToConvexHull(shape: bullet.Bt.ConvexHullShape, scale: Vec4, margin: Float) { + var positions = cast(object, MeshObject).data.geom.positions.values; + + var sx = scale.x * (1.0 - margin); + var sy = scale.y * (1.0 - margin); + var sz = scale.z * (1.0 - margin); + + for (i in 0...Std.int(positions.length / 4)) { + vec1.setX(positions[i * 3] * sx); + vec1.setY(positions[i * 3 + 1] * sy); + vec1.setZ(positions[i * 3 + 2] * sz); + shape.addPoint(vec1, true); + } + } +} + +@:enum abstract ControllerShape(Int) from Int to Int { + var Box = 0; + var Sphere = 1; + var ConvexHull = 2; + var Cone = 3; + var Cylinder = 4; + var Capsule = 5; +} + +@:enum abstract ControllerActivationState(Int) from Int to Int { + var Active = 1; + var NoDeactivation = 4; + var NoSimulation = 5; +} + +#end diff --git a/Sources/armory/trait/physics/bullet/PhysicsConstraint.hx b/Sources/armory/trait/physics/bullet/PhysicsConstraint.hx new file mode 100644 index 0000000000..040d60132b --- /dev/null +++ b/Sources/armory/trait/physics/bullet/PhysicsConstraint.hx @@ -0,0 +1,476 @@ +package armory.trait.physics.bullet; + +import iron.math.Vec4; +import iron.Scene; +import iron.object.Object; +#if arm_bullet +import Math; +import iron.math.Quat; +import armory.trait.physics.RigidBody; +import armory.trait.physics.PhysicsWorld; + +/** + * A trait to add Bullet physics constraints + **/ +class PhysicsConstraint extends iron.Trait { + + static var nextId:Int = 0; + public var id:Int = 0; + + var physics: PhysicsWorld; + var body1: Object; + var body2: Object; + var type: ConstraintType; + public var disableCollisions: Bool; + var breakingThreshold: Float; + var limits: Array; + public var con: bullet.Bt.TypedConstraint = null; + + static var nullvec = true; + static var vec1: bullet.Bt.Vector3; + static var vec2: bullet.Bt.Vector3; + static var vec3: bullet.Bt.Vector3; + static var trans1: bullet.Bt.Transform; + static var trans2: bullet.Bt.Transform; + static var transt: bullet.Bt.Transform; + + /** + * Function to initialize physics constraint trait. + * + * @param object Pivot object to which this constraint trait will be added. The constraint limits are applied along the local axes of this object. This object need not + * be a Rigid Body. Typically an `Empty` object may be used. Moving/rotating/parenting this pivot object has no effect once the constraint trait is added. Removing + * the pivot object removes the constraint. + * + * @param body1 First rigid body to be constrained. This rigid body may be constrained by other constraints. + * + * @param body2 Second rigid body to be constrained. This rigid body may be constrained by other constraints. + * + * @param type Type of the constraint. + * + * @param disableCollisions Disable collisions between constrained objects. + * + * @param breakingThreshold Break the constraint if stress on this constraint exceeds this value. Set to 0 to make un-breakable. + * + * @param limits Constraint limits. This may be set before adding the trait to pivot object using the set limits functions. + * + **/ + public function new(body1: Object, body2: Object, type: ConstraintType, disableCollisions: Bool, breakingThreshold: Float, limits: Array = null) { + super(); + + if (nullvec) { + nullvec = false; + vec1 = new bullet.Bt.Vector3(0, 0, 0); + vec2 = new bullet.Bt.Vector3(0, 0, 0); + vec3 = new bullet.Bt.Vector3(0, 0, 0); + trans1 = new bullet.Bt.Transform(); + trans2 = new bullet.Bt.Transform(); + } + + this.body1 = body1; + this.body2 = body2; + this.type = type; + this.disableCollisions = disableCollisions; + this.breakingThreshold = breakingThreshold; + if(limits == null) limits = [for(i in 0...36) 0]; + this.limits = limits; + + notifyOnInit(init); + } + + function init() { + + physics = PhysicsWorld.active; + var target1 = body1; + var target2 = body2; + + if (target1 == null || target2 == null) return;//no objects selected + + var rb1: RigidBody = target1.getTrait(RigidBody); + var rb2: RigidBody = target2.getTrait(RigidBody); + + if (rb1 != null && rb1.ready && rb2 != null && rb2.ready) {//Check if rigid bodies are ready + + var t = object.transform; + var t1 = target1.transform; + var t2 = target2.transform; + + var frameT = t.world.clone();//Transform of pivot in world space + + var frameInA = t1.world.clone();//Transform of rb1 in world space + frameInA.getInverse(frameInA);//Inverse Transform of rb1 in world space + frameT.multmat(frameInA);//Transform of pivot object in rb1 space + frameInA = frameT.clone();//Frame In A + + frameT = t.world.clone();//Transform of pivot in world space + + var frameInB = t2.world.clone();//Transform of rb2 in world space + frameInB.getInverse(frameInB);//Inverse Transform of rb2 in world space + frameT.multmat(frameInB);//Transform of pivot object in rb2 space + frameInB = frameT.clone();//Frame In B + + var loc = new Vec4(); + var rot = new Quat(); + var scl = new Vec4(); + + frameInA.decompose(loc,rot,scl); + trans1.setIdentity(); + vec1.setX(loc.x); + vec1.setY(loc.y); + vec1.setZ(loc.z); + trans1.setOrigin(vec1); + trans1.setRotation(new bullet.Bt.Quaternion(rot.x, rot.y, rot.z, rot.w)); + + frameInB.decompose(loc,rot,scl); + trans2.setIdentity(); + vec2.setX(loc.x); + vec2.setY(loc.y); + vec2.setZ(loc.z); + trans2.setOrigin(vec2); + trans2.setRotation(new bullet.Bt.Quaternion(rot.x, rot.y, rot.z, rot.w)); + + if (type == Generic || type == Fixed) { + #if hl + var c = new bullet.Bt.Generic6DofConstraint(rb1.body, rb2.body, trans1, trans2, false); + #else + var c = bullet.Bt.Generic6DofConstraint.new2(rb1.body, rb2.body, trans1, trans2, false); + #end + if (type == Fixed) { + vec1.setX(0); + vec1.setY(0); + vec1.setZ(0); + c.setLinearLowerLimit(vec1); + c.setLinearUpperLimit(vec1); + c.setAngularLowerLimit(vec1); + c.setAngularUpperLimit(vec1); + } + else if (type == ConstraintType.Generic) { + if (limits[0] == 0) { + limits[1] = 1.0; + limits[2] = -1.0; + } + if (limits[3] == 0) { + limits[4] = 1.0; + limits[5] = -1.0; + } + if (limits[6] == 0) { + limits[7] = 1.0; + limits[8] = -1.0; + } + if (limits[9] == 0) { + limits[10] = 1.0; + limits[11] = -1.0; + } + if (limits[12] == 0) { + limits[13] = 1.0; + limits[14] = -1.0; + } + if (limits[15] == 0) { + limits[16] = 1.0; + limits[17] = -1.0; + } + vec1.setX(limits[1]); + vec1.setY(limits[4]); + vec1.setZ(limits[7]); + c.setLinearLowerLimit(vec1); + vec1.setX(limits[2]); + vec1.setY(limits[5]); + vec1.setZ(limits[8]); + c.setLinearUpperLimit(vec1); + vec1.setX(limits[10]); + vec1.setY(limits[13]); + vec1.setZ(limits[16]); + c.setAngularLowerLimit(vec1); + vec1.setX(limits[11]); + vec1.setY(limits[14]); + vec1.setZ(limits[17]); + c.setAngularUpperLimit(vec1); + } + con = cast c; + } + else if (type == ConstraintType.GenericSpring){ + var c = new bullet.Bt.Generic6DofSpringConstraint(rb1.body, rb2.body, trans1, trans2, false); + + if (limits[0] == 0) { + limits[1] = 1.0; + limits[2] = -1.0; + } + if (limits[3] == 0) { + limits[4] = 1.0; + limits[5] = -1.0; + } + if (limits[6] == 0) { + limits[7] = 1.0; + limits[8] = -1.0; + } + if (limits[9] == 0) { + limits[10] = 1.0; + limits[11] = -1.0; + } + if (limits[12] == 0) { + limits[13] = 1.0; + limits[14] = -1.0; + } + if (limits[15] == 0) { + limits[16] = 1.0; + limits[17] = -1.0; + } + vec1.setX(limits[1]); + vec1.setY(limits[4]); + vec1.setZ(limits[7]); + c.setLinearLowerLimit(vec1); + vec1.setX(limits[2]); + vec1.setY(limits[5]); + vec1.setZ(limits[8]); + c.setLinearUpperLimit(vec1); + vec1.setX(limits[10]); + vec1.setY(limits[13]); + vec1.setZ(limits[16]); + c.setAngularLowerLimit(vec1); + vec1.setX(limits[11]); + vec1.setY(limits[14]); + vec1.setZ(limits[17]); + c.setAngularUpperLimit(vec1); + if (limits[18] != 0) { + c.enableSpring(0, true); + c.setStiffness(0, limits[19]); + c.setDamping(0, limits[20]); + } + else { + c.enableSpring(0, false); + } + if (limits[21] != 0) { + c.enableSpring(1, true); + c.setStiffness(1, limits[22]); + c.setDamping(1, limits[23]); + } + else { + c.enableSpring(1, false); + } + if (limits[24] != 0) { + c.enableSpring(2, true); + c.setStiffness(2, limits[25]); + c.setDamping(2, limits[26]); + } + else { + c.enableSpring(2, false); + } + if (limits[27] != 0) { + c.enableSpring(3, true); + c.setStiffness(3, limits[28]); + c.setDamping(3, limits[29]); + } + else { + c.enableSpring(3, false); + } + if (limits[30] != 0) { + c.enableSpring(4, true); + c.setStiffness(4, limits[31]); + c.setDamping(4, limits[32]); + } + else { + c.enableSpring(4, false); + } + if (limits[33] != 0) { + c.enableSpring(5, true); + c.setStiffness(5, limits[34]); + c.setDamping(5, limits[35]); + } + else { + c.enableSpring(5, false); + } + con = cast c; + + } + else if (type == ConstraintType.Point){ + var c = new bullet.Bt.Point2PointConstraint(rb1.body, rb2.body, vec1, vec2); + con = cast c; + } + else if (type == ConstraintType.Hinge) { + var axis = vec3; + var _softness: Float = 0.9; + var _biasFactor: Float = 0.3; + var _relaxationFactor: Float = 1.0; + + axis.setX(t.up().x); + axis.setY(t.up().y); + axis.setZ(t.up().z); + + var c = new bullet.Bt.HingeConstraint(rb1.body, rb2.body, vec1, vec2, axis, axis, false); + + if (limits[0] != 0) { + c.setLimit(limits[1], limits[2], _softness, _biasFactor, _relaxationFactor); + } + + con = cast c; + } + else if (type == ConstraintType.Slider) { + var c = new bullet.Bt.SliderConstraint(rb1.body, rb2.body, trans1, trans2, true); + + if (limits[0] != 0) { + c.setLowerLinLimit(limits[1]); + c.setUpperLinLimit(limits[2]); + } + + con = cast c; + } + else if (type == ConstraintType.Piston) { + var c = new bullet.Bt.SliderConstraint(rb1.body, rb2.body, trans1, trans2, true); + + if (limits[0] != 0) { + c.setLowerLinLimit(limits[1]); + c.setUpperLinLimit(limits[2]); + } + if (limits[3] != 0) { + c.setLowerAngLimit(limits[4]); + c.setUpperAngLimit(limits[5]); + } + else { + c.setLowerAngLimit(1); + c.setUpperAngLimit(-1); + + } + con = cast c; + } + + if (breakingThreshold > 0) con.setBreakingImpulseThreshold(breakingThreshold); + + physics.addPhysicsConstraint(this); + + id = nextId; + nextId++; + + + notifyOnRemove(removeFromWorld); + } + else this.remove(); // Rigid body not initialized yet. Remove trait without adding constraint + } + + public function removeFromWorld() { + physics.removePhysicsConstraint(this); + } + + /** + * Function to set constraint limits when using Hinge constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ + public function setHingeConstraintLimits(angLimit: Bool, lowerAngLimit: Float, upperAngLimit: Float) { + + angLimit? limits[0] = 1 : limits[0] = 0; + + limits[1] = lowerAngLimit * (Math.PI/ 180); + limits[2] = upperAngLimit * (Math.PI/ 180); + } + + /** + * Function to set constraint limits when using Slider constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ + public function setSliderConstraintLimits(linLimit: Bool, lowerLinLimit: Float, upperLinLimit: Float) { + + linLimit? limits[0] = 1 : limits[0] = 0; + + limits[1] = lowerLinLimit; + limits[2] = upperLinLimit; + } + + /** + * Function to set constraint limits when using Piston constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ + public function setPistonConstraintLimits(linLimit: Bool, lowerLinLimit: Float, upperLinLimit: Float, angLimit: Bool, lowerAngLimit: Float, upperAngLimit: Float) { + + linLimit? limits[0] = 1 : limits[0] = 0; + + limits[1] = lowerLinLimit; + limits[2] = upperLinLimit; + + angLimit? limits[3] = 1 : limits[3] = 0; + + limits[4] = lowerAngLimit * (Math.PI/ 180); + limits[5] = upperAngLimit * (Math.PI/ 180); + } + + /** + * Function to set customized constraint limits when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it + * to the pivot object. Multiple constarints may be set by calling this function with different parameters. + **/ + public function setGenericConstraintLimits(setLimit: Bool = false, lowerLimit: Float = 1.0, upperLimit: Float = -1.0, axis: ConstraintAxis = X, isAngular: Bool = false) { + + var i = 0; + var j = 0; + var radian = (Math.PI/ 180); + + switch (axis){ + case X: + i = 0; + case Y: + i = 3; + case Z: + i = 6; + } + + isAngular? j = 9 : j = 0; + + isAngular? radian = (Math.PI/ 180) : radian = 1; + + setLimit? limits[i + j] = 1 : 0; + limits[i + j + 1] = lowerLimit * radian; + limits[i + j + 2] = upperLimit * radian; + + } + + /** + * Function to set customized spring parameters when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it + * to the pivot object. Multiple parameters to different axes may be set by calling this function with different parameters. + **/ + public function setSpringParams(setSpring: Bool = false, stiffness: Float = 10.0, damping: Float = 0.5, axis: ConstraintAxis = X, isAngular: Bool = false) { + + var i = 0; + var j = 0; + + switch (axis){ + case X: + i = 18; + case Y: + i = 21; + case Z: + i = 24; + } + + isAngular? j = 9 : j = 0; + + setSpring? limits[i + j] = 1 : 0; + limits[i + j + 1] = stiffness; + limits[i + j + 2] = damping; + + } + + public function delete() { + #if js + bullet.Bt.Ammo.destroy(con); + #else + con.delete(); + #end + } + + +} + +@:enum abstract ConstraintType(Int) from Int to Int { + var Fixed = 0; + var Point = 1; + var Hinge = 2; + var Slider = 3; + var Piston = 4; + var Generic = 5; + var GenericSpring = 6; + var Motor = 7; +} + +@:enum abstract ConstraintAxis(Int) from Int to Int { + var X = 0; + var Y = 1; + var Z = 2; +} + +#end diff --git a/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx b/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx new file mode 100644 index 0000000000..bb536bf2bd --- /dev/null +++ b/Sources/armory/trait/physics/bullet/PhysicsConstraintExportHelper.hx @@ -0,0 +1,60 @@ +package armory.trait.physics.bullet; + +import iron.Scene; +import iron.object.Object; +#if arm_bullet + +/** + * A helper trait to add physics constraints when exporting via Blender. + * This trait will be automatically removed once the constraint is added. Note that this trait + * uses object names instead of object reference. + **/ +class PhysicsConstraintExportHelper extends iron.Trait { + + var body1: String; + var body2: String; + var type: Int; + var disableCollisions: Bool; + 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, relatieConstraint: Bool = false, limits: Array = null) { + super(); + + this.body1 = body1; + this.body2 = body2; + this.type = type; + this.disableCollisions = disableCollisions; + this.breakingThreshold = breakingThreshold; + this.relativeConstraint = relatieConstraint; + this.limits = limits; + notifyOnInit(init); + notifyOnUpdate(update); + } + + function init() { + 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; + } + + function update() { + if(constraintAdded) this.remove(); + } +} + +#end diff --git a/Sources/armory/trait/physics/bullet/PhysicsHook.hx b/Sources/armory/trait/physics/bullet/PhysicsHook.hx new file mode 100644 index 0000000000..e0f8e45008 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/PhysicsHook.hx @@ -0,0 +1,189 @@ +package armory.trait.physics.bullet; + +#if arm_bullet + +import iron.math.Vec4; +import iron.math.Mat4; +import iron.math.Quat; +import iron.Trait; +import iron.object.Object; +import iron.object.MeshObject; +import iron.object.Transform; +import iron.data.MeshData; +import iron.data.SceneFormat; +import armory.trait.physics.RigidBody; +import armory.trait.physics.PhysicsWorld; + +class PhysicsHook extends Trait { + + var target: Object; + var targetName: String; + var targetTransform: Transform; + var verts: Array; + + var constraint: bullet.Bt.Generic6DofConstraint = null; + + #if arm_physics_soft + var hookRB: bullet.Bt.RigidBody = null; + #end + + static var nullvec = true; + static var vec1: bullet.Bt.Vector3; + static var quat1: bullet.Bt.Quaternion; + static var trans1: bullet.Bt.Transform; + static var trans2: bullet.Bt.Transform; + static var quat = new Quat(); + + public function new(targetName: String, verts: Array) { + super(); + + if (nullvec) { + nullvec = false; + vec1 = new bullet.Bt.Vector3(0, 0, 0); + quat1 = new bullet.Bt.Quaternion(0, 0, 0, 0); + trans1 = new bullet.Bt.Transform(); + trans2 = new bullet.Bt.Transform(); + } + + this.targetName = targetName; + this.verts = verts; + + iron.Scene.active.notifyOnInit(function() { + notifyOnInit(init); + notifyOnUpdate(update); + }); + } + + function init() { + // Hook to empty axes + target = targetName != "" ? iron.Scene.active.getChild(targetName) : null; + targetTransform = target != null ? target.transform : object.transform; + + var physics = PhysicsWorld.active; + + // Soft body hook + #if arm_physics_soft + var sb: SoftBody = object.getTrait(SoftBody); + if (sb != null && sb.ready) { + + // Place static rigid body at target location + trans1.setIdentity(); + vec1.setX(targetTransform.loc.x); + vec1.setY(targetTransform.loc.y); + vec1.setZ(targetTransform.loc.z); + trans1.setOrigin(vec1); + quat1.setX(targetTransform.rot.x); + quat1.setY(targetTransform.rot.y); + quat1.setZ(targetTransform.rot.z); + quat1.setW(targetTransform.rot.w); + trans1.setRotation(quat1); + var centerOfMassOffset = trans2; + centerOfMassOffset.setIdentity(); + var mass = 0.0; + var motionState = new bullet.Bt.DefaultMotionState(trans1, centerOfMassOffset); + var inertia = vec1; + inertia.setX(0); + inertia.setY(0); + inertia.setZ(0); + var shape = new bullet.Bt.SphereShape(0.01); + shape.calculateLocalInertia(mass, inertia); + var bodyCI = new bullet.Bt.RigidBodyConstructionInfo(mass, motionState, shape, inertia); + hookRB = new bullet.Bt.RigidBody(bodyCI); + + #if js + var nodes = sb.body.get_m_nodes(); + #elseif cpp + var nodes = sb.body.m_nodes; + #end + + var geom = cast(object, MeshObject).data.geom; + var numNodes = Std.int(geom.positions.values.length / 4); + for (i in 0...numNodes) { + var node = nodes.at(i); + #if js + var nodePos = node.get_m_x(); + #elseif cpp + var nodePos = node.m_x; + #end + + // Find nodes at marked vertex group locations + var numVerts = Std.int(verts.length / 3); + for (j in 0...numVerts) { + var x = verts[j * 3] + sb.vertOffsetX; + var y = verts[j * 3 + 1] + sb.vertOffsetY; + var z = verts[j * 3 + 2] + sb.vertOffsetZ; + + // Anchor node to rigid body + if (Math.abs(nodePos.x() - x) < 0.01 && Math.abs(nodePos.y() - y) < 0.01 && Math.abs(nodePos.z() - z) < 0.01) { + sb.body.appendAnchor(i, hookRB, false, 1.0 / numVerts); + } + } + } + return; + } + #end + + // Rigid body hook + var rb1: RigidBody = object.getTrait(RigidBody); + if (rb1 != null && rb1.ready) { + trans1.setIdentity(); + vec1.setX(targetTransform.worldx() - object.transform.worldx()); + vec1.setY(targetTransform.worldy() - object.transform.worldy()); + vec1.setZ(targetTransform.worldz() - object.transform.worldz()); + trans1.setOrigin(vec1); + constraint = new bullet.Bt.Generic6DofConstraint(rb1.body, trans1, false); + vec1.setX(0); + vec1.setY(0); + vec1.setZ(0); + constraint.setLinearLowerLimit(vec1); + constraint.setLinearUpperLimit(vec1); + vec1.setX(-10); + vec1.setY(-10); + vec1.setZ(-10); + constraint.setAngularLowerLimit(vec1); + vec1.setX(10); + vec1.setY(10); + vec1.setZ(10); + constraint.setAngularUpperLimit(vec1); + physics.world.addConstraint(constraint, false); + return; + } + + // Rigid body or soft body not initialized yet + notifyOnInit(init); + } + + function update() { + #if arm_physics_soft + if (hookRB != null) { + trans1.setIdentity(); + vec1.setX(targetTransform.world.getLoc().x); + vec1.setY(targetTransform.world.getLoc().y); + vec1.setZ(targetTransform.world.getLoc().z); + trans1.setOrigin(vec1); + quat.fromMat(targetTransform.world); + quat1.setX(quat.x); + quat1.setY(quat.y); + quat1.setZ(quat.z); + quat1.setW(quat.w); + trans1.setRotation(quat1); + hookRB.setWorldTransform(trans1); + } + #end + + // if (constraint != null) { + // vec1.setX(targetTransform.worldx() - object.transform.worldx()); + // vec1.setY(targetTransform.worldy() - object.transform.worldy()); + // vec1.setZ(targetTransform.worldz() - object.transform.worldz()); + // var pivot = vec1; + // #if js + // constraint.getFrameOffsetA().setOrigin(pivot); + // #elseif cpp + // constraint.setFrameOffsetAOrigin(pivot); + // #end + // } + } + +} + +#end diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx new file mode 100644 index 0000000000..6af4cd2761 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -0,0 +1,488 @@ +package armory.trait.physics.bullet; + +#if arm_bullet + +import iron.Trait; +import iron.system.Time; +import iron.math.Vec4; +import iron.math.Quat; +import iron.math.RayCaster; + +class Hit { + + public var rb: RigidBody; + public var pos: Vec4; + public var normal: Vec4; + public function new(rb: RigidBody, pos: Vec4, normal: Vec4){ + this.rb = rb; + this.pos = pos; + this.normal = normal; + } +} + +class ConvexHit { + public var pos: Vec4; + public var normal: Vec4; + public var hitFraction: Float; + public function new(pos: Vec4, normal: Vec4, hitFraction: Float){ + this.pos = pos; + this.normal = normal; + this.hitFraction = hitFraction; + } +} + +class ContactPair { + + public var a: Int; + public var b: Int; + public var posA: Vec4; + public var posB: Vec4; + public var normOnB: Vec4; + public var impulse: Float; + public var distance: Float; + public function new(a: Int, b: Int) { + this.a = a; + this.b = b; + } +} + +class PhysicsWorld extends Trait { + + public static var active: PhysicsWorld = null; + static var sceneRemoved = false; + + #if arm_physics_soft + public var world: bullet.Bt.SoftRigidDynamicsWorld; + #else + public var world: bullet.Bt.DiscreteDynamicsWorld; + #end + + var dispatcher: bullet.Bt.CollisionDispatcher; + var gimpactRegistered = false; + var contacts: Array; + var preUpdates: ArrayVoid> = null; + public var rbMap: Map; + public var conMap: Map; + public var timeScale = 1.0; + var maxSteps = 1; + public var solverIterations = 10; + public var hitPointWorld = new Vec4(); + public var hitNormalWorld = new Vec4(); + public var convexHitPointWorld = new Vec4(); + public var convexHitNormalWorld = new Vec4(); + var pairCache: Bool = false; + + static var nullvec = true; + static var vec1: bullet.Bt.Vector3 = null; + static var vec2: bullet.Bt.Vector3 = null; + static var quat1: bullet.Bt.Quaternion = null; + static var transform1: bullet.Bt.Transform = null; + static var transform2: bullet.Bt.Transform = null; + + #if arm_debug + public static var physTime = 0.0; + #end + + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10) { + super(); + + if (nullvec) { + nullvec = false; + vec1 = new bullet.Bt.Vector3(0, 0, 0); + vec2 = new bullet.Bt.Vector3(0, 0, 0); + transform1 = new bullet.Bt.Transform(); + transform2 = new bullet.Bt.Transform(); + quat1 = new bullet.Bt.Quaternion(0, 0, 0, 1.0); + } + + // Scene spawn + if (active != null && !sceneRemoved) return; + sceneRemoved = false; + + this.timeScale = timeScale; + this.maxSteps = maxSteps; + this.solverIterations = solverIterations; + + // First scene + if (active == null) { + createPhysics(); + } + else { // Scene switch + world = active.world; + dispatcher = active.dispatcher; + gimpactRegistered = active.gimpactRegistered; + } + + contacts = []; + rbMap = new Map(); + conMap = new Map(); + active = this; + + // Ensure physics are updated first in the lateUpdate list + _lateUpdate = [lateUpdate]; + @:privateAccess iron.App.traitLateUpdates.insert(0, lateUpdate); + + iron.Scene.active.notifyOnRemove(function() { + sceneRemoved = true; + }); + } + + public function reset() { + for (rb in active.rbMap) removeRigidBody(rb); + } + + function createPhysics() { + var broadphase = new bullet.Bt.DbvtBroadphase(); + +#if arm_physics_soft + var collisionConfiguration = new bullet.Bt.SoftBodyRigidBodyCollisionConfiguration(); +#else + var collisionConfiguration = new bullet.Bt.DefaultCollisionConfiguration(); +#end + + dispatcher = new bullet.Bt.CollisionDispatcher(collisionConfiguration); + var solver = new bullet.Bt.SequentialImpulseConstraintSolver(); + + var g = iron.Scene.active.raw.gravity; + var gravity = g == null ? new Vec4(0, 0, -9.81) : new Vec4(g[0], g[1], g[2]); + +#if arm_physics_soft + var softSolver = new bullet.Bt.DefaultSoftBodySolver(); + world = new bullet.Bt.SoftRigidDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration, softSolver); + vec1.setX(gravity.x); + vec1.setY(gravity.y); + vec1.setZ(gravity.z); + #if js + world.getWorldInfo().set_m_gravity(vec1); + #else + world.getWorldInfo().m_gravity = vec1; + #end +#else + world = new bullet.Bt.DiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration); +#end + + setGravity(gravity); + } + + public function setGravity(v: Vec4) { + vec1.setValue(v.x, v.y, v.z); + world.setGravity(vec1); + } + + public function getGravity(): Vec4{ + var g = world.getGravity(); + return (new Vec4(g.x(), g.y(), g.z())); + } + + public function addRigidBody(body: RigidBody) { + #if js + world.addRigidBodyToGroup(body.body, body.group, body.mask); + #else + world.addRigidBody(body.body, body.group, body.mask); + #end + rbMap.set(body.id, body); + } + + public function addPhysicsConstraint(constraint: PhysicsConstraint) { + world.addConstraint(constraint.con, constraint.disableCollisions); + conMap.set(constraint.id, constraint); + } + + public function removeRigidBody(body: RigidBody) { + if (body.destroyed) return; + body.destroyed = true; + if (world != null) world.removeRigidBody(body.body); + rbMap.remove(body.id); + body.delete(); + } + + public function removePhysicsConstraint(constraint: PhysicsConstraint) { + if(world != null) world.removeConstraint(constraint.con); + conMap.remove(constraint.id); + constraint.delete(); + } + + // public function addKinematicCharacterController(controller:KinematicCharacterController) { + // if (!pairCache){ // Only create PairCache if needed + // world.getPairCache().setInternalGhostPairCallback(BtGhostPairCallbackPointer.create()); + // pairCache = true; + // } + // world.addAction(controller.character); + // world.addCollisionObjectToGroup(controller.body, controller.group, controller.group); + // } + + // public function removeKinematicCharacterController(controller:KinematicCharacterController) { + // if (world != null) { + // world.removeCollisionObject(controller.body); + // world.removeAction(controller.character); + // } + // #if js + // bullet.Bt.Ammo.destroy(controller.body); + // #else + // var cbody = controller.body; + // untyped __cpp__("delete cbody"); + // #end + // } + + /** + Used to get intersecting rigid bodies with the passed in RigidBody as reference. Often used when checking for object collisions. + @param body The passed in RigidBody to be checked for intersecting rigid bodies. + @return `Array` + **/ + public function getContacts(body: RigidBody): Array { + if (contacts.length == 0) return null; + var res: Array = []; + for (i in 0...contacts.length) { + var c = contacts[i]; + var rb = null; + + #if js + if (c.a == untyped body.body.userIndex) rb = rbMap.get(c.b); + else if (c.b == untyped body.body.userIndex) rb = rbMap.get(c.a); + + #else + var ob: bullet.Bt.CollisionObject = body.body; + if (c.a == ob.getUserIndex()) rb = rbMap.get(c.b); + else if (c.b == ob.getUserIndex()) rb = rbMap.get(c.a); + #end + + if (rb != null && res.indexOf(rb) == -1) res.push(rb); + } + return res; + } + + public function getContactPairs(body: RigidBody): Array { + if (contacts.length == 0) return null; + var res: Array = []; + for (i in 0...contacts.length) { + var c = contacts[i]; + #if js + + if (c.a == untyped body.body.userIndex) res.push(c); + else if (c.b == untyped body.body.userIndex) res.push(c); + + #else + + var ob: bullet.Bt.CollisionObject = body.body; + if (c.a == ob.getUserIndex()) res.push(c); + else if (c.b == ob.getUserIndex()) res.push(c); + + #end + } + return res; + } + + public function findBody(id: Int): RigidBody{ + var rb = rbMap.get(id); + return rb; + } + + function lateUpdate() { + var t = Time.delta * timeScale; + if (t == 0.0) return; // Simulation paused + + #if arm_debug + var startTime = kha.Scheduler.realTime(); + #end + + if (preUpdates != null) for (f in preUpdates) f(); + + //Bullet physics fixed timescale + var fixedTime = 1.0 / 60; + + //This condition must be satisfied to not loose time + var currMaxSteps = t < (fixedTime * maxSteps) ? maxSteps : 1; + + world.stepSimulation(t, currMaxSteps, fixedTime); + updateContacts(); + + for (rb in rbMap) @:privateAccess rb.physicsUpdate(); + + #if arm_debug + physTime = kha.Scheduler.realTime() - startTime; + #end + } + + function updateContacts() { + contacts.resize(0); + + var disp: bullet.Bt.Dispatcher = dispatcher; + var numManifolds = disp.getNumManifolds(); + + for (i in 0...numManifolds) { + var contactManifold = disp.getManifoldByIndexInternal(i); + #if js + var body0 = untyped bullet.Bt.Ammo.btRigidBody.prototype.upcast(contactManifold.getBody0()); + var body1 = untyped bullet.Bt.Ammo.btRigidBody.prototype.upcast(contactManifold.getBody1()); + #else + var body0: bullet.Bt.CollisionObject = contactManifold.getBody0(); + var body1: bullet.Bt.CollisionObject = contactManifold.getBody1(); + #end + + var numContacts = contactManifold.getNumContacts(); + for (j in 0...numContacts) { + + var pt = contactManifold.getContactPoint(j); + var posA: bullet.Bt.Vector3 = null; + var posB: bullet.Bt.Vector3 = null; + var nor: bullet.Bt.Vector3 = null; + var cp: ContactPair = null; + + #if js + posA = pt.get_m_positionWorldOnA(); + posB = pt.get_m_positionWorldOnB(); + nor = pt.get_m_normalWorldOnB(); + cp = new ContactPair(untyped body0.userIndex, untyped body1.userIndex); + #else + posA = pt.m_positionWorldOnA; + posB = pt.m_positionWorldOnB; + nor = pt.m_normalWorldOnB; + cp = new ContactPair(body0.getUserIndex(), body1.getUserIndex()); + #end + + cp.posA = new Vec4(posA.x(), posA.y(), posA.z()); + cp.posB = new Vec4(posB.x(), posB.y(), posB.z()); + cp.normOnB = new Vec4(nor.x(), nor.y(), nor.z()); + cp.impulse = pt.getAppliedImpulse(); + cp.distance = pt.getDistance(); + contacts.push(cp); + + #if hl + pt.delete(); + posA.delete(); + posB.delete(); + nor.delete(); + #end + } + } + } + + public function pickClosest(inputX: Float, inputY: Float, group: Int = 0x00000001, mask = 0xFFFFFFFF): RigidBody { + var camera = iron.Scene.active.camera; + var start = new Vec4(); + var end = new Vec4(); + RayCaster.getDirection(start, end, inputX, inputY, camera); + var hit = rayCast(camera.transform.world.getLoc(), end, group, mask); + var rb = (hit != null) ? hit.rb : null; + return rb; + } + + public function rayCast(from: Vec4, to: Vec4, group: Int = 0x00000001, mask = 0xFFFFFFFF): Hit { + var rayFrom = vec1; + var rayTo = vec2; + rayFrom.setValue(from.x, from.y, from.z); + rayTo.setValue(to.x, to.y, to.z); + + var rayCallback = new bullet.Bt.ClosestRayResultCallback(rayFrom, rayTo); + #if js + rayCallback.set_m_collisionFilterGroup(group); + rayCallback.set_m_collisionFilterMask(mask); + #elseif (cpp || hl) + rayCallback.m_collisionFilterGroup = group; + rayCallback.m_collisionFilterMask = mask; + #end + var worldDyn: bullet.Bt.DynamicsWorld = world; + var worldCol: bullet.Bt.CollisionWorld = worldDyn; + worldCol.rayTest(rayFrom, rayTo, rayCallback); + var rb: RigidBody = null; + var hitInfo: Hit = null; + + var rc: bullet.Bt.RayResultCallback = rayCallback; + if (rc.hasHit()) { + #if js + var co = rayCallback.get_m_collisionObject(); + var body = untyped bullet.Bt.Ammo.btRigidBody.prototype.upcast(co); + var hit = rayCallback.get_m_hitPointWorld(); + hitPointWorld.set(hit.x(), hit.y(), hit.z()); + var norm = rayCallback.get_m_hitNormalWorld(); + hitNormalWorld.set(norm.x(), norm.y(), norm.z()); + rb = rbMap.get(untyped body.userIndex); + hitInfo = new Hit(rb, hitPointWorld, hitNormalWorld); + #elseif (cpp || hl) + var hit = rayCallback.m_hitPointWorld; + hitPointWorld.set(hit.x(), hit.y(), hit.z()); + var norm = rayCallback.m_hitNormalWorld; + hitNormalWorld.set(norm.x(), norm.y(), norm.z()); + rb = rbMap.get(rayCallback.m_collisionObject.getUserIndex()); + hitInfo = new Hit(rb, hitPointWorld, hitNormalWorld); + #end + } + + #if js + bullet.Bt.Ammo.destroy(rayCallback); + #else + rayCallback.delete(); + #end + + return hitInfo; + } + + public function convexSweepTest(rb: RigidBody, from: Vec4, to: Vec4, rotation: Quat, group: Int = 0x00000001, mask = 0xFFFFFFFF): ConvexHit { + var transformFrom = transform1; + var transformTo = transform2; + transformFrom.setIdentity(); + transformTo.setIdentity(); + + vec1.setValue(from.x, from.y, from.z); + transformFrom.setOrigin(vec1); + quat1.setValue(rotation.x, rotation.y, rotation.z, rotation.w); + transformFrom.setRotation(quat1); + + vec2.setValue(to.x, to.y, to.z); + transformTo.setOrigin(vec2); + quat1.setValue(rotation.x, rotation.y, rotation.z, rotation.w); + transformFrom.setRotation(quat1); + + var convexCallback = new bullet.Bt.ClosestConvexResultCallback(vec1, vec2); + #if js + convexCallback.set_m_collisionFilterGroup(group); + convexCallback.set_m_collisionFilterMask(mask); + #elseif (cpp || hl) + convexCallback.m_collisionFilterGroup = group; + convexCallback.m_collisionFilterMask = mask; + #end + var worldDyn: bullet.Bt.DynamicsWorld = world; + var worldCol: bullet.Bt.CollisionWorld = worldDyn; + var bodyColl: bullet.Bt.ConvexShape = cast rb.body.getCollisionShape(); + worldCol.convexSweepTest(bodyColl, transformFrom, transformTo, convexCallback, 0.0); + + var hitInfo: ConvexHit = null; + + var cc: bullet.Bt.ClosestConvexResultCallback = convexCallback; + if (cc.hasHit()) { + #if js + var hit = convexCallback.get_m_hitPointWorld(); + convexHitPointWorld.set(hit.x(), hit.y(), hit.z()); + var norm = convexCallback.get_m_hitNormalWorld(); + convexHitNormalWorld.set(norm.x(), norm.y(), norm.z()); + var hitFraction = convexCallback.get_m_closestHitFraction(); + #elseif (cpp || hl) + var hit = convexCallback.m_hitPointWorld; + convexHitPointWorld.set(hit.x(), hit.y(), hit.z()); + var norm = convexCallback.m_hitNormalWorld; + convexHitNormalWorld.set(norm.x(), norm.y(), norm.z()); + var hitFraction = convexCallback.m_closestHitFraction; + #end + hitInfo = new ConvexHit(convexHitPointWorld, convexHitNormalWorld, hitFraction); + } + + #if js + bullet.Bt.Ammo.destroy(convexCallback); + #else + convexCallback.delete(); + #end + + return hitInfo; + } + + public function notifyOnPreUpdate(f: Void->Void) { + if (preUpdates == null) preUpdates = []; + preUpdates.push(f); + } + + public function removePreUpdate(f: Void->Void) { + preUpdates.remove(f); + } +} + +#end diff --git a/Sources/armory/trait/physics/bullet/RigidBody.hx b/Sources/armory/trait/physics/bullet/RigidBody.hx new file mode 100644 index 0000000000..da764fc908 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/RigidBody.hx @@ -0,0 +1,694 @@ +package armory.trait.physics.bullet; + +#if arm_bullet + +import iron.math.Vec4; +import iron.math.Quat; +import iron.object.Transform; +import iron.object.MeshObject; +import iron.data.MeshData; + +/** + RigidBody is used to allow objects to interact with Physics in your game including collisions and gravity. + RigidBody can also be used with the getContacts method to detect collisions and run appropriate code. + The Bullet physics engine is used for these calculations. +**/ +@:access(armory.trait.physics.bullet.PhysicsWorld) +class RigidBody extends iron.Trait { + + var shape: Shape; + public var physics: PhysicsWorld; + public var transform: Transform = null; + public var mass: Float; + public var friction: Float; + public var angularFriction: Float; + public var restitution: Float; + public var collisionMargin: Float; + public var linearDamping: Float; + public var angularDamping: Float; + public var animated: Bool; + public var staticObj: Bool; + public var destroyed = false; + var linearFactors: Array; + var angularFactors: Array; + var useDeactivation: Bool; + var deactivationParams: Array; + var ccd = false; // Continuous collision detection + public var group = 1; + public var mask = 1; + var trigger = false; + var bodyScaleX: Float; // Transform scale at creation time + var bodyScaleY: Float; + var bodyScaleZ: Float; + var currentScaleX: Float; + var currentScaleY: Float; + var currentScaleZ: Float; + var meshInterface: bullet.Bt.TriangleMesh; + + public var body: bullet.Bt.RigidBody = null; + public var motionState: bullet.Bt.MotionState; + public var btshape: bullet.Bt.CollisionShape; + public var ready = false; + static var nextId = 0; + public var id = 0; + public var onReady: Void->Void = null; + public var onContact: ArrayVoid> = null; + public var heightData: haxe.io.Bytes = null; + #if js + static var ammoArray: Int = -1; + #end + + static var nullvec = true; + static var vec1: bullet.Bt.Vector3; + static var vec2: bullet.Bt.Vector3; + static var vec3: bullet.Bt.Vector3; + static var quat1: bullet.Bt.Quaternion; + static var trans1: bullet.Bt.Transform; + static var trans2: bullet.Bt.Transform; + static var quat = new Quat(); + + static var CF_STATIC_OBJECT = 1; + static var CF_KINEMATIC_OBJECT = 2; + static var CF_NO_CONTACT_RESPONSE = 4; + static var CF_CHARACTER_OBJECT = 16; + + static var convexHullCache = new Map(); + static var triangleMeshCache = new Map(); + static var usersCache = new Map(); + + public function new(shape = Shape.Box, mass = 1.0, friction = 0.5, restitution = 0.0, group = 1, mask = 1, + params: RigidBodyParams = null, flags: RigidBodyFlags = null) { + super(); + + if (nullvec) { + nullvec = false; + vec1 = new bullet.Bt.Vector3(0, 0, 0); + vec2 = new bullet.Bt.Vector3(0, 0, 0); + vec3 = new bullet.Bt.Vector3(0, 0, 0); + quat1 = new bullet.Bt.Quaternion(0, 0, 0, 0); + trans1 = new bullet.Bt.Transform(); + trans2 = new bullet.Bt.Transform(); + } + + this.shape = shape; + this.mass = mass; + this.friction = friction; + this.restitution = restitution; + this.group = group; + this.mask = mask; + + if (params == null) params = { + linearDamping: 0.04, + angularDamping: 0.1, + angularFriction: 0.1, + linearFactorsX: 1.0, + linearFactorsY: 1.0, + linearFactorsZ: 1.0, + angularFactorsX: 1.0, + angularFactorsY: 1.0, + angularFactorsZ: 1.0, + collisionMargin: 0.0, + linearDeactivationThreshold: 0.0, + angularDeactivationThrshold: 0.0, + deactivationTime: 0.0 + }; + + if (flags == null) flags = { + animated: false, + trigger: false, + ccd: false, + staticObj: false, + useDeactivation: true + }; + + this.linearDamping = params.linearDamping; + this.angularDamping = params.angularDamping; + this.angularFriction = params.angularFriction; + this.linearFactors = [params.linearFactorsX, params.linearFactorsY, params.linearFactorsZ]; + this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; + this.collisionMargin = params.collisionMargin; + this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; + this.animated = flags.animated; + this.trigger = flags.trigger; + this.ccd = flags.ccd; + this.staticObj = flags.staticObj; + this.useDeactivation = flags.useDeactivation; + + notifyOnAdd(init); + } + + inline function withMargin(f: Float) { + return f + f * collisionMargin; + } + + public function notifyOnReady(f: Void->Void) { + onReady = f; + if (ready) onReady(); + } + + public function init() { + if (ready) return; + ready = true; + + if (!Std.isOfType(object, MeshObject)) return; // No mesh data + + transform = object.transform; + physics = armory.trait.physics.PhysicsWorld.active; + + if (shape == Shape.Box) { + vec1.setX(withMargin(transform.dim.x / 2)); + vec1.setY(withMargin(transform.dim.y / 2)); + vec1.setZ(withMargin(transform.dim.z / 2)); + btshape = new bullet.Bt.BoxShape(vec1); + } + else if (shape == Shape.Sphere) { + btshape = new bullet.Bt.SphereShape(withMargin(transform.dim.x / 2)); + } + else if (shape == Shape.ConvexHull) { + var shapeConvex = fillConvexHull(transform.scale, collisionMargin); + btshape = shapeConvex; + } + else if (shape == Shape.Cone) { + var coneZ = new bullet.Bt.ConeShapeZ( + withMargin(transform.dim.x / 2), // Radius + withMargin(transform.dim.z)); // Height + var cone: bullet.Bt.ConeShape = coneZ; + btshape = cone; + } + else if (shape == Shape.Cylinder) { + vec1.setX(withMargin(transform.dim.x / 2)); + vec1.setY(withMargin(transform.dim.y / 2)); + vec1.setZ(withMargin(transform.dim.z / 2)); + var cylZ = new bullet.Bt.CylinderShapeZ(vec1); + var cyl: bullet.Bt.CylinderShape = cylZ; + btshape = cyl; + } + else if (shape == Shape.Capsule) { + var r = transform.dim.x / 2; + var capsZ = new bullet.Bt.CapsuleShapeZ( + withMargin(r), // Radius + withMargin(transform.dim.z - r * 2)); // Height between 2 sphere centers + var caps: bullet.Bt.CapsuleShape = capsZ; + btshape = caps; + } + else if (shape == Shape.Mesh) { + meshInterface = fillTriangleMesh(transform.scale); + if (mass > 0) { + var shapeGImpact = new bullet.Bt.GImpactMeshShape(meshInterface); + shapeGImpact.updateBound(); + var shapeConcave: bullet.Bt.ConcaveShape = shapeGImpact; + btshape = shapeConcave; + if (!physics.gimpactRegistered) { + #if js + new bullet.Bt.GImpactCollisionAlgorithm().registerAlgorithm(physics.dispatcher); + #else + shapeGImpact.registerAlgorithm(physics.dispatcher); + #end + physics.gimpactRegistered = true; + } + } + else { + var shapeBvh = new bullet.Bt.BvhTriangleMeshShape(meshInterface, true, true); + var shapeTri: bullet.Bt.TriangleMeshShape = shapeBvh; + var shapeConcave: bullet.Bt.ConcaveShape = shapeTri; + btshape = shapeConcave; + } + } + else if (shape == Shape.Terrain) { + #if js + var length = heightData.length; + if (ammoArray == -1) { + ammoArray = bullet.Bt.Ammo._malloc(length); + } + // From texture bytes + for (i in 0...length) { + bullet.Bt.Ammo.HEAPU8[ammoArray + i] = heightData.get(i); + } + var slice = Std.int(Math.sqrt(length)); // Assuming square terrain data + var axis = 2; // z + var dataType = 5; // u8 + btshape = new bullet.Bt.HeightfieldTerrainShape(slice, slice, ammoArray, 1 / 255, 0, 1, axis, dataType, false); + vec1.setX(transform.dim.x / slice); + vec1.setY(transform.dim.y / slice); + vec1.setZ(transform.dim.z); + btshape.setLocalScaling(vec1); + #end + } + + trans1.setIdentity(); + vec1.setX(transform.worldx()); + vec1.setY(transform.worldy()); + vec1.setZ(transform.worldz()); + trans1.setOrigin(vec1); + quat.fromMat(transform.world); + quat1.setValue(quat.x, quat.y, quat.z, quat.w); + trans1.setRotation(quat1); + + var centerOfMassOffset = trans2; + centerOfMassOffset.setIdentity(); + motionState = new bullet.Bt.DefaultMotionState(trans1, centerOfMassOffset); + + vec1.setX(0); + vec1.setY(0); + vec1.setZ(0); + var inertia = vec1; + + if (staticObj || animated) mass = 0; + if (mass > 0) btshape.calculateLocalInertia(mass, inertia); + var bodyCI = new bullet.Bt.RigidBodyConstructionInfo(mass, motionState, btshape, inertia); + body = new bullet.Bt.RigidBody(bodyCI); + + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setFriction(friction); + bodyColl.setRollingFriction(angularFriction); + bodyColl.setRestitution(restitution); + + if ( useDeactivation) { + setDeactivationParams(deactivationParams[0], deactivationParams[1], deactivationParams[2]); + } + else { + setActivationState(bullet.Bt.CollisionObjectActivationState.DISABLE_DEACTIVATION); + } + + if (linearDamping != 0.04 || angularDamping != 0.1) { + body.setDamping(linearDamping, angularDamping); + } + + if (linearFactors != null) { + setLinearFactor(linearFactors[0], linearFactors[1], linearFactors[2]); + } + + if (angularFactors != null) { + setAngularFactor(angularFactors[0], angularFactors[1], angularFactors[2]); + } + + if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE); + if (animated) { + bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_KINEMATIC_OBJECT); + bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() & ~CF_STATIC_OBJECT); + } + if (staticObj && !animated) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_STATIC_OBJECT); + + if (ccd) setCcd(transform.radius); + + bodyScaleX = currentScaleX = transform.scale.x; + bodyScaleY = currentScaleY = transform.scale.y; + bodyScaleZ = currentScaleZ = transform.scale.z; + + id = nextId; + nextId++; + + #if js + //body.setUserIndex(nextId); + untyped body.userIndex = id; + #else + bodyColl.setUserIndex(id); + #end + + physics.addRigidBody(this); + notifyOnRemove(removeFromWorld); + + if (onReady != null) onReady(); + + #if js + bullet.Bt.Ammo.destroy(bodyCI); + #else + bodyCI.delete(); + #end + } + + function physicsUpdate() { + if (!ready) return; + if (animated) { + syncTransform(); + } + else { + var trans = body.getWorldTransform(); + var p = trans.getOrigin(); + var q = trans.getRotation(); + + transform.loc.set(p.x(), p.y(), p.z()); + transform.rot.set(q.x(), q.y(), q.z(), q.w()); + if (object.parent != null) { + var ptransform = object.parent.transform; + transform.loc.x -= ptransform.worldx(); + transform.loc.y -= ptransform.worldy(); + transform.loc.z -= ptransform.worldz(); + } + transform.buildMatrix(); + + #if hl + p.delete(); + q.delete(); + trans.delete(); + #end + } + + if (onContact != null) { + var rbs = physics.getContacts(this); + if (rbs != null) for (rb in rbs) for (f in onContact) f(rb); + } + } + + public function disableCollision() { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE); + } + + public function enableCollision() { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setCollisionFlags(~bodyColl.getCollisionFlags() & CF_NO_CONTACT_RESPONSE); + } + + public function removeFromWorld() { + if (physics != null) physics.removeRigidBody(this); + } + + public function isActive() : Bool { + return body.isActive(); + } + + public function activate() { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.activate(false); + } + + public function disableGravity() { + vec1.setValue(0, 0, 0); + body.setGravity(vec1); + } + + public function enableGravity() { + body.setGravity(physics.world.getGravity()); + } + + public function setGravity(v: Vec4) { + vec1.setValue(v.x, v.y, v.z); + body.setGravity(vec1); + } + + public function setActivationState(newState: Int) { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setActivationState(newState); + } + + public function setDeactivationParams(linearThreshold: Float, angularThreshold: Float, time: Float) { + body.setSleepingThresholds(linearThreshold, angularThreshold); + // body.setDeactivationTime(time); // not available in ammo + } + + public function setUpDeactivation(useDeactivation: Bool, linearThreshold: Float, angularThreshold: Float, time: Float) { + this.useDeactivation = useDeactivation; + this.deactivationParams[0] = linearThreshold; + this.deactivationParams[1] = angularThreshold; + this.deactivationParams[2] = time; + } + + public function isTriggerObject(isTrigger: Bool) { + this.trigger = isTrigger; + } + + public function applyForce(force: Vec4, loc: Vec4 = null) { + activate(); + vec1.setValue(force.x, force.y, force.z); + if (loc == null) { + body.applyCentralForce(vec1); + } + else { + vec2.setValue(loc.x, loc.y, loc.z); + body.applyForce(vec1, vec2); + } + } + + public function applyImpulse(impulse: Vec4, loc: Vec4 = null) { + activate(); + vec1.setValue(impulse.x, impulse.y, impulse.z); + if (loc == null) { + body.applyCentralImpulse(vec1); + } + else { + vec2.setValue(loc.x, loc.y, loc.z); + body.applyImpulse(vec1, vec2); + } + } + + public function applyTorque(torque: Vec4) { + activate(); + vec1.setValue(torque.x, torque.y, torque.z); + body.applyTorque(vec1); + } + + public function applyTorqueImpulse(torque: Vec4) { + activate(); + vec1.setValue(torque.x, torque.y, torque.z); + body.applyTorqueImpulse(vec1); + } + + public function setLinearFactor(x: Float, y: Float, z: Float) { + vec1.setValue(x, y, z); + body.setLinearFactor(vec1); + } + + public function setAngularFactor(x: Float, y: Float, z: Float) { + vec1.setValue(x, y, z); + body.setAngularFactor(vec1); + } + + public function getLinearVelocity(): Vec4 { + var v = body.getLinearVelocity(); + return new Vec4(v.x(), v.y(), v.z()); + } + + public function setLinearVelocity(x: Float, y: Float, z: Float) { + vec1.setValue(x, y, z); + body.setLinearVelocity(vec1); + } + + public function getAngularVelocity(): Vec4 { + var v = body.getAngularVelocity(); + return new Vec4(v.x(), v.y(), v.z()); + } + + public function setAngularVelocity(x: Float, y: Float, z: Float) { + vec1.setValue(x, y, z); + body.setAngularVelocity(vec1); + } + + public function getPointVelocity(x: Float, y: Float, z: Float) { + var linear = getLinearVelocity(); + + var relativePoint = new Vec4(x, y, z).sub(transform.world.getLoc()); + var angular = getAngularVelocity().cross(relativePoint); + + return linear.add(angular); + } + + public function setFriction(f: Float) { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setFriction(f); + // bodyColl.setRollingFriction(f); + this.friction = f; + } + + public function notifyOnContact(f: RigidBody->Void) { + if (onContact == null) onContact = []; + onContact.push(f); + } + + public function removeContact(f: RigidBody->Void) { + onContact.remove(f); + } + + function setScale(v: Vec4) { + currentScaleX = v.x; + currentScaleY = v.y; + currentScaleZ = v.z; + vec1.setX(v.x / bodyScaleX); + vec1.setY(v.y / bodyScaleY); + vec1.setZ(v.z / bodyScaleZ); + btshape.setLocalScaling(vec1); + var worldDyn: bullet.Bt.DynamicsWorld = physics.world; + var worldCol: bullet.Bt.CollisionWorld = worldDyn; + worldCol.updateSingleAabb(body); + } + + public function syncTransform() { + var t = transform; + t.buildMatrix(); + vec1.setValue(t.worldx(), t.worldy(), t.worldz()); + trans1.setOrigin(vec1); + quat.fromMat(t.world); + quat1.setValue(quat.x, quat.y, quat.z, quat.w); + trans1.setRotation(quat1); + if (animated) body.getMotionState().setWorldTransform(trans1); + else body.setCenterOfMassTransform(trans1); + if (currentScaleX != t.scale.x || currentScaleY != t.scale.y || currentScaleZ != t.scale.z) setScale(t.scale); + activate(); + } + + // Continuous collision detection + public function setCcd(sphereRadius: Float, motionThreshold = 1e-7) { + var bodyColl: bullet.Bt.CollisionObject = body; + bodyColl.setCcdSweptSphereRadius(sphereRadius); + bodyColl.setCcdMotionThreshold(motionThreshold); + } + + function fillConvexHull(scale: Vec4, margin: kha.FastFloat): bullet.Bt.ConvexHullShape { + // Check whether shape already exists + var data = cast(object, MeshObject).data; + var shape = convexHullCache.get(data); + if (shape != null) { + usersCache.set(data, usersCache.get(data) + 1); + return shape; + } + + shape = new bullet.Bt.ConvexHullShape(); + convexHullCache.set(data, shape); + usersCache.set(data, 1); + + var positions = data.geom.positions.values; + + var sx: kha.FastFloat = scale.x * (1.0 - margin) * (1 / 32767); + var sy: kha.FastFloat = scale.y * (1.0 - margin) * (1 / 32767); + var sz: kha.FastFloat = scale.z * (1.0 - margin) * (1 / 32767); + + if (data.raw.scale_pos != null) { + sx *= data.raw.scale_pos; + sy *= data.raw.scale_pos; + sz *= data.raw.scale_pos; + } + + for (i in 0...Std.int(positions.length / 4)) { + vec1.setX(positions[i * 4 ] * sx); + vec1.setY(positions[i * 4 + 1] * sy); + vec1.setZ(positions[i * 4 + 2] * sz); + shape.addPoint(vec1, true); + } + return shape; + } + + function fillTriangleMesh(scale: Vec4): bullet.Bt.TriangleMesh { + // Check whether shape already exists + var data = cast(object, MeshObject).data; + var triangleMesh = triangleMeshCache.get(data); + if (triangleMesh != null) { + usersCache.set(data, usersCache.get(data) + 1); + return triangleMesh; + } + + triangleMesh = new bullet.Bt.TriangleMesh(true, true); + triangleMeshCache.set(data, triangleMesh); + usersCache.set(data, 1); + + var positions = data.geom.positions.values; + var indices = data.geom.indices; + + var sx: kha.FastFloat = scale.x * (1 / 32767); + var sy: kha.FastFloat = scale.y * (1 / 32767); + var sz: kha.FastFloat = scale.z * (1 / 32767); + + if (data.raw.scale_pos != null) { + sx *= data.raw.scale_pos; + sy *= data.raw.scale_pos; + sz *= data.raw.scale_pos; + } + + for (ar in indices) { + for (i in 0...Std.int(ar.length / 3)) { + vec1.setX(positions[ar[i * 3 ] * 4 ] * sx); + vec1.setY(positions[ar[i * 3 ] * 4 + 1] * sy); + vec1.setZ(positions[ar[i * 3 ] * 4 + 2] * sz); + vec2.setX(positions[ar[i * 3 + 1] * 4 ] * sx); + vec2.setY(positions[ar[i * 3 + 1] * 4 + 1] * sy); + vec2.setZ(positions[ar[i * 3 + 1] * 4 + 2] * sz); + vec3.setX(positions[ar[i * 3 + 2] * 4 ] * sx); + vec3.setY(positions[ar[i * 3 + 2] * 4 + 1] * sy); + vec3.setZ(positions[ar[i * 3 + 2] * 4 + 2] * sz); + triangleMesh.addTriangle(vec1, vec2, vec3); + } + } + return triangleMesh; + } + + public function delete() { + #if js + bullet.Bt.Ammo.destroy(motionState); + bullet.Bt.Ammo.destroy(body); + #else + motionState.delete(); + body.delete(); + #end + + // Delete shape if no other user is found + if (shape == Shape.ConvexHull || shape == Shape.Mesh) { + var data = cast(object, MeshObject).data; + var i = usersCache.get(data) - 1; + usersCache.set(data, i); + if(shape == Shape.Mesh) deleteShape(); + if (i <= 0) { + if(shape == Shape.ConvexHull) + { + deleteShape(); + convexHullCache.remove(data); + } + else + { + triangleMeshCache.remove(data); + if(meshInterface != null) + { + #if js + bullet.Bt.Ammo.destroy(meshInterface); + #else + meshInterface.delete(); + #end + } + } + } + } + else deleteShape(); + } + + inline function deleteShape() { + #if js + bullet.Bt.Ammo.destroy(btshape); + #else + btshape.delete(); + #end + } +} + +@:enum abstract Shape(Int) from Int to Int { + var Box = 0; + var Sphere = 1; + var ConvexHull = 2; + var Mesh = 3; + var Cone = 4; + var Cylinder = 5; + var Capsule = 6; + var Terrain = 7; +} + +typedef RigidBodyParams = { + var linearDamping: Float; + var angularDamping: Float; + var angularFriction: Float; + var linearFactorsX: Float; + var linearFactorsY: Float; + var linearFactorsZ: Float; + var angularFactorsX: Float; + var angularFactorsY: Float; + var angularFactorsZ: Float; + var collisionMargin: Float; + var linearDeactivationThreshold: Float; + var angularDeactivationThrshold: Float; + var deactivationTime: Float; +} + +typedef RigidBodyFlags = { + var animated: Bool; + var trigger: Bool; + var ccd: Bool; + var staticObj: Bool; + var useDeactivation: Bool; +} +#end diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx new file mode 100644 index 0000000000..e45344e16b --- /dev/null +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -0,0 +1,277 @@ +package armory.trait.physics.bullet; + +#if arm_bullet + +import iron.math.Vec4; +import iron.math.Mat4; +import iron.Trait; +import iron.object.MeshObject; +import iron.data.Geometry; +import iron.data.MeshData; +import iron.data.SceneFormat; +#if arm_physics_soft +import armory.trait.physics.RigidBody; +import armory.trait.physics.PhysicsWorld; +#end + +class SoftBody extends Trait { +#if (!arm_physics_soft) + public function new() { super(); } +#else + + static var physics: PhysicsWorld = null; + + public var ready = false; + var shape: SoftShape; + var bend: Float; + var mass: Float; + var margin: Float; + + public var vertOffsetX = 0.0; + public var vertOffsetY = 0.0; + public var vertOffsetZ = 0.0; + + public var body: bullet.Bt.SoftBody; + + static var helpers: bullet.Bt.SoftBodyHelpers; + static var helpersCreated = false; + static var worldInfo: bullet.Bt.SoftBodyWorldInfo; + + public function new(shape = SoftShape.Cloth, bend = 0.5, mass = 1.0, margin = 0.04) { + super(); + this.shape = shape; + this.bend = bend; + this.mass = mass; + this.margin = margin; + + iron.Scene.active.notifyOnInit(function() { + notifyOnInit(init); + }); + } + + function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): kha.arrays.Float32Array { + var vals = new kha.arrays.Float32Array(Std.int(ar.length / 4) * 3); + for (i in 0...Std.int(vals.length / 3)) { + vals[i * 3 ] = (ar[i * 4 ] / 32767) * scalePos; + vals[i * 3 + 1] = (ar[i * 4 + 1] / 32767) * scalePos; + vals[i * 3 + 2] = (ar[i * 4 + 2] / 32767) * scalePos; + } + return vals; + } + + function fromU32(ars: Array): kha.arrays.Uint32Array { + var len = 0; + for (ar in ars) len += ar.length; + var vals = new kha.arrays.Uint32Array(len); + var i = 0; + for (ar in ars) { + for (j in 0...ar.length) { + vals[i] = ar[j]; + i++; + } + } + return vals; + } + + var v = new Vec4(); + function init() { + if (ready) return; + ready = true; + + if (physics == null) physics = armory.trait.physics.PhysicsWorld.active; + + var mo = cast(object, MeshObject); + mo.frustumCulling = false; + var geom = mo.data.geom; + + // Parented soft body - clear parent location + if (object.parent != null && object.parent.name != "") { + object.transform.loc.x += object.parent.transform.worldx(); + object.transform.loc.y += object.parent.transform.worldy(); + object.transform.loc.z += object.parent.transform.worldz(); + object.transform.localOnly = true; + object.transform.buildMatrix(); + } + + var positions = fromI16(geom.positions.values, mo.data.scalePos); + for (i in 0...Std.int(positions.length / 3)) { + v.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); + v.applyQuat(object.transform.rot); + v.x *= object.transform.scale.x; + v.y *= object.transform.scale.y; + v.z *= object.transform.scale.z; + v.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); + positions[i * 3 ] = v.x; + positions[i * 3 + 1] = v.y; + positions[i * 3 + 2] = v.z; + } + vertOffsetX = object.transform.worldx(); + vertOffsetY = object.transform.worldy(); + vertOffsetZ = object.transform.worldz(); + + object.transform.scale.set(1, 1, 1); + object.transform.loc.set(0, 0, 0); + object.transform.rot.set(0, 0, 0, 1); + object.transform.buildMatrix(); + + var vecind = fromU32(geom.indices); + var numtri = 0; + for (ar in geom.indices) numtri += Std.int(ar.length / 3); + + if (!helpersCreated) { + helpers = new bullet.Bt.SoftBodyHelpers(); + worldInfo = physics.world.getWorldInfo(); + helpersCreated = true; + } + + #if js + body = helpers.CreateFromTriMesh(worldInfo, cast positions, cast vecind, numtri); + #elseif cpp + untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); + #end + + // body.generateClusters(4); + + #if js + var cfg = body.get_m_cfg(); + cfg.set_viterations(physics.solverIterations); + cfg.set_piterations(physics.solverIterations); + // cfg.set_collisions(0x0001 + 0x0020 + 0x0040); // self collision + // cfg.set_collisions(0x11); // Soft-rigid, soft-soft + if (shape == SoftShape.Volume) { + cfg.set_kDF(0.1); + cfg.set_kDP(0.01); + cfg.set_kPR(bend); + } + + #elseif cpp + body.m_cfg.viterations = physics.solverIterations; + body.m_cfg.piterations = physics.solverIterations; + // body.m_cfg.collisions = 0x0001 + 0x0020 + 0x0040; + if (shape == SoftShape.Volume) { + body.m_cfg.kDF = 0.1; + body.m_cfg.kDP = 0.01; + body.m_cfg.kPR = bend; + } + #end + + body.setTotalMass(mass, false); + body.getCollisionShape().setMargin(margin); + + physics.world.addSoftBody(body, 1, -1); + body.setActivationState(bullet.Bt.CollisionObject.DISABLE_DEACTIVATION); + + notifyOnUpdate(update); + } + + var va = new Vec4(); + var vb = new Vec4(); + var vc = new Vec4(); + var cb = new Vec4(); + var ab = new Vec4(); + function update() { + var mo = cast(object, MeshObject); + var geom = mo.data.geom; + + #if arm_deinterleaved + var v = geom.vertexBuffers[0].lock(); + var n = geom.vertexBuffers[1].lock(); + #else + var v = geom.vertexBuffer.lock(); + var vbPos = geom.vertexBufferMap.get("pos"); + var v2 = vbPos != null ? vbPos.lock() : null; // For shadows + var l = geom.structLength; + #end + var numVerts = Std.int(v.length / l); + + #if js + var nodes = body.get_m_nodes(); + #elseif cpp + var nodes = body.m_nodes; + #end + + var scalePos = 1.0; + for (i in 0...numVerts) { + var node = nodes.at(i); + #if js + var nodePos = node.get_m_x(); + #elseif cpp + var nodePos = node.m_x; + #end + if (Math.abs(nodePos.x()) > scalePos) scalePos = Math.abs(nodePos.x()); + if (Math.abs(nodePos.y()) > scalePos) scalePos = Math.abs(nodePos.y()); + if (Math.abs(nodePos.z()) > scalePos) scalePos = Math.abs(nodePos.z()); + } + mo.data.scalePos = scalePos; + mo.transform.scaleWorld = scalePos; + mo.transform.buildMatrix(); + + for (i in 0...numVerts) { + var node = nodes.at(i); + #if js + var nodePos = node.get_m_x(); + var nodeNor = node.get_m_n(); + #elseif cpp + var nodePos = node.m_x; + var nodeNor = node.m_n; + #end + #if arm_deinterleaved + v.set(i * 4 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.set(i * 4 + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.set(i * 4 + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + n.set(i * 2 , Std.int(nodeNor.x() * 32767)); + n.set(i * 2 + 1, Std.int(nodeNor.y() * 32767)); + v.set(i * 4 + 3, Std.int(nodeNor.z() * 32767)); + #else + v.set(i * l , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.set(i * l + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.set(i * l + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + if (vbPos != null) { + v2.set(i * 4 , v.get(i * l )); + v2.set(i * 4 + 1, v.get(i * l + 1)); + v2.set(i * 4 + 2, v.get(i * l + 2)); + } + v.set(i * l + 3, Std.int(nodeNor.z() * 32767)); + v.set(i * l + 4, Std.int(nodeNor.x() * 32767)); + v.set(i * l + 5, Std.int(nodeNor.y() * 32767)); + #end + } + // for (i in 0...Std.int(geom.indices[0].length / 3)) { + // var a = geom.indices[0][i * 3]; + // var b = geom.indices[0][i * 3 + 1]; + // var c = geom.indices[0][i * 3 + 2]; + // va.set(v.get(a * l), v.get(a * l + 1), v.get(a * l + 2)); + // vb.set(v.get(b * l), v.get(b * l + 1), v.get(b * l + 2)); + // vc.set(v.get(c * l), v.get(c * l + 1), v.get(c * l + 2)); + // cb.subvecs(vc, vb); + // ab.subvecs(va, vb); + // cb.cross(ab); + // cb.normalize(); + // v.set(a * l + 3, cb.x); + // v.set(a * l + 4, cb.y); + // v.set(a * l + 5, cb.z); + // v.set(b * l + 3, cb.x); + // v.set(b * l + 4, cb.y); + // v.set(b * l + 5, cb.z); + // v.set(c * l + 3, cb.x); + // v.set(c * l + 4, cb.y); + // v.set(c * l + 5, cb.z); + // } + #if arm_deinterleaved + geom.vertexBuffers[0].unlock(); + geom.vertexBuffers[1].unlock(); + #else + geom.vertexBuffer.unlock(); + if (vbPos != null) vbPos.unlock(); + #end + } + +#end +} + +@:enum abstract SoftShape(Int) from Int { + var Cloth = 0; + var Volume = 1; +} + +#end diff --git a/Sources/armory/ui/Canvas.hx b/Sources/armory/ui/Canvas.hx new file mode 100644 index 0000000000..c9950e96c2 --- /dev/null +++ b/Sources/armory/ui/Canvas.hx @@ -0,0 +1,498 @@ +package armory.ui; + +import zui.Zui; + +using zui.GraphicsExtension; + +@:access(zui.Zui) +class Canvas { + + public static inline var defaultFontName = "font_default.ttf"; + + public static var assetMap = new Map(); // kha.Image | kha.Font + public static var themes = new Array(); + static var events:Array = []; + + public static var screenW = -1; + public static var screenH = -1; + public static var locale = "en"; + static var _ui: Zui; + static var h = new zui.Zui.Handle(); // TODO: needs one handle per canvas + + public static function draw(ui: Zui, canvas: TCanvas, g: kha.graphics2.Graphics): Array { + + screenW = kha.System.windowWidth(); + screenH = kha.System.windowHeight(); + + events.resize(0); + + _ui = ui; + + g.end(); + ui.begin(g); // Bake elements + g.begin(false); + + ui.g = g; + + for (elem in canvas.elements) { + if (elem.parent == null) drawElement(ui, canvas, elem); + } + + g.end(); + ui.end(); // Finish drawing + g.begin(false); + + return events; + } + + static function drawElement(ui: Zui, canvas: TCanvas, element: TElement, px = 0.0, py = 0.0) { + + if (element == null || element.visible == false) return; + + var anchorOffset = getAnchorOffset(canvas, element); + px += anchorOffset[0]; + py += anchorOffset[1]; + + ui._x = canvas.x + scaled(element.x) + px; + ui._y = canvas.y + scaled(element.y) + py; + ui._w = scaled(element.width); + + var rotated = element.rotation != null && element.rotation != 0; + if (rotated) ui.g.pushRotation(element.rotation, ui._x + scaled(element.width) / 2, ui._y + scaled(element.height) / 2); + + var font = ui.ops.font; + var fontAsset = isFontAsset(element.asset); + if (fontAsset) ui.ops.font = getAsset(canvas, element.asset); + + switch (element.type) { + case Text: + var prevFontSize = ui.fontSize; + var prevTEXT_COL = ui.t.TEXT_COL; + ui.fontSize = scaled(element.height); + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + + ui.text(getText(canvas, element), element.alignment); + + ui.fontSize = prevFontSize; + ui.t.TEXT_COL = prevTEXT_COL; + + case Button: + var prevELEMENT_H = ui.t.ELEMENT_H; + var prevBUTTON_H = ui.t.BUTTON_H; + var prevBUTTON_COL = ui.t.BUTTON_COL; + var prevBUTTON_TEXT_COL = ui.t.BUTTON_TEXT_COL; + var prevBUTTON_HOVER_COL = ui.t.BUTTON_HOVER_COL; + var prevBUTTON_PRESSED_COL = ui.t.BUTTON_PRESSED_COL; + ui.t.ELEMENT_H = element.height; + ui.t.BUTTON_H = element.height; + ui.t.BUTTON_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.BUTTON_TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).BUTTON_TEXT_COL); + ui.t.BUTTON_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + ui.t.BUTTON_PRESSED_COL = getColor(element.color_press, getTheme(canvas.theme).BUTTON_PRESSED_COL); + + if (ui.button(getText(canvas, element), element.alignment)) { + var e = element.event; + if (e != null && e != "") events.push(e); + } + + ui.t.ELEMENT_H = prevELEMENT_H; + ui.t.BUTTON_H = prevBUTTON_H; + ui.t.BUTTON_COL = prevBUTTON_COL; + ui.t.BUTTON_TEXT_COL = prevBUTTON_TEXT_COL; + ui.t.BUTTON_HOVER_COL = prevBUTTON_HOVER_COL; + ui.t.BUTTON_PRESSED_COL = prevBUTTON_PRESSED_COL; + + case Image: + var image = getAsset(canvas, element.asset); + if (image != null && !fontAsset) { + ui.imageScrollAlign = false; + var tint = element.color != null ? element.color : 0xffffffff; + if (ui.image(image, tint, scaled(element.height)) == zui.Zui.State.Released) { + var e = element.event; + if (e != null && e != "") events.push(e); + } + ui.imageScrollAlign = true; + } + + case FRectangle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.fillRect(ui._x, ui._y, ui._w, scaled(element.height)); + ui.g.color = col; + + case FCircle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.fillCircle(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2); + ui.g.color = col; + + case Rectangle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.drawRect(ui._x, ui._y, ui._w, scaled(element.height), element.strength); + ui.g.color = col; + + case Circle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.drawCircle(ui._x+(scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2, element.strength); + ui.g.color = col; + + case FTriangle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.fillTriangle(ui._x + (ui._w / 2), ui._y, ui._x, ui._y + scaled(element.height), ui._x + ui._w, ui._y + scaled(element.height)); + ui.g.color = col; + + case Triangle: + var col = ui.g.color; + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.drawLine(ui._x + (ui._w / 2), ui._y, ui._x, ui._y + scaled(element.height), element.strength); + ui.g.drawLine(ui._x, ui._y + scaled(element.height), ui._x + ui._w, ui._y + scaled(element.height), element.strength); + ui.g.drawLine(ui._x + ui._w, ui._y + scaled(element.height), ui._x + (ui._w / 2), ui._y, element.strength); + ui.g.color = col; + + case Check: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + ui.check(h.nest(element.id), getText(canvas, element)); + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case Radio: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + zui.Ext.inlineRadio(ui, h.nest(element.id), getText(canvas, element).split(";")); + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case Combo: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevLABEL_COL = ui.t.LABEL_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevSEPARATOR_COL = ui.t.SEPARATOR_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.SEPARATOR_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + ui.combo(h.nest(element.id), getText(canvas, element).split(";")); + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.LABEL_COL = prevLABEL_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.SEPARATOR_COL = prevSEPARATOR_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case Slider: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevLABEL_COL = ui.t.LABEL_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + ui.slider(h.nest(element.id), getText(canvas, element), 0.0, 1.0, true, 100, true, element.alignment); + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.LABEL_COL = prevLABEL_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case TextInput: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevLABEL_COL = ui.t.LABEL_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + ui.textInput(h.nest(element.id), getText(canvas, element), element.alignment); + if (h.nest(element.id).changed) { + var e = element.event; + if (e != null && e != "") events.push(e); + } + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.LABEL_COL = prevLABEL_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case TextArea: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevLABEL_COL = ui.t.LABEL_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + h.nest(element.id).text = getText(canvas, element); + zui.Ext.textArea(ui,h.nest(element.id), element.alignment,element.editable); + if (h.nest(element.id).changed) { + var e = element.event; + if (e != null && e != "") events.push(e); + } + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.LABEL_COL = prevLABEL_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case KeyInput: + var prevTEXT_COL = ui.t.TEXT_COL; + var prevLABEL_COL = ui.t.LABEL_COL; + var prevACCENT_COL = ui.t.ACCENT_COL; + var prevACCENT_HOVER_COL = ui.t.ACCENT_HOVER_COL; + ui.t.TEXT_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.LABEL_COL = getColor(element.color_text, getTheme(canvas.theme).TEXT_COL); + ui.t.ACCENT_COL = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.t.ACCENT_HOVER_COL = getColor(element.color_hover, getTheme(canvas.theme).BUTTON_HOVER_COL); + + Ext.keyInput(ui, h.nest(element.id), getText(canvas, element)); + + ui.t.TEXT_COL = prevTEXT_COL; + ui.t.LABEL_COL = prevLABEL_COL; + ui.t.ACCENT_COL = prevACCENT_COL; + ui.t.ACCENT_HOVER_COL = prevACCENT_HOVER_COL; + + case ProgressBar: + var col = ui.g.color; + var progress = element.progress_at; + var totalprogress = element.progress_total; + ui.g.color = getColor(element.color_progress, getTheme(canvas.theme).TEXT_COL); + ui.g.fillRect(ui._x, ui._y, ui._w / totalprogress * Math.min(progress, totalprogress), scaled(element.height)); + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.drawRect(ui._x, ui._y, ui._w, scaled(element.height), element.strength); + ui.g.color = col; + + case CProgressBar: + var col = ui.g.color; + var progress = element.progress_at; + var totalprogress = element.progress_total; + ui.g.color = getColor(element.color_progress, getTheme(canvas.theme).TEXT_COL); + ui.g.drawArc(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), ui._w / 2, -Math.PI / 2, ((Math.PI * 2) / totalprogress * progress) - Math.PI / 2, element.strength); + ui.g.color = getColor(element.color, getTheme(canvas.theme).BUTTON_COL); + ui.g.fillCircle(ui._x + (scaled(element.width) / 2), ui._y + (scaled(element.height) / 2), (ui._w / 2) - 10); + ui.g.color = col; + + case Empty: + } + + ui.ops.font = font; + + if (element.children != null) { + for (id in element.children) { + drawElement(ui, canvas, elemById(canvas, id), scaled(element.x) + px, scaled(element.y) + py); + } + } + + if (rotated) ui.g.popTransformation(); + } + + static inline function getText(canvas: TCanvas, e: TElement): String { + return e.text; + } + + public static function getAsset(canvas: TCanvas, asset: String): Dynamic { // kha.Image | kha.Font { + for (a in canvas.assets) if (a.name == asset) return assetMap.get(a.id); + return null; + } + + static var elemId = -1; + public static function getElementId(canvas: TCanvas): Int { + if (elemId == -1) for (e in canvas.elements) if (elemId < e.id) elemId = e.id; + return ++elemId; + } + + static var assetId = -1; + public static function getAssetId(canvas: TCanvas): Int { + if (assetId == -1) for (a in canvas.assets) if (assetId < a.id) assetId = a.id; + return ++assetId; + } + + static function elemById(canvas: TCanvas, id: Int): TElement { + for (e in canvas.elements) if (e.id == id) return e; + return null; + } + + static inline function scaled(f: Float): Int { + return Std.int(f * _ui.SCALE()); + } + + public static inline function isFontAsset(assetName: Null): Bool { + return assetName != null && StringTools.endsWith(assetName.toLowerCase(), ".ttf"); + } + + public static inline function getColor(color: Null, defaultColor: Int): Int { + return color != null ? color : defaultColor; + } + + public static function getTheme(theme: String): zui.Themes.TTheme { + for (t in Canvas.themes) { + if (t.NAME == theme) return t; + } + return null; + } + + /** + Returns the positional scaled offset of the given element based on its anchor setting. + @param canvas The canvas object + @param element The element + @return `Array [xOffset, yOffset]` + **/ + public static function getAnchorOffset(canvas: TCanvas, element: TElement): Array { + var boxWidth, boxHeight: Float; + var offsetX = 0.0; + var offsetY = 0.0; + + if (element.parent == null) { + boxWidth = canvas.width; + boxHeight = canvas.height; + } + else { + var parent = elemById(canvas, element.parent); + boxWidth = scaled(parent.width); + boxHeight = scaled(parent.height); + } + + switch (element.anchor) { + case Top: + offsetX += boxWidth / 2 - scaled(element.width) / 2; + case TopRight: + offsetX += boxWidth - scaled(element.width); + case CenterLeft: + offsetY += boxHeight / 2 - scaled(element.height) / 2; + case Center: + offsetX += boxWidth / 2 - scaled(element.width) / 2; + offsetY += boxHeight / 2 - scaled(element.height) / 2; + case CenterRight: + offsetX += boxWidth - scaled(element.width); + offsetY += boxHeight / 2 - scaled(element.height) / 2; + case BottomLeft: + offsetY += boxHeight - scaled(element.height); + case Bottom: + offsetX += boxWidth / 2 - scaled(element.width) / 2; + offsetY += boxHeight - scaled(element.height); + case BottomRight: + offsetX += boxWidth - scaled(element.width); + offsetY += boxHeight - scaled(element.height); + } + + return [offsetX, offsetY]; + } +} + +typedef TCanvas = { + var name: String; + var x: Float; + var y: Float; + var width: Int; + var height: Int; + var elements: Array; + var theme: String; + @:optional var assets: Array; + @:optional var locales: Array; +} + +typedef TElement = { + var id: Int; + var type: ElementType; + var name: String; + var x: Float; + var y: Float; + var width: Int; + var height: Int; + @:optional var rotation: Null; + @:optional var text: String; + @:optional var event: String; + // null = follow theme settings + @:optional var color: Null; + @:optional var color_text: Null; + @:optional var color_hover: Null; + @:optional var color_press: Null; + @:optional var color_progress: Null; + @:optional var progress_at: Null; + @:optional var progress_total: Null; + @:optional var strength: Null; + @:optional var alignment: Null; + @:optional var anchor: Null; + @:optional var parent: Null; // id + @:optional var children: Array; // ids + @:optional var asset: String; + @:optional var visible: Null; + @:optional var editable: Null; +} + +typedef TAsset = { + var id: Int; + var name: String; + var file: String; +} + +typedef TLocale = { + var name: String; // "en" + var texts: Array; +} + +typedef TTranslatedText = { + var id: Int; // element id + var text: String; +} + +@:enum abstract ElementType(Int) from Int to Int { + var Text = 0; + var Image = 1; + var Button = 2; + var Empty = 3; + // var HLayout = 4; + // var VLayout = 5; + var Check = 6; + var Radio = 7; + var Combo = 8; + var Slider = 9; + var TextInput = 10; + var KeyInput = 11; + var FRectangle = 12; + var Rectangle = 13; + var FCircle = 14; + var Circle = 15; + var FTriangle = 16; + var Triangle = 17; + var ProgressBar = 18; + var CProgressBar = 19; + var TextArea = 20; +} + +@:enum abstract Anchor(Int) from Int to Int { + var TopLeft = 0; + var Top = 1; + var TopRight = 2; + var CenterLeft = 3; + var Center = 4; + var CenterRight = 5; + var BottomLeft = 6; + var Bottom = 7; + var BottomRight = 8; +} diff --git a/Sources/armory/ui/Ext.hx b/Sources/armory/ui/Ext.hx new file mode 100644 index 0000000000..1197ef46be --- /dev/null +++ b/Sources/armory/ui/Ext.hx @@ -0,0 +1,331 @@ +package armory.ui; + +import zui.Zui; +import kha.input.Keyboard; +import kha.input.KeyCode; + +typedef ListOpts = { + ?addCb: String->Void, + ?removeCb: Int->Void, + ?getNameCb: Int->String, + ?setNameCb: Int->String->Void, + ?getLabelCb: Int->String, + ?itemDrawCb: Handle->Int->Void, + ?showRadio: Bool, // false + ?editable: Bool, // true + ?showAdd: Bool, // true + ?addLabel: String // 'Add' +} + +@:access(zui.Zui) +class Ext { + public static function keyInput(ui: Zui, handle: Handle, label = "", align: Align = Left): Int { + if (!ui.isVisible(ui.ELEMENT_H())) { + ui.endElement(); + return Std.int(handle.value); + } + + var hover = ui.getHover(); + if (hover && Zui.onTextHover != null) Zui.onTextHover(); + ui.g.color = hover ? ui.t.ACCENT_HOVER_COL : ui.t.ACCENT_COL; // Text bg + ui.drawRect(ui.g, ui.t.FILL_ACCENT_BG, ui._x + ui.buttonOffsetY, ui._y + ui.buttonOffsetY, ui._w - ui.buttonOffsetY * 2, ui.BUTTON_H()); + + var startEdit = ui.getReleased() || ui.tabPressed; + if (ui.textSelectedHandle != handle && startEdit) ui.startTextEdit(handle); + if (ui.textSelectedHandle == handle) Ext.listenToKey(ui, handle); + else handle.changed = false; + + if (label != "") { + ui.g.color = ui.t.LABEL_COL; // Label + var labelAlign = align == Align.Right ? Align.Left : Align.Right; + var xOffset = labelAlign == Align.Left ? 7 : 0; + ui.drawString(ui.g, label, xOffset, 0, labelAlign); + } + + handle.text = Ext.keycodeToString(Std.int(handle.value)); + + ui.g.color = ui.t.TEXT_COL; // Text + ui.textSelectedHandle != handle ? ui.drawString(ui.g, handle.text, null, 0, align) : ui.drawString(ui.g, ui.textSelected, null, 0, align); + + ui.endElement(); + + return Std.int(handle.value); + } + + static function listenToKey(ui: Zui, handle: Handle) { + if (ui.isKeyDown) { + handle.value = ui.key; + handle.changed = ui.changed = true; + + ui.textSelectedHandle = null; + ui.isTyping = false; + + if (Keyboard.get() != null) Keyboard.get().hide(); + } + else { + ui.textSelected = "Press a key..."; + } + } + + public static function list(ui: Zui, handle: Handle, ar: Array, ?opts: ListOpts ): Int { + var selected = 0; + if (opts == null) opts = {}; + + var addCb = opts.addCb != null ? opts.addCb : function(name: String) ar.push(name); + var removeCb = opts.removeCb != null ? opts.removeCb : function(i: Int) ar.splice(i, 1); + var getNameCb = opts.getNameCb != null ? opts.getNameCb : function(i: Int) return ar[i]; + var setNameCb = opts.setNameCb != null ? opts.setNameCb : function(i: Int, name: String) ar[i] = name; + var getLabelCb = opts.getLabelCb != null ? opts.getLabelCb : function(i: Int) return ""; + var itemDrawCb = opts.itemDrawCb; + var showRadio = opts.showRadio != null ? opts.showRadio : false; + var editable = opts.editable != null ? opts.editable : true; + var showAdd = opts.showAdd != null ? opts.showAdd : true; + var addLabel = opts.addLabel != null ? opts.addLabel : "Add"; + + var i = 0; + while (i < ar.length) { + if (showRadio) { // Prepend ratio button + ui.row([0.12, 0.68, 0.2]); + if (ui.radio(handle.nest(0), i, "")) { + selected = i; + } + } + else ui.row([0.8, 0.2]); + + var itemHandle = handle.nest(i); + itemHandle.text = getNameCb(i); + editable ? setNameCb(i, ui.textInput(itemHandle, getLabelCb(i))) : ui.text(getNameCb(i)); + if (ui.button("X")) removeCb(i); + else i++; + + if (itemDrawCb != null) itemDrawCb(itemHandle.nest(i), i - 1); + } + if (showAdd && ui.button(addLabel)) addCb("untitled"); + + return selected; + } + + public static function panelList(ui: Zui, handle: Handle, ar: Array, + addCb: String->Void = null, + removeCb: Int->Void = null, + getNameCb: Int->String = null, + setNameCb: Int->String->Void = null, + itemDrawCb: Handle->Int->Void = null, + editable = true, + showAdd = true, + addLabel: String = "Add") { + + if (addCb == null) addCb = function(name: String) { ar.push(name); }; + if (removeCb == null) removeCb = function(i: Int) { ar.splice(i, 1); }; + if (getNameCb == null) getNameCb = function(i: Int) { return ar[i]; }; + if (setNameCb == null) setNameCb = function(i: Int, name: String) { ar[i] = name; }; + + var i = 0; + while (i < ar.length) { + ui.row([0.12, 0.68, 0.2]); + var expanded = ui.panel(handle.nest(i), ""); + + var itemHandle = handle.nest(i); + editable ? setNameCb(i, ui.textInput(itemHandle, getNameCb(i))) : ui.text(getNameCb(i)); + if (ui.button("X")) removeCb(i); + else i++; + + if (itemDrawCb != null && expanded) itemDrawCb(itemHandle.nest(i), i - 1); + } + if (showAdd && ui.button(addLabel)) { + addCb("untitled"); + } + } + + public static function colorField(ui: Zui, handle:Handle, alpha = false): Int { + ui.g.color = handle.color; + + ui.drawRect(ui.g, true, ui._x + 2, ui._y + ui.buttonOffsetY, ui._w - 4, ui.BUTTON_H()); + ui.g.color = ui.getHover() ? ui.t.ACCENT_HOVER_COL : ui.t.ACCENT_COL; + ui.drawRect(ui.g, false, ui._x + 2, ui._y + ui.buttonOffsetY, ui._w - 4, ui.BUTTON_H(), 1.0); + + if (ui.getStarted()) { + Popup.showCustom( + new Zui(ui.ops), + function(ui:Zui) { + zui.Ext.colorWheel(ui, handle, alpha); + }, + Std.int(ui.inputX), Std.int(ui.inputY), 200, 500); + } + + ui.endElement(); + return handle.color; + } + + public static function colorPicker(ui: Zui, handle: Handle, alpha = false): Int { + var r = ui.slider(handle.nest(0, {value: handle.color.R}), "R", 0, 1, true); + var g = ui.slider(handle.nest(1, {value: handle.color.G}), "G", 0, 1, true); + var b = ui.slider(handle.nest(2, {value: handle.color.B}), "B", 0, 1, true); + var a = handle.color.A; + if (alpha) a = ui.slider(handle.nest(3, {value: a}), "A", 0, 1, true); + var col = kha.Color.fromFloats(r, g, b, a); + ui.text("", Right, col); + return col; + } + + /** + Keycodes can be found here: http://api.kha.tech/kha/input/KeyCode.html + **/ + static function keycodeToString(keycode: Int): String { + return switch (keycode) { + default: String.fromCharCode(keycode); + case -1: "None"; + case KeyCode.Unknown: "Unknown"; + case KeyCode.Back: "Back"; + case KeyCode.Cancel: "Cancel"; + case KeyCode.Help: "Help"; + case KeyCode.Backspace: "Backspace"; + case KeyCode.Tab: "Tab"; + case KeyCode.Clear: "Clear"; + case KeyCode.Return: "Return"; + case KeyCode.Shift: "Shift"; + case KeyCode.Control: "Ctrl"; + case KeyCode.Alt: "Alt"; + case KeyCode.Pause: "Pause"; + case KeyCode.CapsLock: "CapsLock"; + case KeyCode.Kana: "Kana"; + // case KeyCode.Hangul: "Hangul"; // Hangul == Kana + case KeyCode.Eisu: "Eisu"; + case KeyCode.Junja: "Junja"; + case KeyCode.Final: "Final"; + case KeyCode.Hanja: "Hanja"; + // case KeyCode.Kanji: "Kanji"; // Kanji == Hanja + case KeyCode.Escape: "Esc"; + case KeyCode.Convert: "Convert"; + case KeyCode.NonConvert: "NonConvert"; + case KeyCode.Accept: "Accept"; + case KeyCode.ModeChange: "ModeChange"; + case KeyCode.Space: "Space"; + case KeyCode.PageUp: "PageUp"; + case KeyCode.PageDown: "PageDown"; + case KeyCode.End: "End"; + case KeyCode.Home: "Home"; + case KeyCode.Left: "Left"; + case KeyCode.Up: "Up"; + case KeyCode.Right: "Right"; + case KeyCode.Down: "Down"; + case KeyCode.Select: "Select"; + case KeyCode.Print: "Print"; + case KeyCode.Execute: "Execute"; + case KeyCode.PrintScreen: "PrintScreen"; + case KeyCode.Insert: "Insert"; + case KeyCode.Delete: "Delete"; + case KeyCode.Colon: "Colon"; + case KeyCode.Semicolon: "Semicolon"; + case KeyCode.LessThan: "LessThan"; + case KeyCode.Equals: "Equals"; + case KeyCode.GreaterThan: "GreaterThan"; + case KeyCode.QuestionMark: "QuestionMark"; + case KeyCode.At: "At"; + case KeyCode.Win: "Win"; + case KeyCode.ContextMenu: "ContextMenu"; + case KeyCode.Sleep: "Sleep"; + case KeyCode.Numpad0: "Numpad0"; + case KeyCode.Numpad1: "Numpad1"; + case KeyCode.Numpad2: "Numpad2"; + case KeyCode.Numpad3: "Numpad3"; + case KeyCode.Numpad4: "Numpad4"; + case KeyCode.Numpad5: "Numpad5"; + case KeyCode.Numpad6: "Numpad6"; + case KeyCode.Numpad7: "Numpad7"; + case KeyCode.Numpad8: "Numpad8"; + case KeyCode.Numpad9: "Numpad9"; + case KeyCode.Multiply: "Multiply"; + case KeyCode.Add: "Add"; + case KeyCode.Separator: "Separator"; + case KeyCode.Subtract: "Subtract"; + case KeyCode.Decimal: "Decimal"; + case KeyCode.Divide: "Divide"; + case KeyCode.F1: "F1"; + case KeyCode.F2: "F2"; + case KeyCode.F3: "F3"; + case KeyCode.F4: "F4"; + case KeyCode.F5: "F5"; + case KeyCode.F6: "F6"; + case KeyCode.F7: "F7"; + case KeyCode.F8: "F8"; + case KeyCode.F9: "F9"; + case KeyCode.F10: "F10"; + case KeyCode.F11: "F11"; + case KeyCode.F12: "F12"; + case KeyCode.F13: "F13"; + case KeyCode.F14: "F14"; + case KeyCode.F15: "F15"; + case KeyCode.F16: "F16"; + case KeyCode.F17: "F17"; + case KeyCode.F18: "F18"; + case KeyCode.F19: "F19"; + case KeyCode.F20: "F20"; + case KeyCode.F21: "F21"; + case KeyCode.F22: "F22"; + case KeyCode.F23: "F23"; + case KeyCode.F24: "F24"; + case KeyCode.NumLock: "NumLock"; + case KeyCode.ScrollLock: "ScrollLock"; + case KeyCode.WinOemFjJisho: "WinOemFjJisho"; + case KeyCode.WinOemFjMasshou: "WinOemFjMasshou"; + case KeyCode.WinOemFjTouroku: "WinOemFjTouroku"; + case KeyCode.WinOemFjLoya: "WinOemFjLoya"; + case KeyCode.WinOemFjRoya: "WinOemFjRoya"; + case KeyCode.Circumflex: "Circumflex"; + case KeyCode.Exclamation: "Exclamation"; + case KeyCode.DoubleQuote: "DoubleQuote"; + case KeyCode.Hash: "Hash"; + case KeyCode.Dollar: "Dollar"; + case KeyCode.Percent: "Percent"; + case KeyCode.Ampersand: "Ampersand"; + case KeyCode.Underscore: "Underscore"; + case KeyCode.OpenParen: "OpenParen"; + case KeyCode.CloseParen: "CloseParen"; + case KeyCode.Asterisk: "Asterisk"; + case KeyCode.Plus: "Plus"; + case KeyCode.Pipe: "Pipe"; + case KeyCode.HyphenMinus: "HyphenMinus"; + case KeyCode.OpenCurlyBracket: "OpenCurlyBracket"; + case KeyCode.CloseCurlyBracket: "CloseCurlyBracket"; + case KeyCode.Tilde: "Tilde"; + case KeyCode.VolumeMute: "VolumeMute"; + case KeyCode.VolumeDown: "VolumeDown"; + case KeyCode.VolumeUp: "VolumeUp"; + case KeyCode.Comma: "Comma"; + case KeyCode.Period: "Period"; + case KeyCode.Slash: "Slash"; + case KeyCode.BackQuote: "BackQuote"; + case KeyCode.OpenBracket: "OpenBracket"; + case KeyCode.BackSlash: "BackSlash"; + case KeyCode.CloseBracket: "CloseBracket"; + case KeyCode.Quote: "Quote"; + case KeyCode.Meta: "Meta"; + case KeyCode.AltGr: "AltGr"; + case KeyCode.WinIcoHelp: "WinIcoHelp"; + case KeyCode.WinIco00: "WinIco00"; + case KeyCode.WinIcoClear: "WinIcoClear"; + case KeyCode.WinOemReset: "WinOemReset"; + case KeyCode.WinOemJump: "WinOemJump"; + case KeyCode.WinOemPA1: "WinOemPA1"; + case KeyCode.WinOemPA2: "WinOemPA2"; + case KeyCode.WinOemPA3: "WinOemPA3"; + case KeyCode.WinOemWSCTRL: "WinOemWSCTRL"; + case KeyCode.WinOemCUSEL: "WinOemCUSEL"; + case KeyCode.WinOemATTN: "WinOemATTN"; + case KeyCode.WinOemFinish: "WinOemFinish"; + case KeyCode.WinOemCopy: "WinOemCopy"; + case KeyCode.WinOemAuto: "WinOemAuto"; + case KeyCode.WinOemENLW: "WinOemENLW"; + case KeyCode.WinOemBackTab: "WinOemBackTab"; + case KeyCode.ATTN: "ATTN"; + case KeyCode.CRSEL: "CRSEL"; + case KeyCode.EXSEL: "EXSEL"; + case KeyCode.EREOF: "EREOF"; + case KeyCode.Play: "Play"; + case KeyCode.Zoom: "Zoom"; + case KeyCode.PA1: "PA1"; + case KeyCode.WinOemClear: "WinOemClear"; + } + } +} diff --git a/Sources/armory/ui/Popup.hx b/Sources/armory/ui/Popup.hx new file mode 100644 index 0000000000..1cfa4b494b --- /dev/null +++ b/Sources/armory/ui/Popup.hx @@ -0,0 +1,127 @@ +package armory.ui; + +import zui.Zui; +import kha.System; + +@:access(zui.Zui) +class Popup { + public static var show = false; + + static var ui: Zui = null; + static var hwnd = new Handle(); + static var boxTitle = ""; + static var boxText = ""; + static var boxCommands: Zui->Void = null; + static var modalX = 0; + static var modalY = 0; + static var modalW = 400; + static var modalH = 160; + + public static function render(g: kha.graphics2.Graphics) { + if (boxCommands == null) { + ui.begin(g); + if (ui.window(hwnd, modalX, modalY, modalW, modalH)) { + drawTitle(g); + + for (line in boxText.split("\n")) { + ui.text(line); + } + + ui._y = ui._h - ui.t.BUTTON_H - 10; + ui.row([1/3, 1/3, 1/3]); + ui.endElement(); + if (ui.button("OK")) { + show = false; + } + } + ui.end(); + } + else { + ui.begin(g); + if (ui.window(hwnd, modalX, modalY, modalW, modalH)) { + drawTitle(g); + + ui._y += 10; + boxCommands(ui); + } + ui.end(); + } + } + + public static function drawTitle(g: kha.graphics2.Graphics) { + if (boxTitle != "") { + g.color = ui.t.SEPARATOR_COL; + ui.drawRect(g, true, ui._x, ui._y, ui._w, ui.t.BUTTON_H); + + g.color = ui.t.TEXT_COL; + ui.text(boxTitle); + } + } + + public static function update() { + var inUse = ui.comboSelectedHandle != null; + + // Close popup + if (ui.inputStarted && !inUse) { + if (ui.inputX < modalX || ui.inputX > modalX + modalW || ui.inputY < modalY || ui.inputY > modalY + modalH) { + show = false; + } + } + } + + /** + Displays a message box with a title, a text body and a centered "OK" button. + @param ui the Zui instance for the popup + @param title the title to display + @param text the text to display + **/ + public static function showMessage(ui: Zui, title: String, text: String) { + Popup.ui = ui; + init(); + + boxTitle = title; + boxText = text; + boxCommands = null; + } + + /** + Displays a popup box with custom drawing code. + @param ui the Zui instance for the popup + @param commands the function for drawing the popup's content + @param mx the x position of the popup. -1 = screen center (defaults to -1) + @param my the y position of the popup. -1 = screen center (defaults to -1) + @param mw the width of the popup (defaults to 400) + @param mh the height of the popup (defaults to 160) + **/ + public static function showCustom(ui: Zui, commands: Zui->Void = null, mx = -1, my = -1, mw = 400, mh = 160) { + Popup.ui = ui; + init(mx, my, mw, mh); + + boxTitle = ""; + boxText = ""; + boxCommands = commands; + } + + static function init(mx = -1, my = -1, mw = 400, mh = 160) { + var appW = System.windowWidth(); + var appH = System.windowHeight(); + + modalX = mx; + modalY = my; + modalW = Std.int(mw * ui.SCALE()); + modalH = Std.int(mh * ui.SCALE()); + + // Center popup window if no value is given + if (mx == -1) modalX = Std.int(appW / 2 - modalW / 2); + if (my == -1) modalY = Std.int(appH / 2 - modalH / 2); + + // Limit popup position to screen + modalX = Std.int(Math.max(0, Math.min(modalX, appW - modalW))); + modalY = Std.int(Math.max(0, Math.min(modalY, appH - modalH))); + + hwnd.dragX = 0; + hwnd.dragY = 0; + hwnd.scrollOffset = 0.0; + show = true; + } +} diff --git a/Sources/armory/ui/Themes.hx b/Sources/armory/ui/Themes.hx new file mode 100644 index 0000000000..34d397a90a --- /dev/null +++ b/Sources/armory/ui/Themes.hx @@ -0,0 +1,42 @@ +package armory.ui; + +import zui.Themes; + +class Themes { + + // 2x scaled, for games + public static var light: TTheme = { + NAME: "Default Light", + WINDOW_BG_COL: 0xffefefef, + WINDOW_TINT_COL: 0xff222222, + ACCENT_COL: 0xffeeeeee, + ACCENT_HOVER_COL: 0xffbbbbbb, + ACCENT_SELECT_COL: 0xffaaaaaa, + BUTTON_COL: 0xffcccccc, + BUTTON_TEXT_COL: 0xff222222, + BUTTON_HOVER_COL: 0xffb3b3b3, + BUTTON_PRESSED_COL: 0xffb1b1b1, + TEXT_COL: 0xff999999, + LABEL_COL: 0xffaaaaaa, + SEPARATOR_COL: 0xff999999, + HIGHLIGHT_COL: 0xff205d9c, + CONTEXT_COL: 0xffaaaaaa, + PANEL_BG_COL: 0xffaaaaaa, + FONT_SIZE: 13 * 2, + ELEMENT_W: 100 * 2, + ELEMENT_H: 24 * 2, + ELEMENT_OFFSET: 4 * 2, + ARROW_SIZE: 5 * 2, + BUTTON_H: 22 * 2, + CHECK_SIZE: 15 * 2, + CHECK_SELECT_SIZE: 8 * 2, + SCROLL_W: 6 * 2, + TEXT_OFFSET: 8 * 2, + TAB_W: 12 * 2, + FILL_WINDOW_BG: false, + FILL_BUTTON_BG: true, + FILL_ACCENT_BG: false, + LINK_STYLE: Line, + FULL_TABS: false + }; +} diff --git a/blender/__init__.py b/blender/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/LICENSE.md b/blender/arm/LICENSE.md new file mode 100644 index 0000000000..8cdb8451d9 --- /dev/null +++ b/blender/arm/LICENSE.md @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/blender/arm/__init__.py b/blender/arm/__init__.py new file mode 100644 index 0000000000..5937ac1524 --- /dev/null +++ b/blender/arm/__init__.py @@ -0,0 +1,44 @@ +import importlib +import sys +import types + +# This gets cleared if this package/the __init__ module is reloaded +_module_cache: dict[str, types.ModuleType] = {} + + +def enable_reload(module_name: str): + """Enable reloading for the next time the module with `module_name` + is executed. + """ + mod = sys.modules[module_name] + setattr(mod, module_name.replace('.', '_') + "_DO_RELOAD_MODULE", True) + + +def is_reload(module_name: str) -> bool: + """True if the module given by `module_name` should reload the + modules it imports. This is the case if `enable_reload()` was called + for the module before. + """ + mod = sys.modules[module_name] + return hasattr(mod, module_name.replace('.', '_') + "_DO_RELOAD_MODULE") + + +def reload_module(module: types.ModuleType) -> types.ModuleType: + """Wrapper around importlib.reload() to make sure no module is + reloaded twice. + + Make sure to call this function in the same order in which the + modules are imported to make sure that the reloading respects the + module dependencies. Otherwise modules could depend on other modules + that are not yet reloaded. + + If you import classes or functions from a module, make sure to + re-import them after the module is reloaded. + """ + mod = _module_cache.get(module.__name__, None) + + if mod is None: + mod = importlib.reload(module) + _module_cache[module.__name__] = mod + + return mod diff --git a/blender/arm/api.py b/blender/arm/api.py new file mode 100644 index 0000000000..0f198a3336 --- /dev/null +++ b/blender/arm/api.py @@ -0,0 +1,55 @@ +from typing import Callable, Dict, Optional + +import bpy +from bpy.types import Material, UILayout + +import arm +from arm.material.shader import ShaderContext + +if arm.is_reload(__name__): + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import ShaderContext +else: + drivers: Dict[str, Dict] = {} #dict() + + arm.enable_reload(__name__) + + +def add_driver(driver_name: str, + make_rpass: Callable[[str], Optional[ShaderContext]], + make_rpath: Callable[[], None], + draw_props: Optional[Callable[[UILayout], None]], + draw_mat_props: Optional[Callable[[UILayout, Material], None]]) -> None: + """Register a new driver. If there already exists a driver with the given name, nothing happens. + + @param driver_name Unique name for the new driver that will be displayed in the UI. + @param make_rpass Function to create render passes. Takes the rpass name as a parameter and may return `None`. + @param make_rpath Function to setup the render path. + @param draw_props Function to draw global driver properties inside the render path panel, may be `None`. + @param draw_mat_props Function to draw per-material driver properties in the material tab, may be `None`. + """ + global drivers + + if driver_name in drivers: + return + + drivers[driver_name] = { + 'driver_name': driver_name, + 'make_rpass': make_rpass, + 'make_rpath': make_rpath, + 'draw_props': draw_props, + 'draw_mat_props': draw_mat_props + } + + wrd = bpy.data.worlds['Arm'] + if len(wrd.rp_driver_list) == 0: + wrd.rp_driver_list.add().name = 'Armory' # Add default driver + wrd.rp_driver_list.add().name = driver_name + + +def remove_drivers(): + wrd = bpy.data.worlds['Arm'] + wrd.rp_driver_list.clear() + wrd.rp_driver_list.add().name = 'Armory' + + drivers.clear() diff --git a/blender/arm/assets.py b/blender/arm/assets.py new file mode 100644 index 0000000000..51acce9135 --- /dev/null +++ b/blender/arm/assets.py @@ -0,0 +1,204 @@ +import shutil +import os +import stat +import bpy +import arm.utils +from arm import log + +if arm.is_reload(__name__): + log = arm.reload_module(log) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +assets = [] +reserved_names = ['return.'] +khafile_params = [] +khafile_defs = [] +khafile_defs_last = [] +embedded_data = [] +shaders = [] +shaders_last = [] +shaders_external = [] +shader_datas = [] +shader_passes = [] +shader_passes_assets = {} +shader_cons = {} + +def reset(): + global assets + global khafile_params + global khafile_defs + global khafile_defs_last + global embedded_data + global shaders + global shaders_last + global shaders_external + global shader_datas + global shader_passes + global shader_cons + assets = [] + khafile_params = [] + khafile_defs_last = khafile_defs + khafile_defs = [] + embedded_data = [] + shaders_last = shaders + shaders = [] + shaders_external = [] + shader_datas = [] + shader_passes = [] + shader_cons = {} + shader_cons['mesh_vert'] = [] + shader_cons['depth_vert'] = [] + shader_cons['depth_frag'] = [] + shader_cons['voxel_vert'] = [] + shader_cons['voxel_frag'] = [] + shader_cons['voxel_geom'] = [] + +def add(asset_file): + global assets + + # Asset already exists, do nothing + if asset_file in assets: + return + + asset_file_base = os.path.basename(asset_file) + for f in assets: + f_file_base = os.path.basename(f) + if f_file_base == asset_file_base: + return + + assets.append(asset_file) + + # Reserved file name + for f in reserved_names: + if f in asset_file: + log.warn(f'File "{asset_file}" contains reserved keyword, this will break C++ builds!') + +def add_khafile_def(d): + global khafile_defs + if d not in khafile_defs: + khafile_defs.append(d) + +def add_khafile_param(p): + global khafile_params + if p not in khafile_params: + khafile_params.append(p) + +def add_embedded_data(file): + global embedded_data + if file not in embedded_data: + embedded_data.append(file) + +def add_shader(file): + global shaders + global shaders_last + if file not in shaders: + shaders.append(file) + +def add_shader_data(file): + global shader_datas + if file not in shader_datas: + shader_datas.append(file) + +def add_shader_pass(data_name): + global shader_passes + # Shader data for passes are written into single shader_datas.arm file + add_shader_data(arm.utils.get_fp_build() + '/compiled/Shaders/shader_datas.arm') + if data_name not in shader_passes: + shader_passes.append(data_name) + +def add_shader_external(file): + global shaders_external + shaders_external.append(file) + name = file.split('/')[-1].split('\\')[-1] + add_shader(arm.utils.get_fp_build() + '/compiled/Shaders/' + name) + +invalidate_enabled = True # Disable invalidating during build process + +def remove_readonly(func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + +def invalidate_shader_cache(self, context): + # compiled.inc changed, recompile all shaders next time + global invalidate_enabled + if invalidate_enabled is False: + return + fp = arm.utils.get_fp_build() + if os.path.isdir(fp + '/compiled/Shaders'): + shutil.rmtree(fp + '/compiled/Shaders', onerror=remove_readonly) + if os.path.isdir(fp + '/debug/html5-resources'): + shutil.rmtree(fp + '/debug/html5-resources', onerror=remove_readonly) + if os.path.isdir(fp + '/krom-resources'): + shutil.rmtree(fp + '/krom-resources', onerror=remove_readonly) + if os.path.isdir(fp + '/debug/krom-resources'): + shutil.rmtree(fp + '/debug/krom-resources', onerror=remove_readonly) + if os.path.isdir(fp + '/windows-resources'): + shutil.rmtree(fp + '/windows-resources', onerror=remove_readonly) + if os.path.isdir(fp + '/linux-resources'): + shutil.rmtree(fp + '/linux-resources', onerror=remove_readonly) + if os.path.isdir(fp + '/osx-resources'): + shutil.rmtree(fp + '/osx-resources', onerror=remove_readonly) + +def invalidate_compiled_data(self, context): + global invalidate_enabled + if invalidate_enabled is False: + return + fp = arm.utils.get_fp_build() + if os.path.isdir(fp + '/compiled'): + shutil.rmtree(fp + '/compiled', onerror=remove_readonly) + +def invalidate_mesh_data(self, context): + fp = arm.utils.get_fp_build() + if os.path.isdir(fp + '/compiled/Assets/meshes'): + shutil.rmtree(fp + '/compiled/Assets/meshes', onerror=remove_readonly) + +def invalidate_envmap_data(self, context): + fp = arm.utils.get_fp_build() + if os.path.isdir(fp + '/compiled/Assets/envmaps'): + shutil.rmtree(fp + '/compiled/Assets/envmaps', onerror=remove_readonly) + +def invalidate_unpacked_data(self, context): + fp = arm.utils.get_fp_build() + if os.path.isdir(fp + '/compiled/Assets/unpacked'): + shutil.rmtree(fp + '/compiled/Assets/unpacked', onerror=remove_readonly) + +def invalidate_mesh_cache(self, context): + if context.object is None or context.object.data is None: + return + context.object.data.arm_cached = False + +def invalidate_instance_cache(self, context): + if context.object is None or context.object.data is None: + return + invalidate_mesh_cache(self, context) + for slot in context.object.material_slots: + slot.material.arm_cached = False + +def invalidate_compiler_cache(self, context): + bpy.data.worlds['Arm'].arm_recompile = True + +def shader_equal(sh, ar, shtype): + # Merge equal shaders + for e in ar: + if sh.is_equal(e): + sh.context.data[shtype] = e.context.data[shtype] + sh.is_linked = True + return + ar.append(sh) + +def vs_equal(c, ar): + shader_equal(c.vert, ar, 'vertex_shader') + +def fs_equal(c, ar): + shader_equal(c.frag, ar, 'fragment_shader') + +def gs_equal(c, ar): + shader_equal(c.geom, ar, 'geometry_shader') + +def tcs_equal(c, ar): + shader_equal(c.tesc, ar, 'tesscontrol_shader') + +def tes_equal(c, ar): + shader_equal(c.tese, ar, 'tesseval_shader') diff --git a/blender/arm/custom_icons/bundle.png b/blender/arm/custom_icons/bundle.png new file mode 100644 index 0000000000000000000000000000000000000000..88ac584360bb6cc3bc962759d03e8399c190be37 GIT binary patch literal 630 zcmV-+0*U>JP)Px%FiAu~R9J=WmOXA1K@f$%B6Ttr2VhBw2ypDZ0lonnkT^hEZm>c^+5n_Q1dc$) z0V1%3k6=qA;KY$r4=D(F9c*Ylg6%OgJ=MLxCP2Jws;m3ePuJ^#|5yQ%_T}U0-yrWK z$UjQDlvK=f&Ycxv7$xjKfCAWA;p{vlbrac-LiUn$bejPjJLKa~{tB13MZS~CKF0dn zAgOeSN=bzm)Vj%giR^n}{VcLytdqRZp9@yUS zGT9$HzVRJt`79GaN^)D}P592GiYfAx8;qQ&38|*yfDam~~ zV_&S4NmZOR1p~)+1^klqPSUQV2aZoX4TU6k8Z5=Esf>PhmWIIn$aW4Kn%Q~S+W}$` zdShn&$d(PD1nx!V@4$P&%NpPmI54xH&i>NO;=uhDgj*Yv1H8_V18f09Nn5nr(D5#A zs{&zWFKDaYiJ5gJZG<4Ufk&BOz5s6{+jBE(JWyT*Qj$9WFtdr7otoJz;2Cg|2;kbW zwVvlj1!iTRAgV6iF3q-0fV;rs$bHky#??n$fssQFkX14>D}fg@tFghf4KW&3xCtO+ z@xeycP+(^b07*qoM6N<$f@2RM_5c6? literal 0 HcmV?d00001 diff --git a/blender/arm/custom_icons/haxe.png b/blender/arm/custom_icons/haxe.png new file mode 100644 index 0000000000000000000000000000000000000000..d9c9655f52b03d15c2f7c6932d8ea1b6c23a9a44 GIT binary patch literal 673 zcmV;S0$%-zP)Px%TS-JgR9J=Wm%nQiK@`V7FDmg@L@P@XB!;9G3yTYOu?dKUo!8o03+7L`QY02O zrV|e=1TA9WCb2OA74g7aj7B{O6@E-VOKpD^$k#>P` z6S!r@E5Mq~a~L?{b^RbBcd&*coerL$3!DdLRCOMBl!KV@yn((2-g$<8B!}cjU=5fo z>m1PoU>!J@IBqk41!G?2B=99+l2u>^xCVSp7}o>N057bgITMw#c@0iqFr!3CHCQPmt9yQ>gjs=@>zM2b0h=~xI*_Wp|? zUIUL4%wcaG0E20Hhw`j=3kSK5OS@xfc@GzramrKv?=gUw;j)?a*h{6B!Xwp=@s@!! zaSJ*#;4bct5cLYLW*ywssc(k4>Rinh48)&f!#sAcX8-jEWtY=li)kU200000NkvXX Hu0mjfMBE}N literal 0 HcmV?d00001 diff --git a/blender/arm/custom_icons/wasm.png b/blender/arm/custom_icons/wasm.png new file mode 100644 index 0000000000000000000000000000000000000000..9de60381482b93c590ee0325e774372eb2423784 GIT binary patch literal 414 zcmV;P0b%}$P)Px$SV=@dR9J=Wmi0fF}k@ye9hGg)003TN2#ufD z1hOayFF8&~51`RF0N5=`Y-U>lHi-kQ-GX0}43GgbKnD0100(_G57FGUe@Z1M*^luW z&E0MV*h~A5=iSVDja%tjx=d`C*>NjCNC4{SUXy5ttw|OnE7_+l03cbK*#JP&f4vO= zm|0EIpT1RvDggHr(8~cZR-skWQVAQ3+mM#O#Ao-k1MuxO%mo;Ns}cb~X=XLX^1Dym zq(FXM1UfB0^pI!y1nHXOF`4V-TgWbTx&1WNl+FcUwC|qm9N9rWo$P|--meH~6F`+D z8=y$u$$pXjkgWA8c$HSQ "NodeType": + """Returns the NodeType enum member belonging to the type of + the given blender object.""" + if bobject.type == "MESH": + if bobject.data.polygons: + return cls.MESH + elif bobject.type in ('FONT', 'META'): + return cls.MESH + elif bobject.type == "LIGHT": + return cls.LIGHT + elif bobject.type == "CAMERA": + return cls.CAMERA + elif bobject.type == "SPEAKER": + return cls.SPEAKER + elif bobject.type == "LIGHT_PROBE": + return cls.PROBE + return cls.EMPTY + + +STRUCT_IDENTIFIER = ("object", "bone_object", "mesh_object", + "light_object", "camera_object", "speaker_object", + "decal_object", "probe_object") + +# Internal target names for single FCurve data paths +FCURVE_TARGET_NAMES = { + "location": ("xloc", "yloc", "zloc"), + "rotation_euler": ("xrot", "yrot", "zrot"), + "rotation_quaternion": ("qwrot", "qxrot", "qyrot", "qzrot"), + "scale": ("xscl", "yscl", "zscl"), + "delta_location": ("dxloc", "dyloc", "dzloc"), + "delta_rotation_euler": ("dxrot", "dyrot", "dzrot"), + "delta_rotation_quaternion": ("dqwrot", "dqxrot", "dqyrot", "dqzrot"), + "delta_scale": ("dxscl", "dyscl", "dzscl"), +} + +current_output = None + + +class ArmoryExporter: + """Export to Armory format. + + Some common naming patterns: + - out_[]: Variables starting with "out_" represent data that is + exported to Iron + - bobject: A Blender object (bpy.types.Object). Used because + `object` is a reserved Python keyword + """ + + compress_enabled = False + export_all_flag = True + # Indicates whether rigid body is exported + export_physics = False + optimize_enabled = False + option_mesh_only = False + + # Class names of referenced traits + import_traits: List[str] = [] + + def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): + global current_output + + self.filepath = filepath + self.scene = context.scene if scene is None else scene + self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph + + # The output dict contains all data that is later exported to Iron format + self.output: Dict[str, Any] = {'frame_time': 1.0 / (self.scene.render.fps / self.scene.render.fps_base)} + current_output = self.output + + # Stores the object type ("objectType") and the asset name + # ("structName") in a dict for each object + self.bobject_array: Dict[bpy.types.Object, Dict[str, Union[NodeType, str]]] = {} + self.bobject_bone_array = {} + self.mesh_array = {} + self.light_array = {} + self.probe_array = {} + self.camera_array = {} + self.speaker_array = {} + self.material_array = [] + self.world_array = [] + self.particle_system_array = {} + + self.has_spawning_camera = False + """Whether there is at least one camera in the scene that spawns by default""" + + self.material_to_object_dict = {} + # If no material is assigned, provide default to mimic cycles + self.default_material_objects = [] + self.default_skin_material_objects = [] + self.default_part_material_objects = [] + self.material_to_arm_object_dict = {} + # Stores the link between a blender object and its + # corresponding export data (arm object) + self.object_to_arm_object_dict: Dict[bpy.types.Object, Dict] = {} + + self.bone_tracks = [] + + ArmoryExporter.preprocess() + + @classmethod + def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None) -> None: + """Exports the given scene to the given file path. This is the + function that is called in make.py and the entry point of the + exporter.""" + with arm.profiler.Profile('profile_exporter.prof', arm.utils.get_pref_or_default('profile_exporter', False)): + cls(context, filepath, scene, depsgraph).execute() + + @classmethod + def preprocess(cls): + wrd = bpy.data.worlds['Arm'] + + if wrd.arm_physics == 'Enabled': + cls.export_physics = True + cls.export_navigation = False + if wrd.arm_navigation == 'Enabled': + cls.export_navigation = True + cls.export_ui = False + cls.export_network = False + if wrd.arm_network == 'Enabled': + cls.export_network = True + + @staticmethod + def write_matrix(matrix): + return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], + matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], + matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3], + matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3]] + + def get_meshes_file_path(self, object_id: str, compressed=False) -> str: + index = self.filepath.rfind('/') + mesh_fp = self.filepath[:(index + 1)] + 'meshes/' + + if not os.path.exists(mesh_fp): + os.makedirs(mesh_fp) + + ext = '.lz4' if compressed else '.arm' + return mesh_fp + object_id + ext + + @staticmethod + def get_shape_keys(mesh): + rpdat = arm.utils.get_rp() + if rpdat.arm_morph_target != 'On': + return False + # Metaball + if not hasattr(mesh, 'shape_keys'): + return False + + shape_keys = mesh.shape_keys + if not shape_keys: + return False + if len(shape_keys.key_blocks) < 2: + return False + for shape_key in shape_keys.key_blocks[1:]: + if not shape_key.mute: + return True + return False + + @staticmethod + def get_morph_uv_index(mesh): + i = 0 + for uv_layer in mesh.uv_layers: + if uv_layer.name == 'UVMap_shape_key': + return i + i +=1 + + def find_bone(self, name: str) -> Optional[Tuple[bpy.types.Bone, Dict]]: + """Finds the bone reference (a tuple containing the bone object + and its data) by the given name and returns it.""" + for bone_ref in self.bobject_bone_array.items(): + if bone_ref[0].name == name: + return bone_ref + return None + + @staticmethod + def collect_bone_animation(armature: bpy.types.Object, name: str) -> List[bpy.types.FCurve]: + path = f"pose.bones[\"{name}\"]." + + if armature.animation_data: + action = armature.animation_data.action + if action: + return [fcurve for fcurve in action.fcurves if fcurve.data_path.startswith(path)] + + return [] + + def export_bone(self, armature, bone: bpy.types.Bone, o, action: bpy.types.Action): + rpdat = arm.utils.get_rp() + bobject_ref = self.bobject_bone_array.get(bone) + + if rpdat.arm_use_armature_deform_only: + if not bone.use_deform: + return + + if bobject_ref: + o['type'] = STRUCT_IDENTIFIER[bobject_ref["objectType"].value] + o['name'] = bobject_ref["structName"] + self.export_bone_transform(armature, bone, o, action) + + o['children'] = [] + for sub_bobject in bone.children: + so = {} + self.export_bone(armature, sub_bobject, so, action) + o['children'].append(so) + + @staticmethod + def export_pose_markers(oanim, action): + if action.pose_markers is None or len(action.pose_markers) == 0: + return + + oanim['marker_frames'] = [] + oanim['marker_names'] = [] + + for pos_marker in action.pose_markers: + oanim['marker_frames'].append(int(pos_marker.frame)) + oanim['marker_names'].append(pos_marker.name) + + @staticmethod + def calculate_anim_frame_range(action: bpy.types.Action) -> Tuple[int, int]: + """Calculates the required frame range of the given action by + also taking fcurve modifiers into account. + + Modifiers that are not range-restricted are ignored in this + calculation. + """ + start = action.frame_range[0] + end = action.frame_range[1] + + # Take FCurve modifiers into account if they have a restricted + # frame range + for fcurve in action.fcurves: + for modifier in fcurve.modifiers: + if not modifier.use_restricted_range: + continue + + if modifier.frame_start < start: + start = modifier.frame_start + + if modifier.frame_end > end: + end = modifier.frame_end + + return int(start), int(end) + + @staticmethod + def export_animation_track(fcurve: bpy.types.FCurve, frame_range: Tuple[int, int], target: str) -> Dict: + """This function exports a single animation track.""" + out_track = {'target': target, 'frames': [], 'values': []} + + start = frame_range[0] + end = frame_range[1] + + for frame in range(start, end + 1): + out_track['frames'].append(frame) + out_track['values'].append(fcurve.evaluate(frame)) + + return out_track + + def export_object_transform(self, bobject: bpy.types.Object, o): + wrd = bpy.data.worlds['Arm'] + + # Static transform + o['transform'] = {'values': ArmoryExporter.write_matrix(bobject.matrix_local)} + + # Animated transform + if bobject.animation_data is not None and bobject.type != "ARMATURE": + action = bobject.animation_data.action + + if action is not None: + action_name = arm.utils.safestr(arm.utils.asset_name(action)) + + fp = self.get_meshes_file_path('action_' + action_name, compressed=ArmoryExporter.compress_enabled) + assets.add(fp) + ext = '.lz4' if ArmoryExporter.compress_enabled else '' + if ext == '' and not wrd.arm_minimize: + ext = '.json' + + if 'object_actions' not in o: + o['object_actions'] = [] + o['object_actions'].append('action_' + action_name + ext) + + frame_range = self.calculate_anim_frame_range(action) + out_anim = { + 'begin': frame_range[0], + 'end': frame_range[1], + 'tracks': [] + } + + self.export_pose_markers(out_anim, action) + + unresolved_data_paths = set() + for fcurve in action.fcurves: + data_path = fcurve.data_path + + try: + out_track = self.export_animation_track(fcurve, frame_range, FCURVE_TARGET_NAMES[data_path][fcurve.array_index]) + except KeyError: + if data_path not in FCURVE_TARGET_NAMES: + # This can happen if the target is simply not + # supported or the action shares both bone + # and object transform data (FCURVE_TARGET_NAMES + # only contains object transform targets) + unresolved_data_paths.add(data_path) + continue + # Missing target entry for array_index or something else + raise + + if data_path.startswith('delta_'): + out_anim['has_delta'] = True + + out_anim['tracks'].append(out_track) + + if len(unresolved_data_paths) > 0: + warning = ( + f'The action "{action_name}" has fcurve channels with data paths that could not be resolved.' + ' This can be caused by the following things:\n' + ' - The data paths are not supported.\n' + ' - The action exists on both armature and non-armature objects or has both bone and object transform data.' + ) + if wrd.arm_verbose_output: + warning += f'\n Unresolved data paths: {unresolved_data_paths}' + else: + warning += '\n To see the list of unresolved data paths please recompile with Armory Project > Verbose Output enabled.' + log.warn(warning) + + if True: # not action.arm_cached or not os.path.exists(fp): + if wrd.arm_verbose_output: + print('Exporting object action ' + action_name) + + out_object_action = { + 'name': action_name, + 'anim': out_anim, + 'type': 'object', + 'data_ref': '', + 'transform': None + } + action_file = {'objects': [out_object_action]} + arm.utils.write_arm(fp, action_file) + + def process_bone(self, bone: bpy.types.Bone) -> None: + if ArmoryExporter.export_all_flag or bone.select: + self.bobject_bone_array[bone] = { + "objectType": NodeType.BONE, + "structName": bone.name + } + + for subbobject in bone.children: + self.process_bone(subbobject) + + def process_bobject(self, bobject: bpy.types.Object) -> None: + """Stores some basic information about the given object (its + name and type). + If the given object is an armature, its bones are also + processed. + """ + if ArmoryExporter.export_all_flag or bobject.select_get(): + btype: NodeType = NodeType.get_bobject_type(bobject) + + if btype is not NodeType.MESH and ArmoryExporter.option_mesh_only: + return + + self.bobject_array[bobject] = { + "objectType": btype, + "structName": arm.utils.asset_name(bobject) + } + + if bobject.type == "ARMATURE": + armature: bpy.types.Armature = bobject.data + if armature: + for bone in armature.bones: + if not bone.parent: + self.process_bone(bone) + + if bobject.arm_instanced == 'Off': + for subbobject in bobject.children: + self.process_bobject(subbobject) + + def process_skinned_meshes(self): + """Iterates through all objects that are exported and ensures + that bones are actually stored as bones.""" + for bobject_ref in self.bobject_array.items(): + if bobject_ref[1]["objectType"] is NodeType.MESH: + armature = bobject_ref[0].find_armature() + if armature is not None: + for bone in armature.data.bones: + bone_ref = self.find_bone(bone.name) + if bone_ref is not None: + # If an object is used as a bone, then we + # force its type to be a bone + bone_ref[1]["objectType"] = NodeType.BONE + + def export_bone_transform(self, armature: bpy.types.Object, bone: bpy.types.Bone, o, action: bpy.types.Action): + pose_bone = armature.pose.bones.get(bone.name) + # if pose_bone is not None: + # transform = pose_bone.matrix.copy() + # if pose_bone.parent is not None: + # transform = pose_bone.parent.matrix.inverted_safe() * transform + # else: + transform = bone.matrix_local.copy() + if bone.parent is not None: + transform = (bone.parent.matrix_local.inverted_safe() @ transform) + + o['transform'] = {'values': ArmoryExporter.write_matrix(transform)} + + fcurve_list = self.collect_bone_animation(armature, bone.name) + + if fcurve_list and pose_bone: + begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) + + out_track = {'target': "transform", 'frames': [], 'values': []} + o['anim'] = {'tracks': [out_track]} + + for i in range(begin_frame, end_frame + 1): + out_track['frames'].append(i - begin_frame) + + self.bone_tracks.append((out_track['values'], pose_bone)) + + def use_default_material(self, bobject: bpy.types.Object, o): + if arm.utils.export_bone_data(bobject): + o['material_refs'].append('armdefaultskin') + self.default_skin_material_objects.append(bobject) + else: + o['material_refs'].append('armdefault') + self.default_material_objects.append(bobject) + + def use_default_material_part(self): + """Select the particle material variant for all particle system + instance objects that use the armdefault material. + """ + for ps in bpy.data.particles: + if ps.render_type != 'OBJECT' or ps.instance_object is None or not ps.instance_object.arm_export: + continue + + po = ps.instance_object + if po not in self.object_to_arm_object_dict: + self.process_bobject(po) + self.export_object(po) + o = self.object_to_arm_object_dict[po] + + # Check if the instance object uses the armdefault material + if len(o['material_refs']) > 0 and o['material_refs'][0] == 'armdefault' and po not in self.default_part_material_objects: + self.default_part_material_objects.append(po) + o['material_refs'] = ['armdefaultpart'] # Replace armdefault + + def export_material_ref(self, bobject: bpy.types.Object, material, index, o): + if material is None: # Use default for empty mat slots + self.use_default_material(bobject, o) + return + if material not in self.material_array: + self.material_array.append(material) + o['material_refs'].append(arm.utils.asset_name(material)) + + def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): + if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT' or not psys.settings.instance_object.arm_export: + return + + self.particle_system_array[psys.settings] = {"structName": psys.settings.name} + pref = { + 'name': psys.name, + 'seed': psys.seed, + 'particle': psys.settings.name + } + out_object['particle_refs'].append(pref) + + @staticmethod + def get_view3d_area() -> Optional[bpy.types.Area]: + screen = bpy.context.window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + return area + return None + + @staticmethod + def get_viewport_view_matrix() -> Optional[Matrix]: + play_area = ArmoryExporter.get_view3d_area() + if play_area is None: + return None + for space in play_area.spaces: + if space.type == 'VIEW_3D': + return space.region_3d.view_matrix + return None + + @staticmethod + def get_viewport_projection_matrix() -> Tuple[Optional[Matrix], bool]: + play_area = ArmoryExporter.get_view3d_area() + if play_area is None: + return None, False + for space in play_area.spaces: + if space.type == 'VIEW_3D': + # return space.region_3d.perspective_matrix # pesp = window * view + return space.region_3d.window_matrix, space.region_3d.is_perspective + return None, False + + def write_bone_matrices(self, scene, action): + # profile_time = time.time() + begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) + if len(self.bone_tracks) > 0: + for i in range(begin_frame, end_frame + 1): + scene.frame_set(i) + for track in self.bone_tracks: + values, pose_bone = track[0], track[1] + parent = pose_bone.parent + if parent: + values += ArmoryExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix)) + else: + values += ArmoryExporter.write_matrix(pose_bone.matrix) + # print('Bone matrices exported in ' + str(time.time() - profile_time)) + + @staticmethod + def has_baked_material(bobject, materials): + for mat in materials: + if mat is None: + continue + baked_mat = mat.name + '_' + bobject.name + '_baked' + if baked_mat in bpy.data.materials: + return True + return False + + @staticmethod + def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Material], List[bpy.types.MaterialSlot]]: + """Creates unique material variants for skinning, tilesheets and + particles.""" + matvars: List[bpy.types.Material] = [] + matslots: List[bpy.types.MaterialSlot] = [] + + bobject: bpy.types.Object + for bobject in scene.collection.all_objects.values(): + variant_suffix = '' + + # Skinning + if arm.utils.export_bone_data(bobject): + variant_suffix = '_armskin' + # Tilesheets + elif bobject.arm_tilesheet != '': + variant_suffix = '_armtile' + + if variant_suffix == '': + continue + + for slot in bobject.material_slots: + if slot.material is None or slot.material.library is not None: + continue + if slot.material.name.endswith(variant_suffix): + continue + + matslots.append(slot) + mat_name = slot.material.name + variant_suffix + mat = bpy.data.materials.get(mat_name) + # Create material variant + if mat is None: + mat = slot.material.copy() + mat.name = mat_name + if variant_suffix == '_armtile': + mat.arm_tilesheet_flag = True + matvars.append(mat) + slot.material = mat + + # Particle and non-particle objects can not share material + particle_sys: bpy.types.ParticleSettings + for particle_sys in bpy.data.particles: + bobject = particle_sys.instance_object + if bobject is None or particle_sys.render_type != 'OBJECT' or not bobject.arm_export: + continue + + for slot in bobject.material_slots: + if slot.material is None or slot.material.library is not None: + continue + if slot.material.name.endswith('_armpart'): + continue + + matslots.append(slot) + mat_name = slot.material.name + '_armpart' + mat = bpy.data.materials.get(mat_name) + if mat is None: + mat = slot.material.copy() + mat.name = mat_name + mat.arm_particle_flag = True + matvars.append(mat) + slot.material = mat + + return matvars, matslots + + @staticmethod + def slot_to_material(bobject: bpy.types.Object, slot: bpy.types.MaterialSlot): + mat = slot.material + # Pick up backed material if present + if mat is not None: + baked_mat = mat.name + '_' + bobject.name + '_baked' + if baked_mat in bpy.data.materials: + mat = bpy.data.materials[baked_mat] + return mat + + # def ExportMorphWeights(self, node, shapeKeys, scene): + # action = None + # curveArray = [] + # indexArray = [] + + # if (shapeKeys.animation_data): + # action = shapeKeys.animation_data.action + # if (action): + # for fcurve in action.fcurves: + # if ((fcurve.data_path.startswith("key_blocks[")) and (fcurve.data_path.endswith("].value"))): + # keyName = fcurve.data_path.strip("abcdehklopstuvy[]_.") + # if ((keyName[0] == "\"") or (keyName[0] == "'")): + # index = shapeKeys.key_blocks.find(keyName.strip("\"'")) + # if (index >= 0): + # curveArray.append(fcurve) + # indexArray.append(index) + # else: + # curveArray.append(fcurve) + # indexArray.append(int(keyName)) + + # if ((not action) and (node.animation_data)): + # action = node.animation_data.action + # if (action): + # for fcurve in action.fcurves: + # if ((fcurve.data_path.startswith("data.shape_keys.key_blocks[")) and (fcurve.data_path.endswith("].value"))): + # keyName = fcurve.data_path.strip("abcdehklopstuvy[]_.") + # if ((keyName[0] == "\"") or (keyName[0] == "'")): + # index = shapeKeys.key_blocks.find(keyName.strip("\"'")) + # if (index >= 0): + # curveArray.append(fcurve) + # indexArray.append(index) + # else: + # curveArray.append(fcurve) + # indexArray.append(int(keyName)) + + # animated = (len(curveArray) != 0) + # referenceName = shapeKeys.reference_key.name if (shapeKeys.use_relative) else "" + + # for k in range(len(shapeKeys.key_blocks)): + # self.IndentWrite(B"MorphWeight", 0, (k == 0)) + + # if (animated): + # self.Write(B" %mw") + # self.WriteInt(k) + + # self.Write(B" (index = ") + # self.WriteInt(k) + # self.Write(B") {float {") + + # block = shapeKeys.key_blocks[k] + # self.WriteFloat(block.value if (block.name != referenceName) else 1.0) + + # self.Write(B"}}\n") + + # if (animated): + # self.IndentWrite(B"Animation (begin = ", 0, True) + # self.WriteFloat((action.frame_range[0]) * self.frameTime) + # self.Write(B", end = ") + # self.WriteFloat((action.frame_range[1]) * self.frameTime) + # self.Write(B")\n") + # self.IndentWrite(B"{\n") + # self.indentLevel += 1 + + # structFlag = False + + # for a in range(len(curveArray)): + # k = indexArray[a] + # target = bytes("mw" + str(k), "UTF-8") + + # fcurve = curveArray[a] + # kind = OpenGexExporter.ClassifyAnimationCurve(fcurve) + # if ((kind != kAnimationSampled) and (not self.sampleAnimationFlag)): + # self.ExportAnimationTrack(fcurve, kind, target, structFlag) + # else: + # self.ExportMorphWeightSampledAnimationTrack(shapeKeys.key_blocks[k], target, scene, structFlag) + + # structFlag = True + + # self.indentLevel -= 1 + # self.IndentWrite(B"}\n") + + def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> None: + """This function exports a single object in the scene and + includes its name, object reference, material references (for + meshes), and transform. + Subobjects are then exported recursively. + """ + if not bobject.arm_export: + return + + bobject_ref = self.bobject_array.get(bobject) + if bobject_ref is not None: + object_type = bobject_ref["objectType"] + + # Linked object, not present in scene + if bobject not in self.object_to_arm_object_dict: + out_object = { + 'traits': [], + 'spawn': False + } + self.object_to_arm_object_dict[bobject] = out_object + + out_object = self.object_to_arm_object_dict[bobject] + out_object['type'] = STRUCT_IDENTIFIER[object_type.value] + out_object['name'] = bobject_ref["structName"] + + if bobject.parent_type == "BONE": + out_object['parent_bone'] = bobject.parent_bone + + if bobject.hide_render or not bobject.arm_visible: + out_object['visible'] = False + + if not bobject.visible_camera: + out_object['visible_mesh'] = False + if not bobject.visible_shadow: + out_object['visible_shadow'] = False + + if not bobject.arm_spawn: + out_object['spawn'] = False + + out_object['mobile'] = bobject.arm_mobile + + if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: + out_object['group_ref'] = bobject.instance_collection.name + + if bobject.arm_tilesheet != '': + out_object['tilesheet_ref'] = bobject.arm_tilesheet + out_object['tilesheet_action_ref'] = bobject.arm_tilesheet_action + + if len(bobject.arm_propertylist) > 0: + out_object['properties'] = [] + for proplist_item in bobject.arm_propertylist: + out_property = { + 'name': proplist_item.name_prop, + 'value': getattr(proplist_item, proplist_item.type_prop + '_prop')} + out_object['properties'].append(out_property) + + # Export the object reference and material references + objref = bobject.data + if objref is not None: + objname = arm.utils.asset_name(objref) + + # LOD + if bobject.type == 'MESH' and hasattr(objref, 'arm_lodlist') and len(objref.arm_lodlist) > 0: + out_object['lods'] = [] + for lodlist_item in objref.arm_lodlist: + if not lodlist_item.enabled_prop: + continue + out_lod = { + 'object_ref': lodlist_item.name, + 'screen_size': lodlist_item.screen_size_prop + } + out_object['lods'].append(out_lod) + if objref.arm_lod_material: + out_object['lod_material'] = True + + if object_type is NodeType.MESH: + if objref not in self.mesh_array: + self.mesh_array[objref] = {"structName": objname, "objectTable": [bobject]} + else: + self.mesh_array[objref]["objectTable"].append(bobject) + + oid = arm.utils.safestr(self.mesh_array[objref]["structName"]) + + wrd = bpy.data.worlds['Arm'] + if wrd.arm_single_data_file: + out_object['data_ref'] = oid + else: + ext = '' if not ArmoryExporter.compress_enabled else '.lz4' + if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: + ext = '.json' + out_object['data_ref'] = 'mesh_' + oid + ext + '/' + oid + + out_object['material_refs'] = [] + for i in range(len(bobject.material_slots)): + mat = self.slot_to_material(bobject, bobject.material_slots[i]) + # Export ref + self.export_material_ref(bobject, mat, i, out_object) + # Decal flag + if mat is not None and mat.arm_decal: + out_object['type'] = 'decal_object' + # No material, mimic cycles and assign default + if len(out_object['material_refs']) == 0: + self.use_default_material(bobject, out_object) + + num_psys = len(bobject.particle_systems) + if num_psys > 0: + out_object['particle_refs'] = [] + out_object['render_emitter'] = bobject.show_instancer_for_render + for i in range(num_psys): + self.export_particle_system_ref(bobject.particle_systems[i], out_object) + + aabb = bobject.data.arm_aabb + if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: + self.calc_aabb(bobject) + out_object['dimensions'] = [aabb[0], aabb[1], aabb[2]] + + # shapeKeys = ArmoryExporter.get_shape_keys(objref) + # if shapeKeys: + # self.ExportMorphWeights(bobject, shapeKeys, scene, out_object) + + elif object_type is NodeType.LIGHT: + if objref not in self.light_array: + self.light_array[objref] = {"structName" : objname, "objectTable" : [bobject]} + else: + self.light_array[objref]["objectTable"].append(bobject) + out_object['data_ref'] = self.light_array[objref]["structName"] + + elif object_type is NodeType.PROBE: + if objref not in self.probe_array: + self.probe_array[objref] = {"structName" : objname, "objectTable" : [bobject]} + else: + self.probe_array[objref]["objectTable"].append(bobject) + + dist = bobject.data.influence_distance + + if objref.type == "PLANAR": + out_object['dimensions'] = [1.0, 1.0, dist] + + # GRID, CUBEMAP + else: + out_object['dimensions'] = [dist, dist, dist] + out_object['data_ref'] = self.probe_array[objref]["structName"] + + elif object_type is NodeType.CAMERA: + if out_object.get('spawn', True): # Also spawn object if 'spawn' attr doesn't exist + self.has_spawning_camera = True + + if objref not in self.camera_array: + self.camera_array[objref] = {"structName" : objname, "objectTable" : [bobject]} + else: + self.camera_array[objref]["objectTable"].append(bobject) + out_object['data_ref'] = self.camera_array[objref]["structName"] + + elif object_type is NodeType.SPEAKER: + if objref not in self.speaker_array: + self.speaker_array[objref] = {"structName" : objname, "objectTable" : [bobject]} + else: + self.speaker_array[objref]["objectTable"].append(bobject) + out_object['data_ref'] = self.speaker_array[objref]["structName"] + + # Export the transform. If object is animated, then animation tracks are exported here + if bobject.type != 'ARMATURE' and bobject.animation_data is not None: + action = bobject.animation_data.action + export_actions = [action] + for track in bobject.animation_data.nla_tracks: + if track.strips is None: + continue + for strip in track.strips: + if strip.action is None or strip.action in export_actions: + continue + export_actions.append(strip.action) + orig_action = action + for a in export_actions: + bobject.animation_data.action = a + self.export_object_transform(bobject, out_object) + if len(export_actions) >= 2 and export_actions[0] is None: # No action assigned + out_object['object_actions'].insert(0, 'null') + bobject.animation_data.action = orig_action + else: + self.export_object_transform(bobject, out_object) + + # If the object is parented to a bone and is not relative, then undo the bone's transform + if bobject.parent_type == "BONE": + armature = bobject.parent.data + bone = armature.bones[bobject.parent_bone] + # if not bone.use_relative_parent: + out_object['parent_bone_connected'] = bone.use_connect + if bone.use_connect: + bone_translation = Vector((0, bone.length, 0)) + bone.head + out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + else: + bone_translation = bone.tail - bone.head + out_object['parent_bone_tail'] = [bone_translation[0], bone_translation[1], bone_translation[2]] + pose_bone = bobject.parent.pose.bones[bobject.parent_bone] + bone_translation_pose = pose_bone.tail - pose_bone.head + out_object['parent_bone_tail_pose'] = [bone_translation_pose[0], bone_translation_pose[1], bone_translation_pose[2]] + + if bobject.type == 'ARMATURE' and bobject.data is not None: + # Armature data + bdata = bobject.data + # Reference start action + action = None + adata = bobject.animation_data + + # Active action + if adata is not None: + action = adata.action + if action is None: + log.warn('Object ' + bobject.name + ' - No action assigned, setting to pose') + bobject.animation_data_create() + actions = bpy.data.actions + action = actions.get('armorypose') + if action is None: + action = actions.new(name='armorypose') + + # Export actions + export_actions = [action] + # hasattr - armature modifier may reference non-parent + # armature object to deform with + if hasattr(adata, 'nla_tracks') and adata.nla_tracks is not None: + for track in adata.nla_tracks: + if track.strips is None: + continue + for strip in track.strips: + if strip.action is None: + continue + if strip.action.name == action.name: + continue + export_actions.append(strip.action) + + armatureid = arm.utils.safestr(arm.utils.asset_name(bdata)) + ext = '.lz4' if ArmoryExporter.compress_enabled else '' + if ext == '' and not bpy.data.worlds['Arm'].arm_minimize: + ext = '.json' + out_object['bone_actions'] = [] + for action in export_actions: + aname = arm.utils.safestr(arm.utils.asset_name(action)) + out_object['bone_actions'].append('action_' + armatureid + '_' + aname + ext) + + clear_op = set() + skelobj = bobject + baked_actions = [] + orig_action = bobject.animation_data.action + if bdata.arm_autobake and bobject.name not in bpy.context.collection.all_objects: + clear_op.add('unlink') + # Clone bobject and put it in the current scene so + # the bake operator can run + if bobject.library is not None: + skelobj = bobject.copy() + clear_op.add('rem') + bpy.context.collection.objects.link(skelobj) + + for action in export_actions: + aname = arm.utils.safestr(arm.utils.asset_name(action)) + skelobj.animation_data.action = action + fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=ArmoryExporter.compress_enabled) + assets.add(fp) + if not bdata.arm_cached or not os.path.exists(fp): + # Store action to use it after autobake was handled + original_action = action + + # Handle autobake + if bdata.arm_autobake: + sel = bpy.context.selected_objects[:] + for _o in sel: + _o.select_set(False) + skelobj.select_set(True) + + bake_result = bpy.ops.nla.bake( + frame_start=int(action.frame_range[0]), + frame_end=int(action.frame_range[1]), + step=1, + only_selected=False, + visual_keying=True + ) + action = skelobj.animation_data.action + + skelobj.select_set(False) + for _o in sel: + _o.select_set(True) + + # Baking creates a new action, but only if it + # was successful + if 'FINISHED' in bake_result: + baked_actions.append(action) + + wrd = bpy.data.worlds['Arm'] + if wrd.arm_verbose_output: + print('Exporting armature action ' + aname) + bones = [] + self.bone_tracks = [] + for bone in bdata.bones: + if not bone.parent: + boneo = {} + self.export_bone(skelobj, bone, boneo, action) + bones.append(boneo) + self.write_bone_matrices(bpy.context.scene, action) + if len(bones) > 0 and 'anim' in bones[0]: + self.export_pose_markers(bones[0]['anim'], original_action) + # Save action separately + action_obj = {'name': aname, 'objects': bones} + arm.utils.write_arm(fp, action_obj) + + # Use relative bone constraints + out_object['relative_bone_constraints'] = bdata.arm_relative_bone_constraints + + # Restore settings + skelobj.animation_data.action = orig_action + for a in baked_actions: + bpy.data.actions.remove(a, do_unlink=True) + if 'unlink' in clear_op: + bpy.context.collection.objects.unlink(skelobj) + if 'rem' in clear_op: + bpy.data.objects.remove(skelobj, do_unlink=True) + + # TODO: cache per action + bdata.arm_cached = True + + if out_parent is None: + self.output['objects'].append(out_object) + else: + out_parent['children'].append(out_object) + + self.post_export_object(bobject, out_object, object_type) + + if not hasattr(out_object, 'children') and len(bobject.children) > 0: + out_object['children'] = [] + + if bobject.arm_instanced == 'Off': + for subbobject in bobject.children: + self.export_object(subbobject, out_object) + + def export_skin(self, bobject: bpy.types.Object, armature, export_mesh: bpy.types.Mesh, out_mesh): + """This function exports all skinning data, which includes the + skeleton and per-vertex bone influence data""" + oskin = {} + out_mesh['skin'] = oskin + + # Write the skin bind pose transform + otrans = {'values': ArmoryExporter.write_matrix(bobject.matrix_world)} + oskin['transform'] = otrans + + bone_array = armature.data.bones + bone_count = len(bone_array) + rpdat = arm.utils.get_rp() + max_bones = rpdat.arm_skin_max_bones + bone_count = min(bone_count, max_bones) + + # Write the bone object reference array + oskin['bone_ref_array'] = np.empty(bone_count, dtype=object) + oskin['bone_len_array'] = np.empty(bone_count, dtype='= 0: #and bone_weight != 0.0: + bone_values.append((bone_weight, bone_index)) + total_weight += bone_weight + bone_count += 1 + + if bone_count > 4: + bone_count = 4 + bone_values.sort(reverse=True) + bone_values = bone_values[:4] + + bone_count_array[index] = bone_count + for bv in bone_values: + bone_weight_array[count] = bv[0] + bone_index_array[count] = bv[1] + count += 1 + + if total_weight not in (0.0, 1.0): + normalizer = 1.0 / total_weight + for i in range(bone_count): + bone_weight_array[count - i - 1] *= normalizer + + bone_index_array = bone_index_array[:count] + bone_weight_array = bone_weight_array[:count] + bone_weight_array *= 32767 + bone_weight_array = np.array(bone_weight_array, dtype=' 0: + if 'constraints' not in oskin: + oskin['constraints'] = [] + self.add_constraints(bone, oskin, bone=True) + + def export_shape_keys(self, bobject: bpy.types.Object, export_mesh: bpy.types.Mesh, out_mesh): + + # Max shape keys supported + max_shape_keys = 32 + # Path to store shape key textures + output_dir = bpy.path.abspath('//') + "MorphTargets" + name = bobject.data.name + vert_pos = [] + vert_nor = [] + names = [] + default_values = [0] * max_shape_keys + # Shape key base mesh + shape_key_base = bobject.data.shape_keys.key_blocks[0] + + count = 0 + # Loop through all shape keys + for shape_key in bobject.data.shape_keys.key_blocks[1:]: + + if count > max_shape_keys - 1: + break + # get vertex data from shape key + if shape_key.mute: + continue + vert_data = self.get_vertex_data_from_shape_key(shape_key_base, shape_key) + vert_pos.append(vert_data['pos']) + vert_nor.append(vert_data['nor']) + names.append(shape_key.name) + default_values[count] = shape_key.value + + count += 1 + + # No shape keys present or all shape keys are muted + if count < 1: + return + + # Convert to array for easy manipulation + pos_array = np.array(vert_pos) + nor_array = np.array(vert_nor) + + # Min and Max values of shape key displacements + max = np.amax(pos_array) + min = np.amin(pos_array) + + array_size = len(pos_array[0]), len(pos_array) + + # Get best 2^n image size to fit shape key data (min = 2 X 2, max = 4096 X 4096) + img_size, extra_zeros, block_size = self.get_best_image_size(array_size) + + # Image size required is too large. Skip export + if img_size < 1: + log.error(f"""object {bobject.name} contains too many vertices or shape keys to support shape keys export""") + self.remove_morph_uv_set(bobject) + return + + # Write data to image + self.bake_to_image(pos_array, nor_array, max, min, extra_zeros, img_size, name, output_dir) + + # Create a new UV set for shape keys + self.create_morph_uv_set(bobject, img_size) + + # Export Shape Key names, defaults, etc.. + morph_target = {} + morph_target['morph_target_data_file'] = name + morph_target['morph_target_ref'] = names + morph_target['morph_target_defaults'] = default_values + morph_target['num_morph_targets'] = count + morph_target['morph_scale'] = max - min + morph_target['morph_offset'] = min + morph_target['morph_img_size'] = img_size + morph_target['morph_block_size'] = block_size + + out_mesh['morph_target'] = morph_target + return + + def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data): + + base_vert_pos = shape_key_base.data.values() + base_vert_nor = shape_key_base.normals_split_get() + vert_pos = shape_key_data.data.values() + vert_nor = shape_key_data.normals_split_get() + + num_verts = len(vert_pos) + + pos = [] + nor = [] + + # Loop through all vertices + for i in range(num_verts): + # Vertex position relative to base vertex + pos.append(list(vert_pos[i].co - base_vert_pos[i].co)) + temp = [] + for j in range(3): + # Vertex normal relative to base vertex + temp.append(vert_nor[j + i * 3] - base_vert_nor[j + i * 3]) + nor.append(temp) + + return {'pos': pos, 'nor': nor} + + def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir): + # Scale position data between [0, 1] to bake to image + pos_array_scaled = np.interp(pos_array, (pos_min, pos_max), (0, 1)) + # Write positions to image + self.write_output_image(pos_array_scaled, extra_x, img_size, name + '_morph_pos', output_dir) + # Scale normal data between [0, 1] to bake to image + nor_array_scaled = np.interp(nor_array, (-1, 1), (0, 1)) + # Write normals to image + self.write_output_image(nor_array_scaled, extra_x, img_size, name + '_morph_nor', output_dir) + + def write_output_image(self, data, extra_x, img_size, name, output_dir): + + # Pad data with zeros to make up for required number of pixels of 2^n format + data = np.pad(data, ((0, 0), (0, extra_x), (0, 0)), 'minimum') + pixel_list = [] + + for y in range(len(data)): + for x in range(len(data[0])): + # assign RGBA + pixel_list.append(data[y, x, 0]) + pixel_list.append(data[y, x, 1]) + pixel_list.append(data[y, x, 2]) + pixel_list.append(1.0) + + pixel_list = (pixel_list + [0] * (img_size * img_size * 4 - len(pixel_list))) + + image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True) + image.pixels = pixel_list + output_path = os.path.join(output_dir, name + ".png") + image.save_render(output_path, scene= bpy.context.scene) + bpy.data.images.remove(image) + + def get_best_image_size(self, size): + + for i in range(1, 12): + block_len = pow(2, i) + block_height = np.ceil(size[0]/block_len) + if block_height * size[1] <= block_len: + extra_zeros_x = block_height * block_len - size[0] + return pow(2,i), round(extra_zeros_x), block_height + + return 0, 0, 0 + + def remove_morph_uv_set(self, obj): + layer = obj.data.uv_layers.get('UVMap_shape_key') + if layer is not None: + obj.data.uv_layers.remove(layer) + + def create_morph_uv_set(self, obj, img_size): + # Get/ create morph UV set + if obj.data.uv_layers.get('UVMap_shape_key') is None: + obj.data.uv_layers.new(name = 'UVMap_shape_key') + + bm = bmesh.new() + bm.from_mesh(obj.data) + uv_layer = bm.loops.layers.uv.get('UVMap_shape_key') + + pixel_size = 1.0 / img_size + + i = 0 + j = 0 + # Arrange UVs to match exported image pixels + for v in bm.verts: + for l in v.link_loops: + uv_data = l[uv_layer] + uv_data.uv = Vector(((i + 0.5) * pixel_size, (j + 0.5) * pixel_size)) + i += 1 + if i > img_size - 1: + j += 1 + i = 0 + + bm.to_mesh(obj.data) + bm.free() + + def write_mesh(self, bobject: bpy.types.Object, fp, out_mesh): + if bpy.data.worlds['Arm'].arm_single_data_file: + self.output['mesh_datas'].append(out_mesh) + + # One mesh data per file + else: + mesh_obj = {'mesh_datas': [out_mesh]} + arm.utils.write_arm(fp, mesh_obj) + bobject.data.arm_cached = True + + @staticmethod + def calc_aabb(bobject): + aabb_center = 0.125 * sum((Vector(b) for b in bobject.bound_box), Vector()) + bobject.data.arm_aabb = [ + abs((bobject.bound_box[6][0] - bobject.bound_box[0][0]) / 2 + abs(aabb_center[0])) * 2, + abs((bobject.bound_box[6][1] - bobject.bound_box[0][1]) / 2 + abs(aabb_center[1])) * 2, + abs((bobject.bound_box[6][2] - bobject.bound_box[0][2]) / 2 + abs(aabb_center[2])) * 2 + ] + + @staticmethod + def get_num_vertex_colors(mesh: bpy.types.Mesh) -> int: + """Return the amount of vertex color attributes of the given mesh.""" + num = 0 + for attr in mesh.attributes: + if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): + if attr.domain == 'CORNER': + num += 1 + else: + log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') + + return num + + @staticmethod + def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[bpy.types.Attribute]: + """Return the n-th vertex color attribute from the given mesh, + ignoring all other attribute types and unsupported domains. + """ + i = 0 + for attr in mesh.attributes: + if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): + if attr.domain != 'CORNER': + log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') + continue + if i == n: + return attr + i += 1 + return None + + @staticmethod + def check_uv_precision(mesh: bpy.types.Mesh, uv_max_dim: float, max_dim_uvmap: bpy.types.MeshUVLoopLayer, invscale_tex: float): + """Check whether the pixel size (assuming max_texture_size below) + can be represented inside the usual [0, 1] UV range with the + given `invscale_tex` that is used to normalize the UV coords. + If it is not possible, display a warning. + """ + max_texture_size = 16384 + pixel_size = 1 / max_texture_size + + # There are fewer distinct floating point values around 1 than + # around 0, so we use 1 for checking here. We do not check whether + # UV coords with an absolute value > 1 can be reliably represented. + if np.float32(1.0) == np.float32(1.0) - np.float32(pixel_size * invscale_tex): + log.warn( + f'Mesh "{mesh.name}": The UV map "{max_dim_uvmap.name}"' + ' contains very large coordinates (max. distance from' + f' origin: {uv_max_dim}). The UV precision may suffer.' + ) + + def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Object, o, has_armature=False): + export_mesh.calc_normals_split() + export_mesh.calc_loop_triangles() + + loops = export_mesh.loops + num_verts = len(loops) + num_uv_layers = len(export_mesh.uv_layers) + is_baked = self.has_baked_material(bobject, export_mesh.materials) + num_colors = self.get_num_vertex_colors(export_mesh) + has_col = self.get_export_vcols(bobject.data) and num_colors > 0 + # Check if shape keys were exported + has_morph_target = self.get_shape_keys(bobject.data) + if has_morph_target: + # Shape keys UV are exported separately, so reduce UV count by 1 + num_uv_layers -= 1 + morph_uv_index = self.get_morph_uv_index(bobject.data) + has_tex = (self.get_export_uvs(bobject.data) and num_uv_layers > 0) or is_baked + has_tex1 = has_tex and num_uv_layers > 1 + has_tang = self.has_tangents(bobject.data) + + pdata = np.empty(num_verts * 4, dtype=' maxdim: + maxdim = abs(v.uv[0]) + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + if has_tex1: + lay1 = uv_layers[t1map] + for v in lay1.data: + if abs(v.uv[0]) > maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay1 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay1 + if has_morph_target: + morph_data = np.empty(num_verts * 2, dtype=' maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay2 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay2 + if maxdim > 1: + o['scale_tex'] = maxdim + invscale_tex = (1 / o['scale_tex']) * 32767 + else: + invscale_tex = 1 * 32767 + self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) + if has_tang: + try: + export_mesh.calc_tangents(uvmap=lay0.name) + except Exception as e: + if hasattr(e, 'message'): + log.error(e.message) + else: + # Assume it was caused because of encountering n-gons + log.error(f"""object {bobject.name} contains n-gons in its mesh, so it's impossible to compute tanget space for normal mapping. +Make sure the mesh only has tris/quads.""") + + tangdata = np.empty(num_verts * 3, dtype=' 2: + o['scale_pos'] = maxdim / 2 + else: + o['scale_pos'] = 1.0 + if has_armature: # Allow up to 2x bigger bounds for skinned mesh + o['scale_pos'] *= 2.0 + + scale_pos = o['scale_pos'] + invscale_pos = (1 / scale_pos) * 32767 + + verts = export_mesh.vertices + if has_tex: + lay0 = export_mesh.uv_layers[t0map] + if has_tex1: + lay1 = export_mesh.uv_layers[t1map] + if has_morph_target: + lay2 = export_mesh.uv_layers[morph_uv_index] + if has_col: + vcol0 = self.get_nth_vertex_colors(export_mesh, 0).data + + loop: bpy.types.MeshLoop + for i, loop in enumerate(loops): + v = verts[loop.vertex_index] + co = v.co + normal = loop.normal + tang = loop.tangent + + i4 = i * 4 + i2 = i * 2 + pdata[i4 ] = co[0] + pdata[i4 + 1] = co[1] + pdata[i4 + 2] = co[2] + pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale + ndata[i2 ] = normal[0] + ndata[i2 + 1] = normal[1] + if has_tex: + uv = lay0.data[loop.index].uv + t0data[i2 ] = uv[0] + t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y + if has_tex1: + uv = lay1.data[loop.index].uv + t1data[i2 ] = uv[0] + t1data[i2 + 1] = 1.0 - uv[1] + if has_tang: + i3 = i * 3 + tangdata[i3 ] = tang[0] + tangdata[i3 + 1] = tang[1] + tangdata[i3 + 2] = tang[2] + if has_morph_target: + uv = lay2.data[loop.index].uv + morph_data[i2 ] = uv[0] + morph_data[i2 + 1] = 1.0 - uv[1] + if has_col: + col = vcol0[loop.index].color + i3 = i * 3 + cdata[i3 ] = col[0] + cdata[i3 + 1] = col[1] + cdata[i3 + 2] = col[2] + + mats = export_mesh.materials + poly_map = [] + for i in range(max(len(mats), 1)): + poly_map.append([]) + for poly in export_mesh.polygons: + poly_map[poly.material_index].append(poly) + + o['index_arrays'] = [] + + # map polygon indices to triangle loops + tri_loops = {} + for loop in export_mesh.loop_triangles: + if loop.polygon_index not in tri_loops: + tri_loops[loop.polygon_index] = [] + tri_loops[loop.polygon_index].append(loop) + + for index, polys in enumerate(poly_map): + tris = 0 + for poly in polys: + tris += poly.loop_total - 2 + if tris == 0: # No face assigned + continue + prim = np.empty(tris * 3, dtype=' 1: + for i in range(len(mats)): # Multi-mat mesh + if mats[i] == mats[index]: # Default material for empty slots + ia['material'] = i + break + o['index_arrays'].append(ia) + + # Pack + pdata *= invscale_pos + ndata *= 32767 + pdata = np.array(pdata, dtype=' 0 + + def export_mesh(self, object_ref): + """Exports a single mesh object.""" + # profile_time = time.time() + table = object_ref[1]["objectTable"] + bobject = table[0] + oid = arm.utils.safestr(object_ref[1]["structName"]) + + wrd = bpy.data.worlds['Arm'] + if wrd.arm_single_data_file: + fp = None + else: + fp = self.get_meshes_file_path('mesh_' + oid, compressed=ArmoryExporter.compress_enabled) + assets.add(fp) + # No export necessary + if bobject.data.arm_cached and os.path.exists(fp): + return + + # Mesh users have different modifier stack + for i in range(1, len(table)): + if not self.mod_equal_stack(bobject, table[i]): + log.warn('{0} users {1} and {2} differ in modifier stack - use Make Single User - Object & Data for now'.format(oid, bobject.name, table[i].name)) + break + + if wrd.arm_verbose_output: + print('Exporting mesh ' + arm.utils.asset_name(bobject.data)) + + out_mesh = {'name': oid} + mesh = object_ref[0] + struct_flag = False + + # Save the morph state if necessary + active_shape_key_index = 0 + show_only_shape_key = False + current_morph_value = 0 + + shape_keys = ArmoryExporter.get_shape_keys(mesh) + if shape_keys: + # Save the morph state + active_shape_key_index = bobject.active_shape_key_index + show_only_shape_key = bobject.show_only_shape_key + current_morph_value = bobject.active_shape_key.value + # Reset morph state to base for mesh export + bobject.active_shape_key_index = 0 + bobject.show_only_shape_key = True + self.depsgraph.update() + + armature = bobject.find_armature() + apply_modifiers = not armature + + bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject + export_mesh = bobject_eval.to_mesh() + + # Export shape keys here + if shape_keys: + self.export_shape_keys(bobject, export_mesh, out_mesh) + # Update dependancy after new UV layer was added + self.depsgraph.update() + bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject + export_mesh = bobject_eval.to_mesh() + + if export_mesh is None: + log.warn(oid + ' was not exported') + return + + if len(export_mesh.uv_layers) > 2: + log.warn(oid + ' exceeds maximum of 2 UV Maps supported') + + # Update aabb + self.calc_aabb(bobject) + + # Process meshes + if ArmoryExporter.optimize_enabled: + vert_list = exporter_opt.export_mesh_data(self, export_mesh, bobject, out_mesh, has_armature=armature is not None) + if armature: + exporter_opt.export_skin(self, bobject, armature, vert_list, out_mesh) + else: + self.export_mesh_data(export_mesh, bobject, out_mesh, has_armature=armature is not None) + if armature: + self.export_skin(bobject, armature, export_mesh, out_mesh) + + # Restore the morph state after mesh export + if shape_keys: + bobject.active_shape_key_index = active_shape_key_index + bobject.show_only_shape_key = show_only_shape_key + bobject.active_shape_key.value = current_morph_value + self.depsgraph.update() + + mesh.update() + + # Check if mesh is using instanced rendering + instanced_type, instanced_data = self.object_process_instancing(table, out_mesh['scale_pos']) + + # Save offset data for instanced rendering + if instanced_type > 0: + out_mesh['instanced_data'] = instanced_data + out_mesh['instanced_type'] = instanced_type + + # Export usage + if bobject.data.arm_dynamic_usage: + out_mesh['dynamic_usage'] = bobject.data.arm_dynamic_usage + + self.write_mesh(bobject, fp, out_mesh) + # print('Mesh exported in ' + str(time.time() - profile_time)) + + if hasattr(bobject, 'evaluated_get'): + bobject_eval.to_mesh_clear() + + def export_light(self, object_ref): + """Exports a single light object.""" + rpdat = arm.utils.get_rp() + light_ref = object_ref[0] + objtype = light_ref.type + out_light = { + 'name': object_ref[1]["structName"], + 'type': objtype.lower(), + 'cast_shadow': light_ref.use_shadow, + 'near_plane': light_ref.arm_clip_start, + 'far_plane': light_ref.arm_clip_end, + 'fov': light_ref.arm_fov, + 'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]], + 'strength': light_ref.energy, + 'shadows_bias': light_ref.arm_shadows_bias * 0.0001 + } + if rpdat.rp_shadows: + if objtype == 'POINT': + out_light['shadowmap_size'] = int(rpdat.rp_shadowmap_cube) + else: + out_light['shadowmap_size'] = arm.utils.get_cascade_size(rpdat) + else: + out_light['shadowmap_size'] = 0 + + if objtype == 'SUN': + out_light['strength'] *= 0.325 + # Scale bias for ortho light matrix + out_light['shadows_bias'] *= 20.0 + if out_light['shadowmap_size'] > 1024: + # Less bias for bigger maps + out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024) + elif objtype == 'POINT': + out_light['strength'] *= 0.01 + out_light['fov'] = 1.5708 # pi/2 + out_light['shadowmap_cube'] = True + if light_ref.shadow_soft_size > 0.1: + out_light['light_size'] = light_ref.shadow_soft_size * 10 + elif objtype == 'SPOT': + out_light['strength'] *= 0.01 + out_light['spot_size'] = math.cos(light_ref.spot_size / 2) + # Cycles defaults to 0.15 + out_light['spot_blend'] = light_ref.spot_blend / 10 + elif objtype == 'AREA': + out_light['strength'] *= 0.01 + out_light['size'] = light_ref.size + out_light['size_y'] = light_ref.size_y + + self.output['light_datas'].append(out_light) + + def export_probe(self, objectRef): + o = {'name': objectRef[1]["structName"]} + bo = objectRef[0] + + if bo.type == 'GRID': + o['type'] = 'grid' + elif bo.type == 'PLANAR': + o['type'] = 'planar' + else: + o['type'] = 'cubemap' + + self.output['probe_datas'].append(o) + + def export_collection(self, collection: bpy.types.Collection): + """Exports a single collection.""" + scene_objects = self.scene.collection.all_objects + + out_collection = { + 'name': collection.name, + 'instance_offset': list(collection.instance_offset), + 'object_refs': [] + } + + for bobject in collection.objects: + if not bobject.arm_export: + continue + + # Only add unparented objects or objects with their parent + # outside the collection, then instantiate the full object + # child tree if the collection gets spawned as a whole + if bobject.parent is None or bobject.parent.name not in collection.objects: + asset_name = arm.utils.asset_name(bobject) + + if collection.library: + # Add external linked objects + # Iron differentiates objects based on their names, + # so errors will happen if two objects with the + # same name exists. This check is only required + # when the object in question is in a library, + # otherwise Blender will not allow duplicate names + if asset_name in scene_objects: + log.warn("skipping export of the object" + f" {bobject.name} (collection" + f" {collection.name}) because it has the same" + " export name as another object in the scene:" + f" {asset_name}") + continue + + self.process_bobject(bobject) + self.export_object(bobject) + + out_collection['object_refs'].append(asset_name) + + self.output['groups'].append(out_collection) + + + def get_camera_clear_color(self): + if self.scene.world is None: + return [0.051, 0.051, 0.051, 1.0] + + if self.scene.world.node_tree is None: + c = self.scene.world.color + return [c[0], c[1], c[2], 1.0] + + if 'Background' in self.scene.world.node_tree.nodes: + background_node = self.scene.world.node_tree.nodes['Background'] + col = background_node.inputs[0].default_value + strength = background_node.inputs[1].default_value + ar = [col[0] * strength, col[1] * strength, col[2] * strength, col[3]] + ar[0] = max(min(ar[0], 1.0), 0.0) + ar[1] = max(min(ar[1], 1.0), 0.0) + ar[2] = max(min(ar[2], 1.0), 0.0) + ar[3] = max(min(ar[3], 1.0), 0.0) + return ar + return [0.051, 0.051, 0.051, 1.0] + + @staticmethod + def extract_projection(o, proj, with_planes=True): + a = proj[0][0] + b = proj[1][1] + c = proj[2][2] + d = proj[2][3] + k = (c - 1.0) / (c + 1.0) + o['fov'] = 2.0 * math.atan(1.0 / b) + if with_planes: + o['near_plane'] = (d * (1.0 - k)) / (2.0 * k) + o['far_plane'] = k * o['near_plane'] + + @staticmethod + def extract_ortho(o, proj): + # left, right, bottom, top + o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \ + (1 - proj[3][0]) / proj[0][0], \ + -(1 + proj[3][1]) / proj[1][1], \ + (1 - proj[3][1]) / proj[1][1]] + o['near_plane'] = (1 + proj[3][2]) / proj[2][2] + o['far_plane'] = -(1 - proj[3][2]) / proj[2][2] + o['near_plane'] *= 2 + o['far_plane'] *= 2 + + def export_camera(self, objectRef): + o = {} + o['name'] = objectRef[1]["structName"] + objref = objectRef[0] + + camera = objectRef[1]["objectTable"][0] + render = self.scene.render + proj = camera.calc_matrix_camera( + self.depsgraph, + x=render.resolution_x, + y=render.resolution_y, + scale_x=render.pixel_aspect_x, + scale_y=render.pixel_aspect_y) + if objref.type == 'PERSP': + self.extract_projection(o, proj) + else: + self.extract_ortho(o, proj) + + o['frustum_culling'] = objref.arm_frustum_culling + o['clear_color'] = self.get_camera_clear_color() + + self.output['camera_datas'].append(o) + + def export_speaker(self, objectRef): + # This function exports a single speaker object + o = {} + o['name'] = objectRef[1]["structName"] + objref = objectRef[0] + if objref.sound: + # Packed + if objref.sound.packed_file is not None: + unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + unpack_filepath = unpack_path + '/' + objref.sound.name + if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != objref.sound.packed_file.size: + with open(unpack_filepath, 'wb') as f: + f.write(objref.sound.packed_file.data) + assets.add(unpack_filepath) + # External + else: + assets.add(arm.utils.asset_path(objref.sound.filepath)) # Link sound to assets + o['sound'] = arm.utils.extract_filename(objref.sound.filepath) + else: + o['sound'] = '' + o['muted'] = objref.muted + o['volume'] = objref.volume + o['pitch'] = objref.pitch + o['volume_min'] = objref.volume_min + o['volume_max'] = objref.volume_max + o['attenuation'] = objref.attenuation + o['distance_max'] = objref.distance_max + o['distance_reference'] = objref.distance_reference + o['play_on_start'] = objref.arm_play_on_start + o['loop'] = objref.arm_loop + o['stream'] = objref.arm_stream + self.output['speaker_datas'].append(o) + + def make_default_mat(self, mat_name, mat_objs, is_particle=False): + if mat_name in bpy.data.materials: + return + mat = bpy.data.materials.new(name=mat_name) + # if default_exists: + # mat.arm_cached = True + if is_particle: + mat.arm_particle_flag = True + # Empty material roughness + mat.use_nodes = True + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + node.inputs[7].default_value = 0.25 + o = {} + o['name'] = mat.name + o['contexts'] = [] + mat_users = { mat: mat_objs } + mat_armusers = { mat: [o] } + make_material.parse(mat, o, mat_users, mat_armusers) + self.output['material_datas'].append(o) + bpy.data.materials.remove(mat) + rpdat = arm.utils.get_rp() + if not rpdat.arm_culling: + o['override_context'] = {} + o['override_context']['cull_mode'] = 'none' + + def signature_traverse(self, node, sign): + sign += node.type + '-' + if node.type == 'TEX_IMAGE' and node.image is not None: + sign += node.image.filepath + '-' + for inp in node.inputs: + if inp.is_linked: + sign = self.signature_traverse(inp.links[0].from_node, sign) + else: + # Unconnected socket + if not hasattr(inp, 'default_value'): + sign += 'o' + elif inp.type in ('RGB', 'RGBA', 'VECTOR'): + sign += str(inp.default_value[0]) + sign += str(inp.default_value[1]) + sign += str(inp.default_value[2]) + else: + sign += str(inp.default_value) + return sign + + def get_signature(self, mat): + nodes = mat.node_tree.nodes + output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') + if output_node is not None: + sign = self.signature_traverse(output_node, '') + return sign + return None + + def export_materials(self): + wrd = bpy.data.worlds['Arm'] + + # Keep materials with fake user + for material in bpy.data.materials: + if material.use_fake_user and material not in self.material_array: + self.material_array.append(material) + + # Ensure the same order for merging materials + self.material_array.sort(key=lambda x: x.name) + + if wrd.arm_batch_materials: + mat_users = self.material_to_object_dict + mat_armusers = self.material_to_arm_object_dict + mat_batch.build(self.material_array, mat_users, mat_armusers) + + transluc_used = False + overlays_used = False + blending_used = False + depthtex_used = False + decals_used = False + sss_used = False + + for material in self.material_array: + # If the material is unlinked, material becomes None + if material is None: + continue + + if not material.use_nodes: + material.use_nodes = True + + # Recache material + signature = self.get_signature(material) + if signature != material.signature: + material.arm_cached = False + if signature is not None: + material.signature = signature + + o = {} + o['name'] = arm.utils.asset_name(material) + + if material.arm_skip_context != '': + o['skip_context'] = material.arm_skip_context + + rpdat = arm.utils.get_rp() + if material.arm_two_sided or not rpdat.arm_culling: + o['override_context'] = {} + o['override_context']['cull_mode'] = 'none' + elif material.arm_cull_mode != 'clockwise': + o['override_context'] = {} + o['override_context']['cull_mode'] = material.arm_cull_mode + + o['contexts'] = [] + + mat_users = self.material_to_object_dict + mat_armusers = self.material_to_arm_object_dict + sd, rpasses, needs_sss = make_material.parse(material, o, mat_users, mat_armusers) + sss_used |= needs_sss + + # Attach MovieTexture + for con in o['contexts']: + for tex in con['bind_textures']: + if 'source' in tex and tex['source'] == 'movie': + trait = {} + trait['type'] = 'Script' + trait['class_name'] = 'armory.trait.internal.MovieTexture' + ArmoryExporter.import_traits.append(trait['class_name']) + trait['parameters'] = ['"' + tex['file'] + '"'] + for user in mat_armusers[material]: + user['traits'].append(trait) + + if 'translucent' in rpasses: + transluc_used = True + if 'overlay' in rpasses: + overlays_used = True + if 'mesh' in rpasses: + if material.arm_blending: + blending_used = True + if material.arm_depth_read: + depthtex_used = True + if 'decal' in rpasses: + decals_used = True + + uv_export = False + tang_export = False + vcol_export = False + vs_str = '' + for con in sd['contexts']: + for elem in con['vertex_elements']: + if len(vs_str) > 0: + vs_str += ',' + vs_str += elem['name'] + if elem['name'] == 'tang': + tang_export = True + elif elem['name'] == 'tex': + uv_export = True + elif elem['name'] == 'col': + vcol_export = True + for con in o['contexts']: # TODO: blend context + if con['name'] == 'mesh' and material.arm_blending: + con['name'] = 'blend' + + if (material.export_tangents != tang_export) or \ + (material.export_uvs != uv_export) or \ + (material.export_vcols != vcol_export): + + material.export_uvs = uv_export + material.export_vcols = vcol_export + material.export_tangents = tang_export + + if material in self.material_to_object_dict: + mat_users = self.material_to_object_dict[material] + for ob in mat_users: + ob.data.arm_cached = False + + self.output['material_datas'].append(o) + material.arm_cached = True + + # Auto-enable render-path features + rebuild_rp = False + rpdat = arm.utils.get_rp() + if rpdat.rp_translucency_state == 'Auto' and rpdat.rp_translucency != transluc_used: + rpdat.rp_translucency = transluc_used + rebuild_rp = True + if rpdat.rp_overlays_state == 'Auto' and rpdat.rp_overlays != overlays_used: + rpdat.rp_overlays = overlays_used + rebuild_rp = True + if rpdat.rp_blending_state == 'Auto' and rpdat.rp_blending != blending_used: + rpdat.rp_blending = blending_used + rebuild_rp = True + if rpdat.rp_depth_texture_state == 'Auto' and rpdat.rp_depth_texture != depthtex_used: + rpdat.rp_depth_texture = depthtex_used + rebuild_rp = True + if rpdat.rp_decals_state == 'Auto' and rpdat.rp_decals != decals_used: + rpdat.rp_decals = decals_used + rebuild_rp = True + if rpdat.rp_sss_state == 'Auto' and rpdat.rp_sss != sss_used: + rpdat.rp_sss = sss_used + rebuild_rp = True + if rebuild_rp: + make_renderpath.build() + + def export_particle_systems(self): + if len(self.particle_system_array) > 0: + self.output['particle_datas'] = [] + for particleRef in self.particle_system_array.items(): + psettings = particleRef[0] + + if psettings is None: + continue + + if psettings.instance_object is None or psettings.render_type != 'OBJECT': + continue + + emit_from = 0 # VERT + if psettings.emit_from == 'FACE': + emit_from = 1 + elif psettings.emit_from == 'VOLUME': + emit_from = 2 + + out_particlesys = { + 'name': particleRef[1]["structName"], + 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR + 'loop': psettings.arm_loop, + # Emission + 'count': int(psettings.count * psettings.arm_count_mult), + 'frame_start': int(psettings.frame_start), + 'frame_end': int(psettings.frame_end), + 'lifetime': psettings.lifetime, + 'lifetime_random': psettings.lifetime_random, + 'emit_from': emit_from, + # Velocity + # 'normal_factor': psettings.normal_factor, + # 'tangent_factor': psettings.tangent_factor, + # 'tangent_phase': psettings.tangent_phase, + 'object_align_factor': ( + psettings.object_align_factor[0], + psettings.object_align_factor[1], + psettings.object_align_factor[2] + ), + # 'object_factor': psettings.object_factor, + 'factor_random': psettings.factor_random, + # Physics + 'physics_type': 1 if psettings.physics_type == 'NEWTON' else 0, + 'particle_size': psettings.particle_size, + 'size_random': psettings.size_random, + 'mass': psettings.mass, + # Render + 'instance_object': arm.utils.asset_name(psettings.instance_object), + # Field weights + 'weight_gravity': psettings.effector_weights.gravity + } + + if psettings.instance_object not in self.object_to_arm_object_dict: + # The instance object is not yet exported, e.g. because it is + # in a different scene outside of every (non-scene) collection + self.process_bobject(psettings.instance_object) + self.export_object(psettings.instance_object) + self.object_to_arm_object_dict[psettings.instance_object]['is_particle'] = True + + self.output['particle_datas'].append(out_particlesys) + + def export_tilesheets(self): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) > 0: + self.output['tilesheet_datas'] = [] + for ts in wrd.arm_tilesheetlist: + o = {} + o['name'] = ts.name + o['tilesx'] = ts.tilesx_prop + o['tilesy'] = ts.tilesy_prop + o['framerate'] = ts.framerate_prop + o['actions'] = [] + for tsa in ts.arm_tilesheetactionlist: + ao = {} + ao['name'] = tsa.name + ao['start'] = tsa.start_prop + ao['end'] = tsa.end_prop + ao['loop'] = tsa.loop_prop + o['actions'].append(ao) + self.output['tilesheet_datas'].append(o) + + def export_world(self): + """Exports the world of the current scene.""" + world = self.scene.world + + if world is not None: + world_name = arm.utils.safestr(world.name) + + if world_name not in self.world_array: + self.world_array.append(world_name) + out_world = {'name': world_name} + + self.post_export_world(world, out_world) + self.output['world_datas'].append(out_world) + + elif arm.utils.get_rp().rp_background == 'World': + log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared') + + def export_objects(self, scene): + """Exports all supported blender objects. + + References to objects are dictionaries storing the type and + name of that object. + + Currently supported: + - Mesh + - Light + - Camera + - Speaker + - Light Probe + """ + if not ArmoryExporter.option_mesh_only: + self.output['light_datas'] = [] + self.output['camera_datas'] = [] + self.output['speaker_datas'] = [] + + for light_ref in self.light_array.items(): + self.export_light(light_ref) + + for camera_ref in self.camera_array.items(): + self.export_camera(camera_ref) + + # Keep sounds with fake user + for sound in bpy.data.sounds: + if sound.use_fake_user: + assets.add(arm.utils.asset_path(sound.filepath)) + + for speaker_ref in self.speaker_array.items(): + self.export_speaker(speaker_ref) + + if bpy.data.lightprobes: + self.output['probe_datas'] = [] + for lightprobe_object in self.probe_array.items(): + self.export_probe(lightprobe_object) + + self.output['mesh_datas'] = [] + for mesh_ref in self.mesh_array.items(): + self.export_mesh(mesh_ref) + + def execute(self): + """Exports the scene.""" + profile_time = time.time() + wrd = bpy.data.worlds['Arm'] + if wrd.arm_verbose_output: + print('Exporting ' + arm.utils.asset_name(self.scene)) + if self.compress_enabled: + print('Scene data will be compressed which might take a while.') + + current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe + + scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() + # bobject => blender object + for bobject in scene_objects: + # Initialize object export data (map objects to game objects) + out_object: Dict[str, Any] = {'traits': []} + self.object_to_arm_object_dict[bobject] = out_object + + # Process + # Skip objects that have a parent because children are + # processed recursively + if not bobject.parent: + self.process_bobject(bobject) + # Softbody needs connected triangles, use optimized + # geometry export + for mod in bobject.modifiers: + if mod.type in ('CLOTH', 'SOFT_BODY'): + ArmoryExporter.optimize_enabled = True + + self.process_skinned_meshes() + + self.output['name'] = arm.utils.safestr(self.scene.name) + if self.filepath.endswith('.lz4'): + self.output['name'] += '.lz4' + elif not bpy.data.worlds['Arm'].arm_minimize: + self.output['name'] += '.json' + + # Create unique material variants for skinning, tilesheets and particles + matvars, matslots = self.create_material_variants(self.scene) + + # Auto-bones + rpdat = arm.utils.get_rp() + if rpdat.arm_skin_max_bones_auto: + max_bones = 8 + for armature in bpy.data.armatures: + if max_bones < len(armature.bones): + max_bones = len(armature.bones) + rpdat.arm_skin_max_bones = max_bones + + # Terrain + if self.scene.arm_terrain_object is not None: + assets.add_khafile_def('arm_terrain') + + # Append trait + out_trait = { + 'type': 'Script', + 'class_name': 'armory.trait.internal.TerrainPhysics' + } + if 'traits' not in self.output: + self.output['traits']: List[Dict[str, str]] = [] + + self.output['traits'].append(out_trait) + + ArmoryExporter.import_traits.append(out_trait['class_name']) + ArmoryExporter.export_physics = True + + # Export material + mat = self.scene.arm_terrain_object.children[0].data.materials[0] + self.material_array.append(mat) + # Terrain data + out_terrain = { + 'name': 'Terrain', + 'sectors_x': self.scene.arm_terrain_sectors[0], + 'sectors_y': self.scene.arm_terrain_sectors[1], + 'sector_size': self.scene.arm_terrain_sector_size, + 'height_scale': self.scene.arm_terrain_height_scale, + 'material_ref': mat.name + } + self.output['terrain_datas'] = [out_terrain] + self.output['terrain_ref'] = 'Terrain' + + # Export objects + self.output['objects'] = [] + for bobject in scene_objects: + # Skip objects that have a parent because children are + # exported recursively + if not bobject.parent: + self.export_object(bobject) + + # Export collections + if bpy.data.collections: + self.output['groups'] = [] + for collection in bpy.data.collections: + if collection.name.startswith(('RigidBodyWorld', 'Trait|')): + continue + + if self.scene.user_of_id(collection) or collection.library: + self.export_collection(collection) + + if not ArmoryExporter.option_mesh_only: + if self.scene.camera is not None: + self.output['camera_ref'] = self.scene.camera.name + else: + if self.scene.name == arm.utils.get_project_scene_name(): + log.warn(f'Scene "{self.scene.name}" is missing a camera') + + self.output['material_datas'] = [] + + # Object with no material assigned in the scene + if len(self.default_material_objects) > 0: + self.make_default_mat('armdefault', self.default_material_objects) + if len(self.default_skin_material_objects) > 0: + self.make_default_mat('armdefaultskin', self.default_skin_material_objects) + if len(bpy.data.particles) > 0: + self.use_default_material_part() + if len(self.default_part_material_objects) > 0: + self.make_default_mat('armdefaultpart', self.default_part_material_objects, is_particle=True) + + self.export_materials() + self.export_particle_systems() + self.output['world_datas'] = [] + self.export_world() + self.export_tilesheets() + + if self.scene.world is not None: + self.output['world_ref'] = arm.utils.safestr(self.scene.world.name) + + if self.scene.use_gravity: + self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]] + rbw = self.scene.rigidbody_world + if rbw is not None: + weights = rbw.effector_weights + self.output['gravity'][0] *= weights.all * weights.gravity + self.output['gravity'][1] *= weights.all * weights.gravity + self.output['gravity'][2] *= weights.all * weights.gravity + else: + self.output['gravity'] = [0.0, 0.0, 0.0] + + self.export_objects(self.scene) + + # Create Viewport camera + if bpy.data.worlds['Arm'].arm_play_camera != 'Scene': + self.create_default_camera(is_viewport_camera=True) + + elif self.scene.camera is not None and self.scene.camera.type != 'CAMERA': + # Blender doesn't directly allow to set arbitrary objects as cameras, + # but there is a `Set Active Object as Camera` operator which might + # cause cases like this to happen + log.warn(f'Camera "{self.scene.camera.name}" in scene "{self.scene.name}" is not a camera object, using default camera') + self.create_default_camera() + + # No camera found, create default one + if not self.has_spawning_camera: + log.warn(f'Scene "{self.scene.name}" is missing a camera, using default camera') + self.create_default_camera() + + self.export_scene_traits() + + self.export_canvas_themes() + + # Write embedded data references + if len(assets.embedded_data) > 0: + self.output['embedded_datas'] = [] + for file in assets.embedded_data: + self.output['embedded_datas'].append(file) + + # Write scene file + arm.utils.write_arm(self.filepath, self.output) + + # Remove created material variants + for slot in matslots: # Set back to original material + orig_mat = bpy.data.materials[slot.material.name[:-8]] # _armskin, _armpart, _armtile + orig_mat.export_uvs = slot.material.export_uvs + orig_mat.export_vcols = slot.material.export_vcols + orig_mat.export_tangents = slot.material.export_tangents + orig_mat.arm_cached = slot.material.arm_cached + slot.material = orig_mat + for mat in matvars: + bpy.data.materials.remove(mat, do_unlink=True) + + # Restore frame + if self.scene.frame_current != current_frame: + self.scene.frame_set(current_frame, subframe=current_subframe) + + if wrd.arm_verbose_output: + print('Scene exported in {:0.3f}s'.format(time.time() - profile_time)) + + def create_default_camera(self, is_viewport_camera=False): + """Creates the default camera and adds a WalkNavigation trait to it.""" + out_camera = { + 'name': 'DefaultCamera', + 'near_plane': 0.1, + 'far_plane': 100.0, + 'fov': 0.85, + 'frustum_culling': True, + 'clear_color': self.get_camera_clear_color() + } + + # Set viewport camera projection + if is_viewport_camera: + proj, is_persp = self.get_viewport_projection_matrix() + if proj is not None: + if is_persp: + self.extract_projection(out_camera, proj, with_planes=False) + else: + self.extract_ortho(out_camera, proj) + self.output['camera_datas'].append(out_camera) + + out_object = { + 'name': 'DefaultCamera', + 'type': 'camera_object', + 'data_ref': 'DefaultCamera', + 'material_refs': [], + 'transform': {} + } + viewport_matrix = self.get_viewport_view_matrix() + if viewport_matrix is not None: + out_object['transform']['values'] = ArmoryExporter.write_matrix(viewport_matrix.inverted_safe()) + out_object['local_only'] = True + else: + out_object['transform']['values'] = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] + + # Add WalkNavigation trait + trait = { + 'type': 'Script', + 'class_name': 'armory.trait.WalkNavigation' + } + out_object['traits'] = [trait] + ArmoryExporter.import_traits.append(trait['class_name']) + + self.output['objects'].append(out_object) + self.output['camera_ref'] = 'DefaultCamera' + self.has_spawning_camera = True + + @staticmethod + def get_export_tangents(mesh): + for material in mesh.materials: + if material is not None and material.export_tangents: + return True + return False + + @staticmethod + def get_export_vcols(mesh): + for material in mesh.materials: + if material is not None and material.export_vcols: + return True + return False + + @staticmethod + def get_export_uvs(mesh): + for material in mesh.materials: + if material is not None and material.export_uvs: + return True + return False + + @staticmethod + def object_process_instancing(refs, scale_pos): + instanced_type = 0 + instanced_data = None + for bobject in refs: + inst = bobject.arm_instanced + if inst != 'Off': + if inst == 'Loc': + instanced_type = 1 + instanced_data = [0.0, 0.0, 0.0] # Include parent + elif inst == 'Loc + Rot': + instanced_type = 2 + instanced_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + elif inst == 'Loc + Scale': + instanced_type = 3 + instanced_data = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] + elif inst == 'Loc + Rot + Scale': + instanced_type = 4 + instanced_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0] + + for child in bobject.children: + if not child.arm_export or child.hide_render: + continue + if 'Loc' in inst: + loc = child.matrix_local.to_translation() # Without parent matrix + instanced_data.append(loc.x / scale_pos) + instanced_data.append(loc.y / scale_pos) + instanced_data.append(loc.z / scale_pos) + if 'Rot' in inst: + rot = child.matrix_local.to_euler() + instanced_data.append(rot.x) + instanced_data.append(rot.y) + instanced_data.append(rot.z) + if 'Scale' in inst: + scale = child.matrix_local.to_scale() + instanced_data.append(scale.x) + instanced_data.append(scale.y) + instanced_data.append(scale.z) + break + + # Instance render collections with same children? + # elif bobject.instance_type == 'GROUP' and bobject.instance_collection is not None: + # instanced_type = 1 + # instanced_data = [] + # for child in bpy.data.collections[bobject.instance_collection].objects: + # loc = child.matrix_local.to_translation() + # instanced_data.append(loc.x) + # instanced_data.append(loc.y) + # instanced_data.append(loc.z) + # break + + return instanced_type, instanced_data + + @staticmethod + def rigid_body_static(rb): + return (not rb.enabled and not rb.kinematic) or (rb.type == 'PASSIVE' and not rb.kinematic) + + def post_export_object(self, bobject: bpy.types.Object, o, type): + # Export traits + self.export_traits(bobject, o) + + wrd = bpy.data.worlds['Arm'] + phys_enabled = wrd.arm_physics != 'Disabled' + phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' + + # Rigid body trait + if bobject.rigid_body is not None and phys_enabled: + ArmoryExporter.export_physics = True + rb = bobject.rigid_body + shape = 0 # BOX + if rb.collision_shape == 'SPHERE': + shape = 1 + elif rb.collision_shape == 'CONVEX_HULL': + shape = 2 + elif rb.collision_shape == 'MESH': + shape = 3 + elif rb.collision_shape == 'CONE': + shape = 4 + elif rb.collision_shape == 'CYLINDER': + shape = 5 + elif rb.collision_shape == 'CAPSULE': + shape = 6 + body_mass = rb.mass + is_static = self.rigid_body_static(rb) + if is_static: + body_mass = 0 + x = {} + x['type'] = 'Script' + x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.RigidBody' + col_group = '' + for b in rb.collision_collections: + col_group = ('1' if b else '0') + col_group + col_mask = '' + for b in bobject.arm_rb_collision_filter_mask: + col_mask = ('1' if b else '0') + col_mask + + x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] + lx = bobject.arm_rb_linear_factor[0] + ly = bobject.arm_rb_linear_factor[1] + lz = bobject.arm_rb_linear_factor[2] + ax = bobject.arm_rb_angular_factor[0] + ay = bobject.arm_rb_angular_factor[1] + az = bobject.arm_rb_angular_factor[2] + if bobject.lock_location[0]: + lx = 0 + if bobject.lock_location[1]: + ly = 0 + if bobject.lock_location[2]: + lz = 0 + if bobject.lock_rotation[0]: + ax = 0 + if bobject.lock_rotation[1]: + ay = 0 + if bobject.lock_rotation[2]: + az = 0 + col_margin = rb.collision_margin if rb.use_margin else 0.0 + if rb.use_deactivation: + deact_lv = rb.deactivate_linear_velocity + deact_av = rb.deactivate_angular_velocity + deact_time = bobject.arm_rb_deactivation_time + else: + deact_lv = 0.0 + deact_av = 0.0 + deact_time = 0.0 + body_params = {} + body_params['linearDamping'] = rb.linear_damping + body_params['angularDamping'] = rb.angular_damping + body_params['linearFactorsX'] = lx + body_params['linearFactorsY'] = ly + body_params['linearFactorsZ'] = lz + body_params['angularFactorsX'] = ax + body_params['angularFactorsY'] = ay + body_params['angularFactorsZ'] = az + body_params['angularFriction'] = bobject.arm_rb_angular_friction + body_params['collisionMargin'] = col_margin + body_params['linearDeactivationThreshold'] = deact_lv + body_params['angularDeactivationThrshold'] = deact_av + body_params['deactivationTime'] = deact_time + body_flags = {} + body_flags['animated'] = rb.kinematic + body_flags['trigger'] = bobject.arm_rb_trigger + body_flags['ccd'] = bobject.arm_rb_ccd + body_flags['staticObj'] = is_static + body_flags['useDeactivation'] = rb.use_deactivation + x['parameters'].append(arm.utils.get_haxe_json_string(body_params)) + x['parameters'].append(arm.utils.get_haxe_json_string(body_flags)) + o['traits'].append(x) + + # Phys traits + if phys_enabled: + for modifier in bobject.modifiers: + if modifier.type in ('CLOTH', 'SOFT_BODY'): + self.add_softbody_mod(o, bobject, modifier) + elif modifier.type == 'HOOK': + self.add_hook_mod(o, bobject, modifier.object.name, modifier.vertex_group) + + # Rigid body constraint + rbc = bobject.rigid_body_constraint + if rbc is not None and rbc.enabled: + self.add_rigidbody_constraint(o, bobject, rbc) + + # Camera traits + if type is NodeType.CAMERA: + # Viewport camera enabled, attach navigation to active camera + if self.scene.camera is not None and bobject.name == self.scene.camera.name and bpy.data.worlds['Arm'].arm_play_camera != 'Scene': + navigation_trait = {} + navigation_trait['type'] = 'Script' + navigation_trait['class_name'] = 'armory.trait.WalkNavigation' + o['traits'].append(navigation_trait) + + # Map objects to materials, can be used in later stages + for i in range(len(bobject.material_slots)): + mat = self.slot_to_material(bobject, bobject.material_slots[i]) + if mat in self.material_to_object_dict: + self.material_to_object_dict[mat].append(bobject) + self.material_to_arm_object_dict[mat].append(o) + 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: + o['constraints'] = [] + self.add_constraints(bobject, o) + + for x in o['traits']: + ArmoryExporter.import_traits.append(x['class_name']) + + @staticmethod + def add_constraints(bobject, o, bone=False): + for constraint in bobject.constraints: + if constraint.mute: + continue + out_constraint = {'name': constraint.name, 'type': constraint.type} + + if bone: + out_constraint['bone'] = bobject.name + if hasattr(constraint, 'target') and constraint.target is not None: + if constraint.type == 'COPY_LOCATION': + out_constraint['target'] = constraint.target.name + out_constraint['use_x'] = constraint.use_x + out_constraint['use_y'] = constraint.use_y + out_constraint['use_z'] = constraint.use_z + out_constraint['invert_x'] = constraint.invert_x + out_constraint['invert_y'] = constraint.invert_y + out_constraint['invert_z'] = constraint.invert_z + out_constraint['use_offset'] = constraint.use_offset + out_constraint['influence'] = constraint.influence + elif constraint.type == 'CHILD_OF': + out_constraint['target'] = constraint.target.name + out_constraint['influence'] = constraint.influence + + o['constraints'].append(out_constraint) + + def export_traits(self, bobject: Union[bpy.types.Scene, bpy.types.Object], o): + if not hasattr(bobject, 'arm_traitlist'): + return + + for traitlistItem in bobject.arm_traitlist: + # Do not export disabled traits but still export those + # with fake user enabled so that nodes like `TraitNode` + # still work + if not traitlistItem.enabled_prop and not traitlistItem.fake_user: + continue + + out_trait = {} + if traitlistItem.type_prop == 'Logic Nodes' and traitlistItem.node_tree_prop is not None and traitlistItem.node_tree_prop.name != '': + group_name = arm.utils.safesrc(traitlistItem.node_tree_prop.name[0].upper() + traitlistItem.node_tree_prop.name[1:]) + + out_trait['type'] = 'Script' + out_trait['class_name'] = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.node.' + group_name + + elif traitlistItem.type_prop == 'WebAssembly': + wpath = os.path.join(arm.utils.get_fp(), 'Bundled', traitlistItem.webassembly_prop + '.wasm') + if not os.path.exists(wpath): + log.warn(f'Wasm "{traitlistItem.webassembly_prop}" not found, skipping') + continue + + out_trait['type'] = 'Script' + out_trait['class_name'] = 'armory.trait.internal.WasmScript' + out_trait['parameters'] = ["'" + traitlistItem.webassembly_prop + "'"] + + elif traitlistItem.type_prop == 'UI Canvas': + cpath = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.json') + if not os.path.exists(cpath): + log.warn(f'Scene "{self.scene.name}" - Object "{bobject.name}" - Referenced canvas "{traitlistItem.canvas_name_prop}" not found, skipping') + continue + + ArmoryExporter.export_ui = True + out_trait['type'] = 'Script' + out_trait['class_name'] = 'armory.trait.internal.CanvasScript' + out_trait['parameters'] = ["'" + traitlistItem.canvas_name_prop + "'"] + + # Read file list and add canvas assets + assetpath = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas', traitlistItem.canvas_name_prop + '.files') + if os.path.exists(assetpath): + with open(assetpath,encoding="utf-8") as f: + file_list = f.read().splitlines() + for asset in file_list: + # Relative to the root/Bundled/canvas path + asset = asset[6:] # Strip ../../ to start in project root + assets.add(asset) + + # Haxe/Bundled Script + else: + # Empty class name, skip + if traitlistItem.class_name_prop == '': + continue + + out_trait['type'] = 'Script' + if traitlistItem.type_prop == 'Bundled Script': + trait_prefix = 'armory.trait.' + + # TODO: temporary, export single mesh navmesh as obj + if traitlistItem.class_name_prop == 'NavMesh' and bobject.type == 'MESH' and bpy.data.worlds['Arm'].arm_navigation != 'Disabled': + ArmoryExporter.export_navigation = True + + nav_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'navigation') + if not os.path.exists(nav_path): + os.makedirs(nav_path) + nav_filepath = os.path.join(nav_path, 'nav_' + bobject.data.name + '.arm') + assets.add(nav_filepath) + + # TODO: Implement cache + # if not os.path.isfile(nav_filepath): + # override = {'selected_objects': [bobject]} + # bobject.scale.y *= -1 + # mesh = obj.data + # for face in mesh.faces: + # face.v.reverse() + # bpy.ops.export_scene.obj(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False) + # bobject.scale.y *= -1 + armature = bobject.find_armature() + apply_modifiers = not armature + + bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject + export_mesh = bobject_eval.to_mesh() + + with open(nav_filepath, 'w') as f: + for v in export_mesh.vertices: + f.write("v %.4f " % (v.co[0] * bobject_eval.scale.x)) + f.write("%.4f " % (v.co[2] * bobject_eval.scale.z)) + f.write("%.4f\n" % (v.co[1] * bobject_eval.scale.y)) # Flipped + for p in export_mesh.polygons: + f.write("f") + # Flipped normals + for i in reversed(p.vertices): + f.write(" %d" % (i + 1)) + f.write("\n") + + # Haxe + else: + trait_prefix = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + '.' + hxfile = os.path.join('Sources', (trait_prefix + traitlistItem.class_name_prop).replace('.', '/') + '.hx') + if not os.path.exists(os.path.join(arm.utils.get_fp(), hxfile)): + # TODO: Halt build here once this check is tested + print(f'Armory Error: Scene "{self.scene.name}" - Object "{bobject.name}": Referenced trait file "{hxfile}" not found') + + out_trait['class_name'] = trait_prefix + traitlistItem.class_name_prop + + # Export trait properties + if traitlistItem.arm_traitpropslist: + out_trait['props'] = [] + for trait_prop in traitlistItem.arm_traitpropslist: + out_trait['props'].append(trait_prop.name) + out_trait['props'].append(trait_prop.type) + + if trait_prop.type.endswith("Object"): + value = arm.utils.asset_name(trait_prop.value_object) + else: + value = trait_prop.get_value() + + out_trait['props'].append(value) + + if not traitlistItem.enabled_prop: + # If we're here, fake_user is enabled, otherwise we + # would have skipped this trait already + ArmoryExporter.import_traits.append(out_trait['class_name']) + else: + o['traits'].append(out_trait) + + def export_scene_traits(self) -> None: + """Exports the traits of the scene and adds some internal traits + to the scene depending on the exporter settings. + """ + wrd = bpy.data.worlds['Arm'] + + if wrd.arm_physics != 'Disabled' and ArmoryExporter.export_physics: + if 'traits' not in self.output: + self.output['traits'] = [] + phys_pkg = 'bullet' if wrd.arm_physics_engine == 'Bullet' else 'oimo' + + out_trait = { + 'type': 'Script', + 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsWorld' + } + + rbw = self.scene.rigidbody_world + if rbw is not None and rbw.enabled: + out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] + + self.output['traits'].append(out_trait) + + if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: + if 'traits' not in self.output: + self.output['traits'] = [] + out_trait = {'type': 'Script', 'class_name': 'armory.trait.navigation.Navigation'} + self.output['traits'].append(out_trait) + + if wrd.arm_debug_console: + if 'traits' not in self.output: + self.output['traits'] = [] + ArmoryExporter.export_ui = True + # Position + debug_console_pos_type = 2 + if wrd.arm_debug_console_position == 'Left': + debug_console_pos_type = 0 + elif wrd.arm_debug_console_position == 'Center': + debug_console_pos_type = 1 + else: + debug_console_pos_type = 2 + # Parameters + out_trait = { + 'type': 'Script', + 'class_name': 'armory.trait.internal.DebugConsole', + 'parameters': [ + str(arm.utils.get_ui_scale()), + str(wrd.arm_debug_console_scale), + str(debug_console_pos_type), + str(int(wrd.arm_debug_console_visible)), + str(int(wrd.arm_debug_console_trace_pos)), + str(int(arm.utils.get_debug_console_visible_sc())), + str(int(arm.utils.get_debug_console_scale_in_sc())), + str(int(arm.utils.get_debug_console_scale_out_sc())) + ] + } + self.output['traits'].append(out_trait) + + if arm.utils.is_livepatch_enabled(): + if 'traits' not in self.output: + self.output['traits'] = [] + out_trait = {'type': 'Script', 'class_name': 'armory.trait.internal.LivePatch'} + self.output['traits'].append(out_trait) + + if len(self.scene.arm_traitlist) > 0: + if 'traits' not in self.output: + self.output['traits'] = [] + self.export_traits(self.scene, self.output) + + if 'traits' in self.output: + for out_trait in self.output['traits']: + ArmoryExporter.import_traits.append(out_trait['class_name']) + + @staticmethod + def export_canvas_themes(): + path_themes = os.path.join(arm.utils.get_fp(), 'Bundled', 'canvas') + file_theme = os.path.join(path_themes, "_themes.json") + + # If there is a canvas but no _themes.json, create it so that + # CanvasScript.hx works + if os.path.exists(path_themes) and not os.path.exists(file_theme): + with open(file_theme, "w+", encoding='utf-8'): + pass + assets.add(file_theme) + + @staticmethod + def add_softbody_mod(o, bobject: bpy.types.Object, modifier: Union[bpy.types.ClothModifier, bpy.types.SoftBodyModifier]): + """Adds a softbody trait to the given object based on the given + softbody/cloth modifier. + """ + ArmoryExporter.export_physics = True + assets.add_khafile_def('arm_physics_soft') + + phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' + out_trait = {'type': 'Script', 'class_name': 'armory.trait.physics.' + phys_pkg + '.SoftBody'} + # ClothModifier + if modifier.type == 'CLOTH': + bend = modifier.settings.bending_stiffness + soft_type = 0 + # SoftBodyModifier + elif modifier.type == 'SOFT_BODY': + bend = (modifier.settings.bend + 1.0) * 10 + soft_type = 1 + else: + # Wrong modifier type + return + + out_trait['parameters'] = [str(soft_type), str(bend), str(modifier.settings.mass), str(bobject.arm_soft_body_margin)] + o['traits'].append(out_trait) + + if soft_type == 0: + ArmoryExporter.add_hook_mod(o, bobject, '', modifier.settings.vertex_group_mass) + + @staticmethod + def add_hook_mod(o, bobject: bpy.types.Object, target_name, group_name): + ArmoryExporter.export_physics = True + + phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' + out_trait = {'type': 'Script', 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsHook'} + + verts = [] + if group_name != '': + group = bobject.vertex_groups[group_name].index + for v in bobject.data.vertices: + for g in v.groups: + if g.group == group: + verts.append(v.co.x) + verts.append(v.co.y) + verts.append(v.co.z) + + out_trait['parameters'] = [f"'{target_name}'", str(verts)] + o['traits'].append(out_trait) + + @staticmethod + def add_rigidbody_constraint(o, bobject, rbc): + rb1 = rbc.object1 + rb2 = rbc.object2 + if rb1 is None or rb2 is None: + return + if rbc.type == "MOTOR": + return + + ArmoryExporter.export_physics = True + phys_pkg = 'bullet' if bpy.data.worlds['Arm'].arm_physics_engine == 'Bullet' else 'oimo' + breaking_threshold = rbc.breaking_threshold if rbc.use_breaking else 0 + + trait = { + 'type': 'Script', + 'class_name': 'armory.trait.physics.' + phys_pkg + '.PhysicsConstraintExportHelper', + 'parameters': [ + "'" + rb1.name + "'", + "'" + rb2.name + "'", + str(rbc.disable_collisions).lower(), + str(breaking_threshold), + str(bobject.arm_relative_physics_constraint).lower() + ] + } + if rbc.type == "FIXED": + trait['parameters'].insert(2,str(0)) + if rbc.type == "POINT": + trait['parameters'].insert(2,str(1)) + if rbc.type == "GENERIC": + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_lin_y else 0, + rbc.limit_lin_y_lower, + rbc.limit_lin_y_upper, + 1 if rbc.use_limit_lin_z else 0, + rbc.limit_lin_z_lower, + rbc.limit_lin_z_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper, + 1 if rbc.use_limit_ang_y else 0, + rbc.limit_ang_y_lower, + rbc.limit_ang_y_upper, + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper + ] + trait['parameters'].insert(2,str(5)) + trait['parameters'].append(str(limits)) + if rbc.type == "GENERIC_SPRING": + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_lin_y else 0, + rbc.limit_lin_y_lower, + rbc.limit_lin_y_upper, + 1 if rbc.use_limit_lin_z else 0, + rbc.limit_lin_z_lower, + rbc.limit_lin_z_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper, + 1 if rbc.use_limit_ang_y else 0, + rbc.limit_ang_y_lower, + rbc.limit_ang_y_upper, + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper, + 1 if rbc.use_spring_x else 0, + rbc.spring_stiffness_x, + rbc.spring_damping_x, + 1 if rbc.use_spring_y else 0, + rbc.spring_stiffness_y, + rbc.spring_damping_y, + 1 if rbc.use_spring_z else 0, + rbc.spring_stiffness_z, + rbc.spring_damping_z, + 1 if rbc.use_spring_ang_x else 0, + rbc.spring_stiffness_ang_x, + rbc.spring_damping_ang_x, + 1 if rbc.use_spring_ang_y else 0, + rbc.spring_stiffness_ang_y, + rbc.spring_damping_ang_y, + 1 if rbc.use_spring_ang_z else 0, + rbc.spring_stiffness_ang_z, + rbc.spring_damping_ang_z + ] + trait['parameters'].insert(2,str(6)) + trait['parameters'].append(str(limits)) + if rbc.type == "HINGE": + limits = [ + 1 if rbc.use_limit_ang_z else 0, + rbc.limit_ang_z_lower, + rbc.limit_ang_z_upper + ] + trait['parameters'].insert(2,str(2)) + trait['parameters'].append(str(limits)) + if rbc.type == "SLIDER": + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper + ] + trait['parameters'].insert(2,str(3)) + trait['parameters'].append(str(limits)) + if rbc.type == "PISTON": + limits = [ + 1 if rbc.use_limit_lin_x else 0, + rbc.limit_lin_x_lower, + rbc.limit_lin_x_upper, + 1 if rbc.use_limit_ang_x else 0, + rbc.limit_ang_x_lower, + rbc.limit_ang_x_upper + ] + trait['parameters'].insert(2,str(4)) + trait['parameters'].append(str(limits)) + o['traits'].append(trait) + + @staticmethod + def post_export_world(world: bpy.types.World, out_world: Dict): + wrd = bpy.data.worlds['Arm'] + + bgcol = world.arm_envtex_color + # No compositor used + if '_LDR' in world.world_defs: + for i in range(0, 3): + bgcol[i] = pow(bgcol[i], 1.0 / 2.2) + out_world['background_color'] = arm.utils.color_to_int(bgcol) + + if '_EnvSky' in world.world_defs: + # Sky data for probe + out_world['sun_direction'] = list(world.arm_envtex_sun_direction) + out_world['turbidity'] = world.arm_envtex_turbidity + out_world['ground_albedo'] = world.arm_envtex_ground_albedo + out_world['nishita_density'] = list(world.arm_nishita_density) + + disable_hdr = world.arm_envtex_name.endswith('.jpg') + + if '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs: + out_world['envmap'] = world.arm_envtex_name.rsplit('.', 1)[0] + if disable_hdr: + out_world['envmap'] += '.jpg' + else: + out_world['envmap'] += '.hdr' + + # Main probe + rpdat = arm.utils.get_rp() + solid_mat = rpdat.arm_material_model == 'Solid' + arm_irradiance = rpdat.arm_irradiance and not solid_mat + arm_radiance = rpdat.arm_radiance + radtex = world.arm_envtex_name.rsplit('.', 1)[0] # Remove file extension + irrsharmonics = world.arm_envtex_irr_name + num_mips = world.arm_envtex_num_mips + strength = world.arm_envtex_strength + + mobile_mat = rpdat.arm_material_model in ('Mobile', 'Solid') + if mobile_mat: + arm_radiance = False + + out_probe = {'name': world.name} + if arm_irradiance: + ext = '' if wrd.arm_minimize else '.json' + out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext + if arm_radiance: + out_probe['radiance'] = radtex + '_radiance' + out_probe['radiance'] += '.jpg' if disable_hdr else '.hdr' + out_probe['radiance_mipmaps'] = num_mips + out_probe['strength'] = strength + out_world['probe'] = out_probe + + @staticmethod + def mod_equal(mod1: bpy.types.Modifier, mod2: bpy.types.Modifier) -> bool: + """Compares whether the given modifiers are equal.""" + # https://blender.stackexchange.com/questions/70629 + return all([getattr(mod1, prop, True) == getattr(mod2, prop, False) for prop in mod1.bl_rna.properties.keys()]) + + @staticmethod + def mod_equal_stack(obj1: bpy.types.Object, obj2: bpy.types.Object) -> bool: + """Returns `True` if the given objects have the same modifiers.""" + if len(obj1.modifiers) == 0 and len(obj2.modifiers) == 0: + return True + if len(obj1.modifiers) == 0 or len(obj2.modifiers) == 0: + return False + if len(obj1.modifiers) != len(obj2.modifiers): + return False + return all([ArmoryExporter.mod_equal(m, obj2.modifiers[i]) for i, m in enumerate(obj1.modifiers)]) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py new file mode 100644 index 0000000000..3e762062fe --- /dev/null +++ b/blender/arm/exporter_opt.py @@ -0,0 +1,420 @@ +""" +Exports smaller geometry but is slower. +To be replaced with https://github.com/zeux/meshoptimizer +""" +from typing import Optional + +import bpy +from mathutils import Vector +import numpy as np + +import arm.utils +from arm import log + +if arm.is_reload(__name__): + log = arm.reload_module(log) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +class Vertex: + __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") + + def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): + self.vertex_index = loop.vertex_index + loop_idx = loop.index + self.co = mesh.vertices[self.vertex_index].co[:] + self.normal = loop.normal[:] + self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) + self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] + self.loop_indices = [loop_idx] + self.index = 0 + + def __hash__(self): + return hash((self.co, self.normal, self.uvs)) + + def __eq__(self, other): + eq = ( + (self.co == other.co) and + (self.normal == other.normal) and + (self.uvs == other.uvs) and + (self.col == other.col) + ) + if eq: + indices = self.loop_indices + other.loop_indices + self.loop_indices = indices + other.loop_indices = indices + return eq + + +def calc_tangents(posa, nora, uva, ias, scale_pos): + num_verts = int(len(posa) / 4) + tangents = np.empty(num_verts * 3, dtype=' 0 + if self.has_baked_material(bobject, export_mesh.materials): + has_tex = True + has_tex1 = has_tex and num_uv_layers > 1 + num_colors = self.get_num_vertex_colors(export_mesh) + has_col = self.get_export_vcols(export_mesh) and num_colors > 0 + has_tang = self.has_tangents(export_mesh) + + pdata = np.empty(num_verts * 4, dtype=' maxdim: + maxdim = abs(v.uv[0]) + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + if has_tex1: + lay1 = uv_layers[t1map] + for v in lay1.data: + if abs(v.uv[0]) > maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay1 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay1 + if has_morph_target: + morph_data = np.empty(num_verts * 2, dtype=' maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay2 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay2 + if maxdim > 1: + o['scale_tex'] = maxdim + invscale_tex = (1 / o['scale_tex']) * 32767 + else: + invscale_tex = 1 * 32767 + self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) + + if has_col: + cdata = np.empty(num_verts * 3, dtype=' 2: + o['scale_pos'] = maxdim / 2 + else: + o['scale_pos'] = 1.0 + if has_armature: # Allow up to 2x bigger bounds for skinned mesh + o['scale_pos'] *= 2.0 + + scale_pos = o['scale_pos'] + invscale_pos = (1 / scale_pos) * 32767 + + # Make arrays + for i, v in enumerate(vert_list): + v.index = i + co = v.co + normal = v.normal + i4 = i * 4 + i2 = i * 2 + pdata[i4 ] = co[0] + pdata[i4 + 1] = co[1] + pdata[i4 + 2] = co[2] + pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale + ndata[i2 ] = normal[0] + ndata[i2 + 1] = normal[1] + if has_tex: + uv = v.uvs[t0map] + t0data[i2 ] = uv[0] + t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y + if has_tex1: + uv = v.uvs[t1map] + t1data[i2 ] = uv[0] + t1data[i2 + 1] = 1.0 - uv[1] + if has_morph_target: + uv = v.uvs[morph_uv_index] + morph_data[i2 ] = uv[0] + morph_data[i2 + 1] = 1.0 - uv[1] + if has_col: + i3 = i * 3 + cdata[i3 ] = v.col[0] + cdata[i3 + 1] = v.col[1] + cdata[i3 + 2] = v.col[2] + + # Indices + prims = {ma.name if ma else '': [] for ma in export_mesh.materials} + if not prims: + prims = {'': []} + + vert_dict = {i : v for v in vert_list for i in v.loop_indices} + for poly in export_mesh.polygons: + first = poly.loop_start + if len(export_mesh.materials) == 0: + prim = prims[''] + else: + mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] + prim = prims[mat.name if mat else ''] + indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + + if poly.loop_total == 3: + prim += indices + elif poly.loop_total > 3: + for i in range(poly.loop_total-2): + prim += (indices[-1], indices[i], indices[i + 1]) + + # Write indices + o['index_arrays'] = [] + for mat, prim in prims.items(): + idata = [0] * len(prim) + for i, v in enumerate(prim): + idata[i] = v + if len(idata) == 0: # No face assigned + continue + ia = {'values': idata, 'material': 0} + # Find material index for multi-mat mesh + if len(export_mesh.materials) > 1: + for i in range(0, len(export_mesh.materials)): + if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ + (export_mesh.materials[i] is None and mat == ''): # Default material for empty slots + ia['material'] = i + break + o['index_arrays'].append(ia) + + if has_tang: + tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) + + pdata *= invscale_pos + ndata *= 32767 + pdata = np.array(pdata, dtype=' max_bones: + log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Armory Render Props - Max Bones') + + for i in range(bone_count): + boneRef = self.find_bone(bone_array[i].name) + if boneRef: + oskin['bone_ref_array'].append(boneRef[1]["structName"]) + oskin['bone_len_array'].append(bone_array[i].length) + else: + oskin['bone_ref_array'].append("") + oskin['bone_len_array'].append(0.0) + + # Write the bind pose transform array + oskin['transformsI'] = [] + for i in range(bone_count): + skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() + skeletonI = (skeletonI @ bobject.matrix_world) + oskin['transformsI'].append(self.write_matrix(skeletonI)) + + # Export the per-vertex bone influence data + group_remap = [] + for group in bobject.vertex_groups: + for i in range(bone_count): + if bone_array[i].name == group.name: + group_remap.append(i) + break + else: + group_remap.append(-1) + + bone_count_array = np.empty(len(vert_list), dtype='= 0: #and bone_weight != 0.0: + bone_values.append((bone_weight, bone_index)) + total_weight += bone_weight + bone_count += 1 + + if bone_count > 4: + bone_count = 4 + bone_values.sort(reverse=True) + bone_values = bone_values[:4] + + bone_count_array[index] = bone_count + for bv in bone_values: + bone_weight_array[count] = bv[0] * 32767 + bone_index_array[count] = bv[1] + count += 1 + + if total_weight not in (0.0, 1.0): + normalizer = 1.0 / total_weight + for i in range(bone_count): + bone_weight_array[count - i - 1] *= normalizer + + oskin['bone_count_array'] = bone_count_array + oskin['bone_index_array'] = bone_index_array[:count] + oskin['bone_weight_array'] = bone_weight_array[:count] + + # Bone constraints + for bone in armature.pose.bones: + if len(bone.constraints) > 0: + if 'constraints' not in oskin: + oskin['constraints'] = [] + self.add_constraints(bone, oskin, bone=True) diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py new file mode 100644 index 0000000000..056a526705 --- /dev/null +++ b/blender/arm/handlers.py @@ -0,0 +1,297 @@ +import importlib +import os +import queue +import sys +import types + +import bpy +from bpy.app.handlers import persistent + +import arm +import arm.api +import arm.nodes_logic +import arm.make_state as state +import arm.utils +import arm.utils_vs +from arm import live_patch, log, make, props +from arm.logicnode import arm_nodes + +if arm.is_reload(__name__): + arm.api = arm.reload_module(arm.api) + live_patch = arm.reload_module(live_patch) + log = arm.reload_module(log) + arm_nodes = arm.reload_module(arm_nodes) + arm.nodes_logic = arm.reload_module(arm.nodes_logic) + make = arm.reload_module(make) + state = arm.reload_module(state) + props = arm.reload_module(props) + arm.utils = arm.reload_module(arm.utils) + arm.utils_vs = arm.reload_module(arm.utils_vs) +else: + arm.enable_reload(__name__) + + +@persistent +def on_depsgraph_update_post(self): + if state.proc_build is not None: + return + + # Recache + depsgraph = bpy.context.evaluated_depsgraph_get() + + for update in depsgraph.updates: + uid = update.id + if hasattr(uid, 'arm_cached'): + # uid.arm_cached = False # TODO: does not trigger update + if isinstance(uid, bpy.types.Mesh) and uid.name in bpy.data.meshes: + bpy.data.meshes[uid.name].arm_cached = False + elif isinstance(uid, bpy.types.Curve) and uid.name in bpy.data.curves: + bpy.data.curves[uid.name].arm_cached = False + elif isinstance(uid, bpy.types.MetaBall) and uid.name in bpy.data.metaballs: + bpy.data.metaballs[uid.name].arm_cached = False + elif isinstance(uid, bpy.types.Armature) and uid.name in bpy.data.armatures: + bpy.data.armatures[uid.name].arm_cached = False + elif isinstance(uid, bpy.types.NodeTree) and uid.name in bpy.data.node_groups: + bpy.data.node_groups[uid.name].arm_cached = False + elif isinstance(uid, bpy.types.Material) and uid.name in bpy.data.materials: + bpy.data.materials[uid.name].arm_cached = False + + # Send last operator to Krom + wrd = bpy.data.worlds['Arm'] + if state.proc_play is not None and state.target == 'krom' and wrd.arm_live_patch: + ops = bpy.context.window_manager.operators + if len(ops) > 0 and ops[-1] is not None: + live_patch.on_operator(ops[-1].bl_idname) + + # Hacky solution to update armory props after operator executions. + # bpy.context.active_operator doesn't always exist, in some cases + # like marking assets for example, this code is also executed before + # the operator actually finishes and sets the variable + last_operator = getattr(bpy.context, 'active_operator', None) + if last_operator is not None: + on_operator_post(last_operator.bl_idname) + + +def on_operator_post(operator_id: str) -> None: + """Called after operator execution. Does not work for operators + executed in another context. Warning: this function is also called + when the operator execution raised an exception!""" + # 3D View > Object > Rigid Body > Copy from Active + if operator_id == "RIGIDBODY_OT_object_settings_copy": + # Copy armory rigid body settings + source_obj = bpy.context.active_object + for target_obj in bpy.context.selected_objects: + target_obj.arm_rb_linear_factor = source_obj.arm_rb_linear_factor + target_obj.arm_rb_angular_factor = source_obj.arm_rb_angular_factor + target_obj.arm_rb_angular_friction = source_obj.arm_rb_angular_friction + target_obj.arm_rb_trigger = source_obj.arm_rb_trigger + target_obj.arm_rb_deactivation_time = source_obj.arm_rb_deactivation_time + target_obj.arm_rb_ccd = source_obj.arm_rb_ccd + target_obj.arm_rb_collision_filter_mask = source_obj.arm_rb_collision_filter_mask + + +def send_operator(op): + if hasattr(bpy.context, 'object') and bpy.context.object is not 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: + for area in bpy.context.screen.areas: + if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): + area.tag_redraw() + state.redraw_ui = False + + return 0.5 + + +def poll_threads() -> float: + """Polls the thread callback queue and if a thread has finished, it + is joined with the main thread and the corresponding callback is + executed in the main thread. + """ + try: + thread, callback = make.thread_callback_queue.get(block=False) + except queue.Empty: + return 0.25 + + thread.join() + + try: + callback() + except Exception as e: + # If there is an exception, we can no longer return the time to + # the next call to this polling function, so to keep it running + # we re-register it and then raise the original exception. + bpy.app.timers.unregister(poll_threads) + bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True) + raise e + + # Quickly check if another thread has finished + return 0.01 + + +loaded_py_libraries: dict[str, types.ModuleType] = {} +context_screen = None + + +@persistent +def on_save_pre(context): + # Ensure that files are saved with the correct version number + # (e.g. startup files with an "Arm" world may have old version numbers) + wrd = bpy.data.worlds['Arm'] + wrd.arm_version = props.arm_version + wrd.arm_commit = props.arm_commit + + +@persistent +def on_load_pre(context): + unload_py_libraries() + + +@persistent +def on_load_post(context): + global context_screen + context_screen = bpy.context.screen + + props.init_properties_on_load() + reload_blend_data() + arm.utils.fetch_bundled_script_names() + + wrd = bpy.data.worlds['Arm'] + wrd.arm_recompile = True + arm.api.remove_drivers() + + load_py_libraries() + + # Show trait users as collections + arm.utils.update_trait_collections() + props.update_armory_world() + + +def load_py_libraries(): + if bpy.data.filepath == '': + # When a blend file is opened from the file explorer, Blender + # first opens the default file and then the actual blend file, + # so this function is called twice. Because the cwd is already + # that of the folder containing the blend file, libraries would + # be loaded/unloaded once for the default file which is not needed. + return + + lib_path = os.path.join(arm.utils.get_fp(), 'Libraries') + if os.path.exists(lib_path): + # Don't register nodes twice when calling register_nodes() + arm_nodes.reset_globals() + + # Make sure that Armory's categories are registered first (on top of the menu) + arm.logicnode.init_categories() + + libs = os.listdir(lib_path) + for lib_name in libs: + fp = os.path.join(lib_path, lib_name) + if os.path.isdir(fp): + if os.path.exists(os.path.join(fp, 'blender.py')): + sys.path.append(fp) + + lib_module = importlib.import_module('blender') + importlib.reload(lib_module) + if hasattr(lib_module, 'register'): + lib_module.register() + + log.debug(f'Armory: Loaded Python library {lib_name}') + loaded_py_libraries[lib_name] = lib_module + + sys.path.remove(fp) + + # Register newly added nodes and node categories + arm.nodes_logic.register_nodes() + + +def unload_py_libraries(): + for lib_name, lib_module in loaded_py_libraries.items(): + if hasattr(lib_module, 'unregister'): + lib_module.unregister() + arm.log.debug(f'Armory: Unloaded Python library {lib_name}') + + loaded_py_libraries.clear() + + +def reload_blend_data(): + armory_pbr = bpy.data.node_groups.get('Armory PBR') + if armory_pbr is None: + load_library('Armory PBR') + + +def load_library(asset_name): + if bpy.data.filepath.endswith('arm_data.blend'): # Prevent load in library itself + return + sdk_path = arm.utils.get_sdk_path() + data_path = sdk_path + '/armory/blender/data/arm_data.blend' + data_names = [asset_name] + + # Import + data_refs = data_names.copy() + with bpy.data.libraries.load(data_path, link=False) as (data_from, data_to): + data_to.node_groups = data_refs + + for ref in data_refs: + ref.use_fake_user = True + + +def post_register(): + """Called in start.py after all Armory modules have been registered. + It is also called in case of add-on reloads. Put code here that + needs to be run once at the beginning of each session. + """ + if arm.utils.get_os_is_windows(): + arm.utils_vs.fetch_installed_vs(silent=True) + + +def register(): + bpy.app.handlers.save_pre.append(on_save_pre) + bpy.app.handlers.load_pre.append(on_load_pre) + 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() != '': + # TODO: On windows, on_load_post is not called when opening .blend file from explorer + if arm.utils.get_os() == 'win': + on_load_post(None) + else: + # load_py_libraries() is called by on_load_post(). This call makes sure that libraries are also loaded + # when a file is already opened during add-on registration + load_py_libraries() + + reload_blend_data() + + +def unregister(): + unload_py_libraries() + + bpy.app.timers.unregister(poll_threads) + bpy.app.timers.unregister(always) + + bpy.app.handlers.load_post.remove(on_load_post) + bpy.app.handlers.load_pre.remove(on_load_pre) + bpy.app.handlers.save_pre.remove(on_save_pre) + 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/keymap.py b/blender/arm/keymap.py new file mode 100644 index 0000000000..f8d39e3d90 --- /dev/null +++ b/blender/arm/keymap.py @@ -0,0 +1,54 @@ +import bpy +import arm +from arm import log, props_ui + +if arm.is_reload(__name__): + props_ui = arm.reload_module(props_ui) +else: + arm.enable_reload(__name__) + +arm_keymaps = [] + + +def register(): + wm = bpy.context.window_manager + addon_keyconfig = wm.keyconfigs.addon + + # Keyconfigs are not available in background mode. If the keyconfig + # was not found despite running _not_ in background mode, a warning + # is printed + if addon_keyconfig is None: + if not bpy.app.background: + log.warn("No keyconfig path found") + return + + km = addon_keyconfig.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW") + km.keymap_items.new(props_ui.ArmoryPlayButton.bl_idname, type='F5', value='PRESS') + km.keymap_items.new("tlm.build_lightmaps", type='F6', value='PRESS') + km.keymap_items.new("tlm.clean_lightmaps", type='F7', value='PRESS') + arm_keymaps.append(km) + + km = addon_keyconfig.keymaps.new(name='Node Editor', space_type='NODE_EDITOR') + + # shift+G: Create a new node call group node + km.keymap_items.new('arm.add_call_group_node', 'G', 'PRESS', shift=True) + + # ctrl+G: make node group from selected + km.keymap_items.new('arm.add_group_tree_from_selected', 'G', 'PRESS', ctrl=True) + + # TAB: enter node groups depending on selection + km.keymap_items.new('arm.edit_group_tree', 'TAB', 'PRESS') + + # ctrl+TAB: exit node groups depending on selectio + km.keymap_items.new('node.tree_path_parent', 'TAB', 'PRESS', ctrl=True) + + # alt+G: ungroup node tree + km.keymap_items.new('arm.ungroup_group_tree', 'G', 'PRESS', alt=True) + arm_keymaps.append(km) + + +def unregister(): + wm = bpy.context.window_manager + for km in arm_keymaps: + wm.keyconfigs.addon.keymaps.remove(km) + del arm_keymaps[:] diff --git a/blender/arm/lib/__init__.py b/blender/arm/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lib/armpack.py b/blender/arm/lib/armpack.py new file mode 100644 index 0000000000..73cf08dfa4 --- /dev/null +++ b/blender/arm/lib/armpack.py @@ -0,0 +1,175 @@ +"""Msgpack parser with typed arrays""" + +# Based on u-msgpack-python v2.4.1 - v at sergeev.io +# https://github.com/vsergeev/u-msgpack-python +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import io +import struct +import numpy as np + + +def _pack_integer(obj, fp): + if obj < 0: + if obj >= -32: + fp.write(struct.pack("b", obj)) + elif obj >= -(2 ** (8 - 1)): + fp.write(b"\xd0" + struct.pack("b", obj)) + elif obj >= -(2 ** (16 - 1)): + fp.write(b"\xd1" + struct.pack("= -(2 ** (32 - 1)): + fp.write(b"\xd2" + struct.pack("= -(2 ** (64 - 1)): + fp.write(b"\xd3" + struct.pack(" 0 and isinstance(obj[0], float): + fp.write(b"\xca") + for e in obj: + fp.write(struct.pack(" 0 and isinstance(obj[0], bool): + for e in obj: + pack(e, fp) + elif len(obj) > 0 and isinstance(obj[0], int): + fp.write(b"\xd2") + for e in obj: + fp.write(struct.pack(" 0 and isinstance(obj[0], np.float32): + fp.write(b"\xca") + fp.write(obj.tobytes()) + # Int32 + elif len(obj) > 0 and isinstance(obj[0], np.int32): + fp.write(b"\xd2") + fp.write(obj.tobytes()) + # Int16 + elif len(obj) > 0 and isinstance(obj[0], np.int16): + fp.write(b"\xd1") + fp.write(obj.tobytes()) + # Regular + else: + for e in obj: + pack(e, fp) + + +def _pack_map(obj, fp): + if len(obj) <= 15: + fp.write(struct.pack("B", 0x80 | len(obj))) + elif len(obj) <= 2**16 - 1: + fp.write(b"\xde" + struct.pack(" int: + return 0 if size > 0x7E000000 else size + (size // 255 | 0) + 16 + + @staticmethod + def encode(b: bytes) -> bytes: + i_buf: np.ndarray = np.frombuffer(b, dtype=uint8) + i_len = i_buf.size + + if i_len >= 0x7E000000: + raise LZ4RangeException("Input buffer is too large") + + # "The last match must start at least 12 bytes before end of block" + last_match_pos = i_len - 12 + + # "The last 5 bytes are always literals" + last_literal_pos = i_len - 5 + + if LZ4.hash_table is None: + LZ4.hash_table = np.full(shape=65536, fill_value=-65536, dtype=int32) + + LZ4.hash_table.fill(-65536) + + o_len = LZ4.encode_bound(i_len) + o_buf = np.full(shape=o_len, fill_value=0, dtype=uint8) + i_pos = 0 + o_pos = 0 + anchor_pos = 0 + + # Sequence-finding loop + while True: + ref_pos = int32(0) + m_offset = 0 + sequence = uint32( + i_buf[i_pos] << 8 | i_buf[i_pos + 1] << 16 | i_buf[i_pos + 2] << 24 + ) + + # Match-finding loop + while i_pos <= last_match_pos: + # Conversion to uint32 is mandatory to ensure correct + # unsigned right shift (compare with .hx implementation) + sequence = uint32( + uint32(sequence) >> uint32(8) | i_buf[i_pos + 3] << 24 + ) + hash_val = (sequence * 0x9E37 & 0xFFFF) + ( + uint32(sequence * 0x79B1) >> uint32(16) + ) & 0xFFFF + ref_pos = LZ4.hash_table[hash_val] + LZ4.hash_table[hash_val] = i_pos + m_offset = i_pos - ref_pos + if ( + m_offset < 65536 + and i_buf[ref_pos + 0] == (sequence & 0xFF) + and i_buf[ref_pos + 1] == ((sequence >> uint32(8)) & 0xFF) + and i_buf[ref_pos + 2] == ((sequence >> uint32(16)) & 0xFF) + and i_buf[ref_pos + 3] == ((sequence >> uint32(24)) & 0xFF) + ): + break + + i_pos += 1 + + # No match found + if i_pos > last_match_pos: + break + + # Match found + l_len = i_pos - anchor_pos + m_len = i_pos + i_pos += 4 + ref_pos += 4 + while i_pos < last_literal_pos and i_buf[i_pos] == i_buf[ref_pos]: + i_pos += 1 + ref_pos += 1 + + m_len = i_pos - m_len + token = m_len - 4 if m_len < 19 else 15 + + # Write token, length of literals if needed + if l_len >= 15: + o_buf[o_pos] = 0xF0 | token + o_pos += 1 + l = l_len - 15 + while l >= 255: + o_buf[o_pos] = 255 + o_pos += 1 + l -= 255 + o_buf[o_pos] = l + o_pos += 1 + else: + o_buf[o_pos] = (l_len << 4) | token + o_pos += 1 + + # Write literals + while l_len > 0: + l_len -= 1 + o_buf[o_pos] = i_buf[anchor_pos] + o_pos += 1 + anchor_pos += 1 + + if m_len == 0: + break + + # Write offset of match + o_buf[o_pos + 0] = m_offset + o_buf[o_pos + 1] = m_offset >> 8 + o_pos += 2 + + # Write length of match if needed + if m_len >= 19: + l = m_len - 19 + while l >= 255: + o_buf[o_pos] = 255 + o_pos += 1 + l -= 255 + + o_buf[o_pos] = l + o_pos += 1 + + anchor_pos = i_pos + + # Last sequence is literals only + l_len = i_len - anchor_pos + if l_len >= 15: + o_buf[o_pos] = 0xF0 + o_pos += 1 + l = l_len - 15 + while l >= 255: + o_buf[o_pos] = 255 + o_pos += 1 + l -= 255 + + o_buf[o_pos] = l + o_pos += 1 + + else: + o_buf[o_pos] = l_len << 4 + o_pos += 1 + + while l_len > 0: + l_len -= 1 + o_buf[o_pos] = i_buf[anchor_pos] + o_pos += 1 + anchor_pos += 1 + + return np.resize(o_buf, o_pos).tobytes() diff --git a/blender/arm/lib/make_datas.py b/blender/arm/lib/make_datas.py new file mode 100644 index 0000000000..2419797c39 --- /dev/null +++ b/blender/arm/lib/make_datas.py @@ -0,0 +1,328 @@ +import arm.utils +from arm import assets + +def parse_context( + c: dict, + sres: dict, + asset, + defs: list[str], + vert: list[str] = None, + frag: list[str] = None, +): + con = { + "name": c["name"], + "constants": [], + "texture_units": [], + "vertex_elements": [], + } + sres["contexts"].append(con) + + # Names + con["vertex_shader"] = c["vertex_shader"].rsplit(".", 1)[0].split("/")[-1] + if con["vertex_shader"] not in asset: + asset.append(con["vertex_shader"]) + + con["fragment_shader"] = c["fragment_shader"].rsplit(".", 1)[0].split("/")[-1] + if con["fragment_shader"] not in asset: + asset.append(con["fragment_shader"]) + + if "geometry_shader" in c: + con["geometry_shader"] = c["geometry_shader"].rsplit(".", 1)[0].split("/")[-1] + if con["geometry_shader"] not in asset: + asset.append(con["geometry_shader"]) + + if "tesscontrol_shader" in c: + con["tesscontrol_shader"] = ( + c["tesscontrol_shader"].rsplit(".", 1)[0].split("/")[-1] + ) + if con["tesscontrol_shader"] not in asset: + asset.append(con["tesscontrol_shader"]) + + if "tesseval_shader" in c: + con["tesseval_shader"] = c["tesseval_shader"].rsplit(".", 1)[0].split("/")[-1] + if con["tesseval_shader"] not in asset: + asset.append(con["tesseval_shader"]) + + if "color_attachments" in c: + con["color_attachments"] = c["color_attachments"] + for i in range(len(con["color_attachments"])): + if con["color_attachments"][i] == "_HDR": + con["color_attachments"][i] = "RGBA32" if "_LDR" in defs else "RGBA64" + + # Params + params = [ + "depth_write", + "compare_mode", + "cull_mode", + "blend_source", + "blend_destination", + "blend_operation", + "alpha_blend_source", + "alpha_blend_destination", + "alpha_blend_operation", + "color_writes_red", + "color_writes_green", + "color_writes_blue", + "color_writes_alpha", + "conservative_raster", + ] + + for p in params: + if p in c: + con[p] = c[p] + + # Parse shaders + if vert is None: + with open(c["vertex_shader"], encoding="utf-8") as f: + vert = f.read().splitlines() + parse_shader(sres, c, con, defs, vert, True) # Parse attribs for vertex shader + + if frag is None: + with open(c["fragment_shader"], encoding="utf-8") as f: + frag = f.read().splitlines() + parse_shader(sres, c, con, defs, frag, False) + + if "geometry_shader" in c: + with open(c["geometry_shader"], encoding="utf-8") as f: + geom = f.read().splitlines() + parse_shader(sres, c, con, defs, geom, False) + + if "tesscontrol_shader" in c: + with open(c["tesscontrol_shader"], encoding="utf-8") as f: + tesc = f.read().splitlines() + parse_shader(sres, c, con, defs, tesc, False) + + if "tesseval_shader" in c: + with open(c["tesseval_shader"], encoding="utf-8") as f: + tese = f.read().splitlines() + parse_shader(sres, c, con, defs, tese, False) + + +def parse_shader( + sres, c: dict, con: dict, defs: list[str], lines: list[str], parse_attributes: bool +): + """Parses the given shader to get information about the used vertex + elements, uniforms and constants. This information is later used in + Iron to check what data each shader requires. + + @param defs A list of set defines for the preprocessor + @param lines The list of lines of the shader file + @param parse_attributes Whether to parse vertex elements + """ + vertex_elements_parsed = False + vertex_elements_parsing = False + + # Stack of the state of all preprocessor conditions for the current + # line. If there is a `False` in the stack, at least one surrounding + # condition is false and the line must not be parsed + stack: list[bool] = [] + + if not parse_attributes: + vertex_elements_parsed = True + + for line in lines: + line = line.lstrip() + + # Preprocessor + if line.startswith("#if"): # if, ifdef, ifndef + s = line.split(" ")[1] + found = s in defs + if line.startswith("#ifndef"): + found = not found + stack.append(found) + continue + + if line.startswith("#else"): + stack[-1] = not stack[-1] + continue + + if line.startswith("#endif"): + stack.pop() + continue + + # Skip lines if the stack contains at least one preprocessor + # condition that is not fulfilled + skip = False + for condition in stack: + if not condition: + skip = True + break + if skip: + continue + + if not vertex_elements_parsed and line.startswith("in "): + vertex_elements_parsing = True + s = line.split(" ") + con["vertex_elements"].append( + { + "data": "float" + s[1][-1:], + "name": s[2][:-1], # [:1] to get rid of the semicolon + } + ) + + # Stop the vertex element parsing if no other vertex elements + # follow directly (assuming all vertex elements are positioned + # directly after each other apart from empty lines and comments) + if ( + vertex_elements_parsing + and len(line) > 0 + and not line.startswith("//") + and not line.startswith("in ") + ): + vertex_elements_parsed = True + + if line.startswith("uniform ") or line.startswith( + "//!uniform" + ): # Uniforms included from header files + s = line.split(" ") + # Examples: + # uniform sampler2D myname; + # uniform layout(RGBA8) image3D myname; + if s[1].startswith("layout"): + ctype = s[2] + cid = s[3] + if cid[-1] == ";": + cid = cid[:-1] + else: + ctype = s[1] + cid = s[2] + if cid[-1] == ";": + cid = cid[:-1] + + found = False # Uniqueness check + if ( + ctype.startswith("sampler") + or ctype.startswith("image") + or ctype.startswith("uimage") + ): # Texture unit + for tu in con["texture_units"]: + if tu["name"] == cid: + # Texture already present + found = True + break + if not found: + if cid[-1] == "]": # Array of samplers - sampler2D mySamplers[2] + # Add individual units - mySamplers[0], mySamplers[1] + for i in range(int(cid[-2])): + tu = {"name": cid[:-2] + str(i) + "]"} + con["texture_units"].append(tu) + else: + tu = {"name": cid} + con["texture_units"].append(tu) + if ctype.startswith("image") or ctype.startswith("uimage"): + tu["is_image"] = True + + check_link(c, defs, cid, tu) + + else: # Constant + if cid.find("[") != -1: # Float arrays + cid = cid.split("[")[0] + ctype = "floats" + for const in con["constants"]: + if const["name"] == cid: + found = True + break + if not found: + const = {"type": ctype, "name": cid} + con["constants"].append(const) + + check_link(c, defs, cid, const) + + +def check_link(source_context: dict, defs: list[str], cid: str, out: dict): + """Checks whether the uniform/constant with the given name (`cid`) + has a link stated in the json (`source_context`) that can be safely + included based on the given defines (`defs`). If that is the case, + the found link is written to the `out` dictionary. + """ + for link in source_context["links"]: + if link["name"] == cid: + valid_link = True + + # Optionally only use link if at least + # one of the given defines is set + if "ifdef" in link: + def_found = False + for d in defs: + for link_def in link["ifdef"]: + if d == link_def: + def_found = True + break + if def_found: + break + if not def_found: + valid_link = False + + # Optionally only use link if none of + # the given defines are set + if "ifndef" in link: + def_found = False + for d in defs: + for link_def in link["ifndef"]: + if d == link_def: + def_found = True + break + if def_found: + break + if def_found: + valid_link = False + + if valid_link: + out["link"] = link["link"] + break + + +def make( + res: dict, base_name: str, json_data: dict, fp, defs: list[str], make_variants: bool +): + sres = {"name": base_name, "contexts": []} + res["shader_datas"].append(sres) + asset = assets.shader_passes_assets[base_name] + + vert = None + frag = None + has_variants = "variants" in json_data and len(json_data["variants"]) > 0 + if make_variants and has_variants: + d = json_data["variants"][0] + if d in defs: + # Write shader variant with define + c = json_data["contexts"][0] + with open(c["vertex_shader"], encoding="utf-8") as f: + vert = f.read().split("\n", 1)[1] + vert = "#version 450\n#define " + d + "\n" + vert + + with open(c["fragment_shader"], encoding="utf-8") as f: + frag = f.read().split("\n", 1)[1] + frag = "#version 450\n#define " + d + "\n" + frag + + with open( + arm.utils.get_fp_build() + + "/compiled/Shaders/" + + base_name + + d + + ".vert.glsl", + "w", + encoding="utf-8", + ) as f: + f.write(vert) + + with open( + arm.utils.get_fp_build() + + "/compiled/Shaders/" + + base_name + + d + + ".frag.glsl", + "w", + encoding="utf-8", + ) as f: + f.write(frag) + + # Add context variant + c2 = c.copy() + c2["vertex_shader"] = base_name + d + ".vert.glsl" + c2["fragment_shader"] = base_name + d + ".frag.glsl" + c2["name"] = c["name"] + d + parse_context(c2, sres, asset, defs, vert.splitlines(), frag.splitlines()) + + for c in json_data["contexts"]: + parse_context(c, sres, asset, defs) diff --git a/blender/arm/lib/server.py b/blender/arm/lib/server.py new file mode 100644 index 0000000000..b6f43f0263 --- /dev/null +++ b/blender/arm/lib/server.py @@ -0,0 +1,33 @@ +import atexit +import http.server +import socketserver +import subprocess + +haxe_server = None + + +def run_tcp(port: int, do_log: bool): + class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def log_message(self, format, *args): + if do_log: + print(format % args) + + try: + http_server = socketserver.TCPServer(("", port), HTTPRequestHandler) + http_server.serve_forever() + except: + print("Server already running") + + +def run_haxe(haxe_path, port=6000): + global haxe_server + if haxe_server is None: + haxe_server = subprocess.Popen([haxe_path, "--wait", str(port)]) + atexit.register(kill_haxe) + + +def kill_haxe(): + global haxe_server + if haxe_server is not None: + haxe_server.kill() + haxe_server = None diff --git a/blender/arm/lightmapper/__init__.py b/blender/arm/lightmapper/__init__.py new file mode 100644 index 0000000000..8df717b451 --- /dev/null +++ b/blender/arm/lightmapper/__init__.py @@ -0,0 +1 @@ +__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file diff --git a/blender/arm/lightmapper/assets/TLM_Overlay.png b/blender/arm/lightmapper/assets/TLM_Overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..aa79e6efc2dbd15d248e9afbb08fa0875150871e GIT binary patch literal 24403 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/assets/dash.ogg b/blender/arm/lightmapper/assets/dash.ogg new file mode 100644 index 0000000000000000000000000000000000000000..319b59505106fd66f823d8002c87abfd7dbe7664 GIT binary patch literal 36333 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 T00000000000002szk!tj=@K!p literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/assets/gentle.ogg b/blender/arm/lightmapper/assets/gentle.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e36d106defb89ff4bb0bb014e87f868d83d5e5c5 GIT binary patch literal 6615 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�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

W4s2c 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/assets/sound.ogg b/blender/arm/lightmapper/assets/sound.ogg new file mode 100644 index 0000000000000000000000000000000000000000..9f581e4d90d32ca0f12a1dea9133563ac8dceaab GIT binary patch literal 20436 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�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

W4s2c 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 s0`XDK!@|Qz}Jr#RVZNbxPC9+;U;c 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 z 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

0jVp<00ydE#4~LtO2xDMxVV6yMOtBr+C9A zCSVDDuHbvH%GGahNAv2t5<>f3yAIs%t9|v-Iep~as4^zSi0>tEPo|r05@z&y?U!w5 z_s?hk6$-_IDESS}il@uxQ0N|%ofo$+=^yQUEysJ^zh;m~tCdwRSigg!y+AP`3WhR@3 zDBmWgvHZa1lmE@Oe z&B$XD!ta`A3`bxcW$--^QG`mJ2@*j4Hovo-Q%@*C>Eh9nF!X8C{?4SGoBYnpZ&FPc zj*z8#2E3QKn(?BJ(}t*Sdt9rFNva;-)PcU6~vx$1;Cg!`oq(`p#{KRzWWyb zd(D}+Omh$T7u5^+x%_9uiH{98L|;{~U49vSAoP914Pm|N3-BxX4;;jc1uMt4sbDhV zdgy*bNhmr-k(>b`Z?_HL_Y)uRN%)tlpSz!g_9hIh9ns>3sJ>5HIbSQptl`fbui|#- z5B90wR+DaM6C)>M&rr9@Or#I{FdYf=O0qBOG0ur@-ibgBFuXH~A&YICSck#+?^V^6 zQI;*{u#KEp#bLT>R(UUc>F7J?)MD+SNn5&QM+irWgua&xs>4@C~E;9IifZi)w%ZRU+| z?A-WY_*P-#p5x0{$(w4!zVl3JMlk_Akh45Vo=4PE*c%+^)d9;_R88p)m{r>KE= zr3XSaXhDmoWof6vSzSZDE@i9>Ij-2!rnsYFr&NJ%+{#zrS8vRA)Cflz?6(WHYX((J zX`53TeIqf_>3Dy@*w%IUq`jKBcwDQjdROh2|I+ zlZM70u>(?8#*^w`Yc@OWPH$8CEgFBrQFO>sFwG>|Vz1!pJ@=L1h3~R&L}>KnnR=L} zmGo5VPz3*JMb@V z(5``SrS%2(2ISqt9}qi>Ra0S0Ohh>wa{qX5evij1a@A7QSVD9W?X@r;{!tcHUci}5Sy=ms(nHNOm7Ym7z39Hrd zT^utLVkEhg6_39JzMAL9`fROaX%JJhWzkqc z5h*yb8*#Co6)>QMKPxHNLJS(Y_Y1v}Q`)fL`#kY)cEU`cMVk=4Z{lkkuq^&QCzj%E zvxs*S(u^p(4VCLiK1T?6>wfszc4!MU0dQZc4GPrj=qyc3io_)?tR=Xt$s2345^Ep2 zV&w5%)6mabRfWxDg^gaCFJs~ane7O%?O1~hI6KJJfy#YY+1;E|X!dq110TIWF#v`9B4Po+DE)Orhz>iVaby7jnME7U6?N%}$IJ*t^B~ z>_B`fa|ESdM8glbf1AyCt0rzsZ$N~mEoxMG5p%QmX03C~WX&`EsYue3j$-#v*W!`l z!3<8xYEYBtwdn+)w4sx6z(xzKw(W1=X(?k`7dTpq;!846=t%};hay9}C%nE7)_(}H zfOx>^;q&&f1Jt?a+!z1vz_kEH_ukMQ8aUShZjQ)xsA9bb7z`2lKLrceTj)qyp1gY8 zs*J4Z+TsYKPJVefs<>i$So2(yHpk#&`fs%@#LI8%bq~R%Zni0odI=WaWsRJG2K;jb zS!OI$H*(SU4LitOU?f<)88ty;Jy1^eW`rNJqx=p0=or{wCR=QUI~W&QCVp#T@{kE8gnGCS7P^_kNDw(kRy z@pc=&NSM=r%an8o*arPCaC;+Z?_kc(hB8f?a+x3UfO2{2?-Uc%(^Md>=IQOz&~i=s z1C@(9dT3;vZc?;?glkDnjh~$d;j06-RSkE9?N0NQzdQM_cOT=)?bSFH)#o0EcW27C zu|JVlh8)Ax?&!eGvndxYoq!?vEyA3M<(q^}KQOENJRhr*N&Bex@SSCrzKHu{>1nS1 ziT<1?JC#4soq9}9=`C2Ne)k~_cPMlNQvc@emlHF7-!Yk@+?>f1j z7U7*kQ9r&Myss=FOu~oKdb+$uzD4XrjFf2gd=oID7`cOZU9g?2Q~k^Rxi#6wp&RyS|pahC;icD$03a<2vHO_P%NCHI`KpF<1$zYVO%Z%;(Qh z!Fidn{8K;;GhRjnNcA=TYDCvlHoxaBYs42ixUL!yV}bJb3){I2TX;J1UXG&5hLP>d z=wA%(FbW3DRD3Vj^Y_0){<%`L6jsF!7~3A|%0!&jvlHKBD_xL!qB`9>$#afwS2knT+1k#CFsfQ{Ogu^5iC zh-(RB+IOk>Q@bhKa!KF#lLb}2U{j(|7~op4PhOdWEAGLfzmTDD2GfLykje0plYqCCQ@tJUgIsuw?9jdXM?9kVgAMPc7$d1*=yD#LHB}?*3`}i-#c6 zY-?Mnt%75EZ3}gYE*C^$-P)Ny<*fIv#;FXq{F1$XJLL7Y2{`m4&&=p|8FW6KKB41;pr$hN249o#g-SNDiI0w(pDavhL z;h8j+;la=qfXZyhQR~$kNVDB4zWkS78Ys79%)-~fjhbhGe7un;02;yAA1bcOgT`vO za7b=|$cP&&*v|Yl;H*XH<4Ro%>=`GJ8aoHwCZ|%fyH9gx{@-C7LN?Q3ypZP%mbxmF zkcCgGZWQL&-3smQS2s2tvibfsJx#(PzcR=koa^ye;k z&(dT^h2mpN>ms25iayJ{^_~2yc-N>tQI+YFGd_#`v+0tyo5=06y)98~;hGKYg(s7N z6(Bk%E*$4p0Jn3a>H>wX#YgVL%NLgN-kaC)4z^Lt9KVq1eMTv0&0jHwB6L=Ewyh!@ z23lvc=g-VjL|mmr20xwlXLj$Q;$BjCNu*%5oAO=qqq(y%*Rw+4LjfJJM&$sSCLld*+($oiMlK z!j;>Y&(63tE5*ExdW(nC>izQM9~ubxa^h39^2Wmn5%3XF(*um__{~vdw^ckUi?rk? z55|7y+GH&*(DIQn5v=)riBo_^B>gG5h;eR7UhkC;du`?6HiRd1jNA53O>+x9?epMy82VXP$bKPAnx?`JKKeZuQ1V>vlYKDg}JvqhK^d5B2{1I!!31st84d z{4W#Ir6Y2UU&adiF=5M4P)gvuf5X`Csm~;O52lu>I8}R0@(bzYe8%B=%61Iu%jm?9 z$-WP-ut{aOkk>(o=JRvbOMgE}t#b?O<{bV*O9~tPF`vklrn7w=o8n0mUS)v>T@&tK znapKj?M8YXxCtcX)9~lF>X&*}VHQVjQy7sH>!X$TDJ%Paiz?|qRY<5$41;3blgX2V z0l2;&@8FQ#WD7(7=QdP{EuqIY*FpA2$P#IE`pZdlFtrW1BwXIA;Y=f3 zDC#RJ5(a}YRZ(C|nM_y3bJNd|;RI;y4b~qwaSlCOv&;hf?46#Nl2_!LX$G@}urS~w zPv7GWtd1aY`t4O*Z0lRr8zA8)G(=SuA^;uRcBQ-3R{BtAM!25hvkY?ru10-Z!Rhth z-|71`s+QA>9xEKmIR&>1_21Qclb&+YcFrbt5OFTQu<{k#uonGktHt8@S80zf3q)GG zDbg_G9#M=D$~lb0QNJ`IKDo7mn$0H>%9a}_5Mv{Ms)dRKdZwZrcXJX*eLChja|DHz zUFFgUJGLi~4$|+FZNFiK#R*q9>)Y%iuBKFulr~~WpP9Z|UsN1CJb2O!xrsru*`$A0 zmy!yy0IaBpou>-pRiaW%vRhdj00uP(e>7KvwLFfv7g#U0{$@~ARY6AIYh}j7#C~=x zf+z_vD~|5sHw!5aU{>tmCaKz0me7|1N84af=U|Twyt~Jy2rga`k{w4cHQmIw7B5?< zJUrz;wL+!=Zwn)`gKnr9o^um-r?;RDmy8vHcH|ztd$;n6R4seskO{5%AoN5fR^HH zi1!ov&Ai$j5u#mj956HAD{Ztu85~A^>QnT|Y-H!AVfa}Jn4W7j%e_}_o<3HQvQKtL zr=}}eRzz<%jpMCb(H7*GM@@Jq_yI^5NYEM3F9?dc_m%eI7ERL)x!91@Bmr+##`01w z9g?Tuaoey^Rd5m1#gW)~`g@^e@jTp(1#*Qk%Kb^w#c!gJ{dRfBXebwh`Tzf^+2Jz< zioq)_V6%k0TVDT;GlS$w5t^-rCxqW*8)hXqTMIt}xNzNFS@fh@AFsGQYD>b`4YPt) zW?al3p;ReJOsYdalUm=opjA*#M>ve4@&@_V<7$Kt1>PlNg`rXrcW`^0oR8I#t+tX6Z&I1VFwCU~?R zn1vThpjn7*D9GZ7m!p3;`(phpO7a+hY?Cx6oZFlp)BtATO5;FUxI>!&t72@apl+n!Ao7*O?9R=rQ6 zUpu1WSh}8SvOH1UtRQDX)Gd6FcP&@9oU>$Y3>8wO8{G@Vv!|BA@2DmqQ8fWHrno&j zefEfSKL7;x=pGzra4=phBt?=|^O=ve66mGO-k|iE@$?0Z!)=X+MJviNF{mvMQ&$l9 z=R&Ysx$ykq!Z}5SRbO6|*zX&0IkZ_hBq*fa6ubIGu?-!&oCY^$=nfiD>*n*XAvo0v#lZ3H*4x{e^ti~i`uO&kro-~R zoDbsVQa53OCi0x2_SzGPudOf;u|Ik|{arTEM~?l=zg&xzAftkah~;N- z?zQyC+j0|p=55}$a!qj4=<}-L8rE)nVa2FX7Qt^Mryhx5-d7g2jB$j4${f`sUF@(bKb+FpHTbmONwb@&gg(`#aq_Pj~jD z=X8W5Ot$9P8X{cvw1`=GnF*_tXujh4lWDw_sQG)U< z_r}XxYThf=PEc30QP9qqNjGUMmdED4?5T7}>oGp3C0holPvz{~QgW9>(Y5}PYr-ng$*rWOc+2H4<}`l+Sjkh{6arLuVk ziVhcARnYx~YVQDp9!Z_A75?DIOYnFx;E@5$;>4=&QOb?2X)M^7y?DLkPk{agaRwQK zPum0Tjn&_?o=Txx6611hy>YU`IwWChTB`p^rUslUb1~9EYuSS|k)kMGHeSJUH#cS4 zxx}GUlQLhKbq3#8RuE+W z`?Wzig)El;2}r_F8G)^10&f04t5W()Rr=go{b@Dl#S_{_`cFX{;RW5-yEmu=@m_kX z+{vZk;|K*3Abd*7C>%Rr{-H2-XX^b_xq(uaWe`KZ#Y$51h*wNZJ8%Y&)rk2~>9(?e zC%svIl3BMl6K9Mt$^oC}3c?WlXWmmgGtg59PBJF~*aGls(qvo50X=09CwjPlC#1Jn^3UiL{NxK6SRy`2;2=Ze>B+mExeZiLBUdZ8GqZO8Aqh1MBu>Ci51J_m~3_ ziJd6Ic!PKaErC?5xi#bkG4MW|lF>H|yi(_7ld+-gRG>U5^_jLI&>XX*sETrh*{cM( z_c6N!Q^E~9)LmxIDm?~fbxE;lxrpMb8sBXG5r>(bcP_$=U${*05G z8g|57O>scHI+wgbl2&jJ?!0bKyqz)v)*ZUA~2@1{<C6T^B+oHk4<& zxz;`ip1%uZZgeC-OkER#R_K1LtN4Q*9M0s5Il7NdwXFhO2L@uv2X4@L++(_FcfiJb zza**+PlqG^a@7SoGFfI_%lZ?M`#KwzeUf~Z;i4`m0JfsGh`VfqlD5;@Ptz%UYvnKRjQ9LM<)Ut>NEwhaF_*bv*SiYxW zdswkr*#6tJdRKJVoR0$BB=#ceNzS50JJg!s#9Cb0c%ctDzn zMSKvh`;)&{#B7*({AhV>=k8Fgk2dXU z<)b={DSGK1kuw1g6X8Z_yU2+)^|KCg+y^MDHh25}xDFlDeN2N1}&E z`U07f5ztJr=gevbmsC_Zo}AIO)<)wc?WXR;5E7(MK}4loexSa zR)UPUM;8H{TDSm8)_aqZLjJ$D1sq?oh0^k=!UCbZ(Y_b(_U z>H%?cZaK_+FpQaK*?cthB27I^$^SN=?kX}1n2=?vr%+*ekw?^r>lDMY0X zHF3YPE-@0UPEpl}Hk&J0@rVmy2e>ww$zc~yJwn9|G}6H78>R9pY!DOoxLmmVpYUuq zUFBZo`*i7-!nfqw0&zqw`D<^{jIiW>n=9izQ?F;=*BNB!Y5@ATO4J~K4YBJwgc-n#Wx0dsW7KPZ zC&|9C!4u3;)v+krcYt^vyrcJV(fMC)#>?g&hEF}1QOCC;FhQq>)NkW zIU4QXosTh9C8X(iocgmXpFJ1W)5`qcd<1s?;y9oX&x>ZhaV=08U44pZKq5t=jw19# z6OKsR+}1jcDpsf#>oy=6xw*yX!oPp*zI-Zs32y>#qZd`oexEKIB2hjRF<_(L=H9v} zE3q?a(9p1b(bcK4OT?E~4JeRa#r|xBvE2@pX~5ZibgV!-u?<;K!-6Ev^d4W0VP2us zA?w~re;qI*9*7SFpBl+VXyNs)O2-Gciv6ZF71;A4-;_@?+m}^|?)d3IOzgosF2q7g zUzqE2#iRbgN6pOmgDi{btYmN^UAwj!<=46@DUNnA2_rE#CrQ`t{B*X~4y2Ri3-%~L@i1?bO zdz|1?y+ivCl~EC8_RkE=qK*ibP76!+qSJa#e-hjP!EEkRPd?+mSPygVHq984STss{I!S?SO3|D7a2uJ9q zcCO0X(^lrY2*}B{JcX8*sSPsozy4(#g7D1H=Rf~mZ%$9JB>#2rIAzoS%BAZ@EZEbs zq3f^@kPY#Zd0?^W@19pbUG@yP|8D!lSZIHF-78XMD72DTG_m-zy-xC+Fr-Xhkx%$E3mQsj0@d-nvE~VzpXzp z+h`yvhBgwPdzgWig|)og1v3$zsI`yhkw1#=dYnFTyziONJf5~X$fYZs(GzmHC(LzL z5bQpJEZl3$NX-~;jTwtAl#kdJ%1dnfGG2Q-$6Ucds=|*XEkQvmDynPht@PVI4gFUY z4Mm4F+=B1n6OK^dUJz}BN32V!R!p5bA4)s5Sf9}Z(dz>`x&wyJ*)iL}`ii6NW-VK5 zR|>EEg;9lNBY!pHE&w<3qXiIxIy~s2TgiI!@*k_a;+k|ET`qK5zi^El!ddQGSkrp4 z#h*;dZKk~ZZ-H6gTVZ$)-Hhvfd5dP!W9Lp?zI|~Me7tioh49l{;EV3JjUOBYwrcqJ zfcnC}CFfZ~f@QxVg%w5Zkhq&$4)*Yyh`A#i_4SRhtjY5BCxz!Pmic9yy{~A@3yW=A z6BsM<>`KoOa(TjWtc;&Clt0KUXVcz`aAz>#IftA~aze)y|4$FKCT<|9qu! zbZV1J_9=6f{O$*5`_Mvj zNkn_$BhrynP2jAV2gxo}gmv8E7>!9$5WBZJN$wSD+6eJIwm63uE@#M;? z1d)ppR^pmOpa}c=rtIhx-ZNo_g~CN29^pQ}*2QN3(>SvG2Tw(-QNu&fqSS zBT7q@p@S|q{e-@8t&loMPm9=rU&xm3e6`7s@g^+1g0D~KWS`bA*0ABzuu?m(0IDub zT@2@&l-qx?6}|Z=Zk-2opKBG>gM#&TAIn*5%WLM0y>qXU%I8;NezelrL50EBbI^uz z6NK^)m#MCBSw}MyN+^{%*NXJBSIHzU=A<2_X2Cnp9)izky(dnUv9Y`;_oi&t<$8%z zuOkHn7DoAhx)BfPfYW$+J={yuV#eR-Lg2aUU=N}wX9-IswZ;ur^O!|_C3p&_T=83_ z+$$ILSYHo^ReKWpjfNplrD`#GArLChRbWj3X~9E`+v-!+4neda~V~ye@mXwV9sB*kA>sRUdpHYTSdY{q9LY`R~g-}r*fd|yjPD+A`{b<)p z4E<&o{+)&=s7$uOc@>fHvA^`Ce-)`18q9pHp>wv2nHp!@;#H)P+_=BgWKh^nji1`7 z`pV*2{0HdFE^mE{RweWq%1J_UGH~XyPGs76QpzB@#>~VX)_xPtaP+`-LqFGxxxTlf zu)FmI>G#T2f2T_Qc9dc@euknNkSkF49Ty+1R`dh4)uGzzn(FW3wt!YU|7*(kjJ6g# z@pU*;xj;+uWfrbjEu?_id&wqC@l?14FkW*2-&6367`$0EoXHe0I$O{`?;iL8zB1u7 zpfNN|uiI@_bf>;-i>q&<3us2emWIUm*s5IJcs%6UH&WVZvblgBhDZXK%dabf=*7DB&_S)WxrS*8;Uvi zC5hbIrNSI!lBMAJd%|qm!H;2sd-JPy5DtbXWM|W@>YwkKe-!k?Yb$@gvsJEHmL9jy zBkX&&W0wKV=i=H1>i&HP>0RBiD;~_+o!yw1vi$2z-bT&9sN~HQbnRduYn{4_a=eC} zje2r$-g?6;!p3PPevMEBol-7UIRi_O;KeQNE^M`!o<4JFd^25zjc!-!r~6z*9$L)bW~b zxPG5hZQ;!HP4TZ6xV0P`3IA)&>lIs+%@Si5lOs=d41Oi9IC1=mr^3X)9(G$41S85b zO1{Y)nNe9N^wjgX640H|=yV_iF0VXatT)x&!{aL2Gn5f7B}{tyCqiD8vbzWINR&yi z_8vJ>8z~A(Es`!NRQ`o|J>l;bz%(p~MT{jhICm6v zAK>6YLg54q!`uyNZEC6{%Hz~68q|R*2Vv3I*Wh{qK;Go?R*%HpHBC>HNVf_TfZ51O zp4)(^Y~eL;N28=RBC@rj@YcFki$AAHG^jzCvRx6Y>=*0_349 zwzsY6Fg5HANP-|^jy$}GBThww&`8dQ&lL?|_quLyUrh{FDWrUhaD;wH#hJLKav z)^io+z_;~xLbo;M_3_hzc?#&rXluw<`wwaoE(zTM>gNNX_3OU{L!c=BKgAyCv5afA^PhyNX`gR^?V)T0ie(^HO*7m=!p_#k8<^j z-C@}y^$Tb$V0ZmhFM>`t#(2xGia;|(c1w25qqr)6=HXh_*Gx528U&vC-rxuaz7!P1 zb5+o3-3TmXhmd@ZS?)!J%w;E1c>k71tN^ODvS#XWlUBy+=wOZCf%XonMVo(+@xQgf zQ$jv4&e|K^3hNes7{y!btfy&fM_-OxJ~;nft47cjnBUg{4Q8y(wmOz(u3##Qk{tGB z{BeFn8TR$Uf-L+q9G#nsEA*1aPoo-QzG6ItvuldVJMe|&gS5@ zyDpHynsR(x+nhO18vT}cz)k~sNmv4B2LzOO1Mhv9a@*FN2dQ}nfkoqE z>f6Q$++)nxVI0hLG(zBkeL3pA2pC_q_>{J^;K34(eou1J9!-~fV2eVZ?E@~3`MxqC=E4O++p$Pl z5cEFKCE--oR;<+z?8L6T^my<2M#b|EL>!iz3qw@X+BuQ;UXw z_5A=(5#4kuGf$(Q3tQjk;_#=S?nxUjB}U%z^-S>?Mxj^`bu)DEC$lTWA~+$=95uJL zr9TPxxpL)HZvgSS?dL-`>N`Qw0 zGUD>&OAoz|S6teOdq}NU9eh@ z?4QIo$WmVN8V&L|fIsobnEoE78OHO`>j>)aaT)%biYmzob4J?P;gcXgRA_ez)#@{c zfyzvwJp^n78xZHAl@8+g-((CXrMac` z8UNN0c@JnYoOWo4)u<*LUv~@rx`IZqPh{#c_^`2sSu@Q~UaF$t@~q_Waz{T2uUg zIaVP8cn0#S%8l-b$^(b0kk%YWOF6pxq4~~(G?pfyUl`W4_Dc|+UbS|h-VRNWO}W2{ z>FN&PJ)erS)x-2=TXM2gRptqs=}v_6<*;mFWIflge;^-#FMNKa% z#Hp|Xa~8VRSirBPp7x=D?gO%-co{oP;Utej=VD)%pP?%PMNGeu!U$S~R%h|MDykH%cFtY+9@yI)=Z_Wi5ITEurATd` zR40{B69ppX%^b^L>{2*GXE1*5$klnr+W0wwoEP`o{xPRQtLnnr_-o*dq=don<;^70 z0ZLh>C@cR@&?)Ar;$1-cc>irOcTQojFVYu9l!qxnGqw?_uP5>h7|%PUqo5YR}WWL6juF)Quq1k{1{g_ zn(8#z+Q6y?MtTn6W>|Os*=dFm^cZ**1>OWf5#~4S`(IuGI?oc%fAV7+->q(EN`@Lr1AS<@Gwvh-O;5dJgl7zf*T5znupaq=LD zUOhs9+CKloru>^TI+&e&t1=9{Bpc?CAZwX$+Myy-^5k{nk2>^ETjK}-8eGg!rp_FH zBOp(1PR58;qekx_{6#4Z_k`G0aFP$Bc*BK5+#F|t%9s4Zb)KN0J8OEk1Gf>`2GR5^ z1Ri_>(kcL<$PR!{H&-+>AD3cgj$47EXut`KP~ATrHy@Bi{$Jv|BFcuu99yJ(kJH^;<>~pJ#dhXSc2KW0WkYuG zu;CTvJb)0V8zU!5&asCa&2Xlt`73ol?;G|FQf~AcEHP&)`-wqGKV^U7!}+H@2*twa zQTU0Tq3u)TshZM z#`0BsNkl`LKDg-!+WCXI5E9r8A%S~h01w;J-F!7Jp`~lGovQxnl`Rp*Y@r-o{t8H{ zL>}gF6{NsApE?L>z?c1GoUc3VAfgy?PLje+8uqQV@Pf?7h1nPsPNVwf zhP-8$%BqowkiGPV%H^$fs-Q4DKM6TtH%iXY>eCCBK_2B7D{pgcJis|g;5uLW2D^l+ zz*~egYKo_hZAJV(eNf`pmho0^!Mk&#P9!a3&cpc@9M$J!j;}h!`JEVX9HL&o zjOVkL8F>>0016j6Ur9mcu>$t{@|4-a84Y$8{i;&cj!R837HdwM$e-(k@uAUy6G$-T>Y2mRI~sK z)lU#zD~|uAyI2IHrlTSjX3M~_;=toVe`L#c&0s>*8GzBmIsZQx9krRbRyH06Sl7G| zWy$XC=<%)V(E!nD@(c3U8lzn#*V%@5c^b7XcO;Lggx|)WiojzE3t84(!zB&i!86ir z3YJp7FHA@BL*@Gp?KVf@_tNJ5r4~VNrfJ`F(B5B8ULLNa$VmjKo4R^CUi|kLkzw9 z>7^rx%v+h}J9~$qVgGLmgv{N53lA*q zm>qv1=Akga?#U0mNzMs8x7+kTXkS*-s!UNg4&_4Py73fX^#+WO5)kY`&FRTJ=N38%zTdHDom7ktI$bn!6O zis^BSfTV8hMn@q9xPzT8l%Ugf)ccU@DP9)TzZ1zN%XkJ;%(XnZPgkmn=gPNqk-pc) zl`!8+#4+B?s9G+d<;2iYB3Ox}=<_-urb-zBj7dADQojT;Ix8`A4o*w~A`U+U zz1DD?C(49VoY)YfCvKR_+`BI2XKaxSmqC<=K%^cjN$IB7|GfBtdncNb)2|}_5XsYw z9Y!?W*6sp`u>Gczc1snv_JOw%w82kW^x0_tBu;;Td=1cX(jLCtY_dKGq7yTN)&Zta z`uQiRE`qA!$pW1FH@<2@G0PODnP`<$t&XX!HS~(mK)E_0P|=G#`-OlCWZV)ZK2-)l z-%`xm4a=kr%6GH|2~uA{B%P<*8vgt2KT9%=&XyERvv#xlH$L{(*Ag${F3=J!Un;W& zW0#s%I96LKZFdjWh7u}Ow`O%Ggno&e*y>jvVXFFnguhkn4*zZ+zKj>#WDoAh($@vE zdgUcC_&3$Bae>ilxMpp!mh_p*t2j9YjAciW-)M>g==WOqov1qV4B zLlpDf$>Qp<@ZgEHI7Y+8maUY4xUAGLWP#06W-f1KNSa+^9)=Ua{nW5AJNvg{Z~@G6 zry1aaMR+T?vC8D?66X1(P9MZoLiq1P$>~jE$wNTffrh$UQm=@HW5P=LR&WdF&D|pf zbD>+XgycC@Cx#=@wJuNf?`BkWtbl#3uhymieG6p51>i|IC%Bh!*<4{gDk&;({+2uSvbj}nmU+f;T#w`AOch6A?x=|FqfDXMZku<){~c8Z+q0TJ)3{egA0Z*rp-*zPA9 z8ZPYU`{eqFCZ%adrnwih7rPf-Sk|!KPHOmQcTMy_`(GbXCuX(TU3pXKZiKrMdU=cW z>sw0~PaciP!kyi``r$dpofXTpGxl8?J^YF$y)PfR`L-mW@JVvDrlI-SzqZ>xe6XN@ zGP>r&&-aPj;=3(i8wzcTdyu0~bH>)2GsgZ3&qVnhT36XP12uuXGkHZh(0=AK0lPJ} zKk)4SkGVer*FUY<|7HT|@NT$Y++-80tz8~jgO5h9G5gTvgGA2beY@RB!-%wqULWVw z7$fOXx9lFRr)kVhOZmLl|C>*uLQyeUS2=l18oDd+)X~G$n#&_4Hw;fBOulYgZUDEa zbdBF^4DWee7~iNr%Z2^d!6>?#)WW;s(f5Fo4c68VEYxhyNsb@n4D4x4enXDvnw4UI z2yZysaP~Q=X;M_0@6coaE*cv(-k51v8FY44#_N5R4HY-$tWG<7xOKT6GU7$>8ne-w zr&r;L)voC=Ax#VtB zb3^pN?M2^H9vKZ?4qRtFym5og;SREwi)Z47ifzy1(rwK)SZ~_9Kkw4BLm~bBnn%ya zM42A$JZ51veD&!qby*^skV&z)pVr30a@$Cs3N2f7}a=*e)` zbv$KvC)+Enn7?QJT4C_6;@6!ya~9pZcPK_9{C9^cd*nICA$(F@X>^5&{>jP=RPnPJ z*#z<;e&6cv7b^UCUfv?DH&Kgbr)vzYu2DyBN^=)XFhlqoUiJ@ey`it0KWnt#`;Ms& zD(+CkEVU=g_8A^yPK}WzX=FL%?z4FglDJT~cgt z)BPkarZA@IVHacnS@Mg-rOWk<#m3n9P|$|4{><64Yd#b);hAx7ew;lrciU{~v-8AV zFNp5ltLsy`hZ3$AyEWfmmv3lyD8)~I|GZbPUM&jSte)w-w&l{U;p=9r%^x+Km$C4+ z3k;rSesZ~l@-H2TIsf6}lEXbG*G^m~p|6P+3p>NRHdmi}4buug=KIGXd5)~_ zmq@Kc{H>zdS2A}kAK7yrdyo(kF?ej>i~W=KdDWW4k?QtM-_e^=et*vQdgZFid^De$ zYrfIXb-Zf(%s6&a(fesY( zdCr^MSs(?{uDx2yxcu%mzn5y@aWT%@J|T@`znoe7?5Xtx#^Cf&MzJ$kij=DA32PZl zk6L$O)2mO8c;nVAE7UGi&&Ew=c)L==1F8P>WLB4N37_${dzW~F$)SE}V^qmdVnCZ4 z6%tH^RGD#=yAl&d%0rp)|O-8&`{JH?^v#gXA5qf_%-&!v1AlZJ+qBPKQ3UE)pjP9~KYOz+a8 zOOs=hCN?>cOJjP?6bn5_XiKM!w<#mO>`zTfi>w9MB5!KxY2r_X$eQ+ZNO+@EYs=9% z$)ty}(%MNqObkj};4ARGeR{jNXI1E*ORR>!X1wvtZu`@3YJ0ye6Z*VXe*aV{MsliO z@_1r+yU+teI<1c|YQ)Va#ZlN}OSjU)840be>zFyTRns01j}47XY8&X^+n&e_!!?9B z+J{UojwgjkS+`zDXVEq5L$vuHkVPrN3jB;wj^isgCP>q^eR4!*eL*j)X8RZBP0W zqiLbW-k3D7lUZy3&awVor+NCj#r$@d-_!MXG&vF+s@=X6J}E_3 z_-s-*@q}q7#}d-un`YxQb*}AW;$95);-IDRRuUv+R^5`gBNahYF5J*wm{MWzGS$Eq zOuc4LCHtj&;h&ha7;l$J4enBoI=E|x`Q2`QwW*VacVfqGlkehXq$M(xP7aJdPYUg6 z`Xf9tos>!==nM4rSoooTB&3+wkqSfzuwbbQ25{mTpW&X{z)A(w8LrCw6R4sLfIU zM+zsS!&8N0zyE5VmNG5m$UJKg^+~^=hJvY_5Vq$zdU>(N@J`D>2HCx)ei zEOzdctNoTnkBhI8JhL3fa5j5oU_^?F9w0DG=3SFNoOVX#_5?+8L8Al z^+uFq$!LTW(_O=oJgt~MsntW9WIJqRy1nh&6Io$PmKpC%s;g7il@J=>`6ENA)4bFc zZ|F2{NPat{Qz&&UDJ`5*IvHK?#AiqcaZ>wvl5WYep)+KFVL;K6^z=*jVSFr`nKZvU z#h6TbWYUVA($5h_J!!>G)5)DQzcS2q$(XplNnh?%N}m0kLgvXX*dzcwbr?syv z-MEo~#CXPUy>+}rn3yNJO^PJ92jwx*dz$`i@zbe&20yLlPzu@Dq$j^-kVD6PhDOr6 zq)rHnH_d{nN!qj1nkYG}<=wPYnUwT(JnN=+Nw!&DGdZH88j~X_JBT+W-MroMPeWOQ zWN1J-mokJR2io|_Pya5>F(v%+qh)JIyrVai7#$wjZ8^|RbEVw~$w?!JeYsr@vfH)v zZBI)_WWWg3B_t@V!x2VGwuoyuO|wVj7GIvG?BDhU?DfLAB|eh>CV`Sw3F%MjK%D8{ z`J#W93ME{D^ zlB|q`2;=nz2F1gD#G)S>oFO0wXR zT{3cIjgX#)^uL7mrN<=5urMfLZNp>YAM_b1k=EH2pY#vueXXw&pALNWOOcX3cl)TK zC{1VksENOQ)EH&Oq|_O6u3n*iRI**NZ&b;<`XB$)Bv3L=Q`SD{pP2xXbu!o_IqGMi zIoxTo(GEBcnd5QuJ7Rt%1C1h)M!iViJ~=W5iZm9gZ#9qD+N5q*lV%*LMZ^b2nup2l z8DTA^0WgJAo!dmxV7)Uvl^n4oJai>7D7{n}evlTyzi#T1L0r9=My-A1jOAu>s3U65 zMZqZ{qZpEBQZ2IMIvzZ^Q=FJKsFFkK&P+FL+?yOzu;ilZ9y7n&eP6j#7_ksDDZiPd zDIT`+?UXk}q@d{YCSQiUVNV8@&NxH5yE+ZKb5Nm}U1CU3= zCum*J(%aUqCz(^)PVw5sIZHRs+bLxHbT2t*246K1(vOwlO;sX&iUDD@(!tQ4o*pTN zBD63F+f|jmkla$zFLheHn2`VEu92N1qIE}7rPN~;wsFxTrOc)%=@SN9UwOaH522sJ z$fS>|BhZt>Ql`hHdv6Oty9|H}H?ZU00^YFnKxLRwXF50&P^1LAbUPChGWuH}8saF$8B233^ikZgJuvN%j@GqXMy7{8HxukF zZIc5#bZQy{8M(7w!w+C;i$zP)5b!!R$$Fr7lIl+uD`&d;Bs+B5)3mNzX<~q8>{(QjT|; zYV9+vgmgQ^PZ<4vWZG^>jW>ftVxZyclJPqx#siF{lOsdhqx}Stq0dq6x3-&>wOzVM zqE*V4X-~w>%b8A1NrkZ0$2O$uE42OBc^Mr+PRqdWX{So{meKIwk`BDdO)~%|QkrR< zPLlyx6*AtHl#ytuDVjp5(vnFs!kXDVDhZs}-EY9KN%7MlnWUDPyt3%Yp=7)1wMyYp zLsX$?Gv!7i(&Dq*^P79=zE2qTp#Dq;fkc%VNim7lVjw^bC2h)0Gh{rj8I%>bp*3Dg zf|3bIV{)f@5UCmx#SR%}*BL%Li!?lD{GgnxG3t-nO(hl*A)d(AXKCZKoe{R3O}*F{ zZu&=(trN;y#V?F-Y)4fr;~dW#DNm#l!xN^y>p}kxVRSmrqmdO|NSMBMa6w9F_=y3T z6EU2(U09~1Pnbl;ICbF#Q{uqnSNXnnjMb<0+iQih;D$7&;;_9@lSfhr)P3uSvdLK) zwbeYep`}F^RI<`6r+u9IH0Wgu<=84E9;x==6XmV`8G(u(w&ayK@WNc;xyhocalO<~Lpd};bNGpO) z(+&I6QD*ShJugh*=oHHEfMldUI8`{enIqkFX=v;mLf|coXLB!_HgP=R&;RMLbyjMg zr;S=hCcMV8dL$N_3&Yagk&-?nBa_mj(5fQzbaLt@B4cH z#5#NbM2s>0LK*Hy_M2;FW-w^1tm~K2nVm}NwI#L9ZE`%Anvszs$wN`RORV!J{e@Gt zrw5V=X#tF|jz;`|bURJC5{I9a?z!cWyQGu|?U(+9vPaX)lS%IWT{0Ld8LOiKdMG^( ze}KcD2F-}MYuvw#E6Oqhzbp==6QklyiDI1ll~(%F8sjM(iG1|Wk96XVjI z5qEz{;o|nhB|6KlliqS<#w(S8Xn)h6CsW;q=M?5#q-9L(4qq~<)+r|8^xF6sLTpt+^<^vfq$RS(q>;1X%c>bdieKp*_>i zrP4&JNdtw*B7q8h(82{+Vg%EHZ#?g*QawMBD zJrVJiwwbZfSXL*MP)6YFe7$WmeEOlyhBh>?5jEcE@&~50>i5gIiHeU=I!VvKKAc)4Z2?s}O~i%o2ZO^UJU@GxchxtNUd> zOGXd(J_E#p&uMASk@h&qc zCRAKTGG$0}mp3@+4es=$XD`)5Mu62%N=&Tvk+l~(SlVfZU4^xp#Vp#%GdQHTrxbpv zIMUTLix3RD>Z=+j|U>?&Wd(B5Fkr78MW8H@SD$SP0)3(6U#0oUp5fC)B%=XKw zE@>-db&KKs?Urh(?Y0Y;m^e*Ow&+iX{^-zHt8q1(1B8PE0})tAexqHB9uY$ux%wOiI`~@pE&d#ZqT+932faP{QDe-onIs#&bEO3Nv8@s$2 z7gTH$;wGwTH{A)bJi8Er5bBcoU?3sLm!M87$h7d3CC}#bZ62aojwHx*m1eh$Py8n~ z%Zt#+X-ay+5RFZNZX}>!CLpqyLY6PtAGu2a614bKcX8nYc%PlfQZ6VC|7LjCG3-ce zDf9YSWJn`d`w$;cfV~J}=E66n(GBlX!@mp}6Uts}W*3%_9<9yXnE$IoyWvXzA`ih{3Mkxrba)yK>hFbJ7+-Sk@}fFmnkc z4JBgnjJRF63Q>nx?8-8n-wc>r#&`q^-y}~+3S}w5%4Ka}FF0}LXm^-^5w(g!ayii- zi#jdM1TC=0zSbXyfROb|nqC;g;>yLz8%b%h0-6r~WwiaP9Y0!DN*iTt6y*}9Rxe&$ zw?Y}i>BtjOz|){gZ*cb(rXR%ftMdno7&P@xc6J=p7{-)y$PbM4azhc%TMn;1EMvb^K6b!`9@Kw3GHD zmg8|mA(U|pd&bd2N(~sf_qn)A>KbO;l6LPJN9K*Nh(0UwXci5RMtl|o%dfp<%W*(f zjzPS_5(~0>YL2N3zcvQ0l0kKMY~s8qlL<6mCOpBQnXz75zX(fX8%KZ&@mWaYrdRui zf)*JTssJUhKQl_J|*Fxu8 z;rPcf+=TPOHgEFye1GM%#O2x|M&To(btKRMiOJK@Ffz0$;gPbDxP0P9t$Q|$CPN+o z)?U_I&hhyvRi)a32=BL1@zmJT5!vuL)}lDtB^ig#JB%+bpGJHc-IN3BDZDSq#@^7U zGKes?A1x@n2|N`>c}x>aPTC1u(411!dU|1DBBqyy>l9tUf(ZLAc%mit)1_(sv5mw6 zg8)B;brSrlm(DWsSdue_{>^*DGKM>83Dhv1(YaXO5F%mWh^ zXA<)a{E4NBCIVBt+lwVEi^&kBz{MdDD0}VH+~VZ4s^5Sd|v!v29~p3 zU86_!R%;!0x>7l7T#U=1QnVK_oj?&tN+E+|lh*(Va~LCXxX3HSG;z0_e-Q)Oy_lJs zbrgi`vY-o~CRt=wUU|A}5Dv@WTbVE`ChUM3;v_Kwu>so%=b+BJAnd@Dt-of>FB;VO zQMFnKFg8CnJ-fb$6^&M|nQTlYv)-#eXDF%JVd&sF^&@P;+EN}*j5a2bnK;|*#~zuX z%>m2}**SAydTb60vR5tcjr8)wtkCX}y&KK4O*k`jVeegPUIC<+i}^l>Kw9R2q7P+0 zuxh?NMi`nok4AJIEV-0SnSEZq(7$+*Q(_>v0+D@+&wFJ~#<>Q|hx9wobF*<%Q*MI& zFl@2h7ky-4F}B(`gP;i8mOF)X;JsNRxgt)HRbVvUyubt)izkAa(s}A?iQg!Ccx{<| zab168&o0%Bba9lO2TP(t(JLV^5gIjJ|2(yiV-C)J%CZbB{)9aZQTEr}iG5cVZiQ?^ zIj@km=sM&osz+|{SrMr~`cvy&G31OHCi`uGzh*vF@abTbYk)RH>24q@ z%C{A$d~H*|$|^HDY?U1qX%ZsoDgt|1<~g&KsQ}$C21Ayr^l_m^LQF@tg;J>^ux~5| zB@>gqRnVdkiI+G7t)&*)M?x!b@vn^JpiKEg4-Sx|x$u%_&DjR1qc z6wo=ZW&q@3ix%WxOiW#XTIO3N3|2SBDV|&rtrzE^-B;#+VV|))i3-7%#06V~j$3LO zlpBz0NG3%uNleRvmc)d5$}Cgs+X58ThTLPN2)YR36`e0WWNA_~w`8Of6Y^Qk?Gdq0 zGcp>Q+o>ZKjEdJw$h+KqlDqjUu~tk8%8=rtY?dOU7bgo7KDLTU0YWylGCBf=(@G<> z`K~I)sz`pk>E@MP|3N1JgGMXt)#$6QN$jQ=do<`{#FEGiX@R&`hSK-_=fD)A?6x9 zR8?lkJQP>HhMY0t;q)V8NhVng4#!02x^T7L?P0^OONFPfx{(tU%kF|eI=RzIYzwH0 znnDu^oLTIUv*au^HKp(ZF=U9iKWtXKPcx7@=OyyR!k<7N!qlQ;OuJxvj;(emd5nJ= zGa_mxy09493Uy_uZD<3bwn6L{g>5N_D-KZRfmXjmyH1C6s0yi+I-o-w#SGsSXoOmE z1zTy6b5_!6D}NwbY=o4#xMM3fP!$CB+I=DauAa{#XBlc|7(OaW*Vf@kREI;J65*`w z&jx!o?63;o_(KZqqUi9Z6o!MWc`UKmzBy!|tqQBG^4maHIYp-8lwW_Q3-ylx0a__< zs2Mkv(0NYpSyNrbpWnL?qRqXXMxw=lz*=Y>G3Ba^Do`~NRo-=FyTYwo!E^u04&HVY zqzNhvD@gG>6dnCoHh<|v7a33x8FH$?uAyQIj0M}X0;`m&vB$_PR`#tbyb5_a=q zCpk0_)V{D}ERi6atE2Mn(-^Oq@ZcV7_$+M_l>*qAa`S!)b53UT?Y z$F?t%5Gw1gRm9t&cMD2q1)ZZepO_EYi4E$8d7;7)(*R)da!%);GDYLP-KRjIdnFeK zvvU3uW`l-NA#BfB=F1SYLd4?qTR?3N-_% zs4&;Ua9$Yvh#_f+Sft;|8+HSr?Ya>Z&m8Es0W1~W1o8;`7Kg9X2yd!pdESV5Y`d9TZWeDKlj z=7JEd!_dz(1HpbZV%b`!!Zb@16$N6Z;cx3+!N3jMkc#+6!3Gf-h$-y`!OAvPc-+zq z&W0_66{HIk|A^c}Aq*H$zk`7*^zEZH*CQU9!cb0n@eoe3{9|va_c^0)-EN@+j>ZLX zAkX}VrH1Scx_Dx@W@^Oqx__R_?mCebmyNON^?;}wPQxfZo4PoujsE?qod=Okr zLZQd&9ju}3jlQ-pFH4qW#b#kbWg!6<*6=$STtR(pks9@UpAjT3O=9>9Ub0;Rk_YqE z)exa=1?kcO>I0S94%H?C5U0$Ml2V~MPTwJ)fH)psV8J%BTIl7h^XFAqmz!W#ucY#t zRYFY{w#0&P9&)$%r`hw&s*Ef{pyM%IImf6j%YwYvks$`e?7UI`6?in+g+c5K4^+X> zjMhQ$psdH0MuyR9jC-P3SkN*K5$Z37{Q2A~p^IP{8Ox|;XqWxh!{$Pl7$D~&VyfJp z!Vbhy83Bq%D18FUS`PaF_U?N@dxl9q>lraN=A%-V*(k@bh>143I1T$vqIsYUGIM2L zHV-NEg34q*&4ba@{S|m}6`{O(p@OI-ZGcTms(90}5Gt99u6*e5(a?CJ2Q_AF1MV1~ z5lvVGh*>0MdQD%%(ugm(2}QaPf5>?#6gJ@InF)$&fP!3z=!`&XWG+Dzdsnp&M2>;? zXh9ccp34YGUAGpvkzF$}GQPsUVZv;&V^t1H8IE*V*;adVkAx@(aR!3uB526Hz|4d5 z$lG0Ao4WoYDs_zmN6MQ?Yj8W;ykvaijxp8V+7LI(<9xzQr;}8i2cR7D4${?=FcbXt zv%p<}h&FFhz6fHT8Ay+rDXET(2S>0~OeU1ZorL#hxj#8g}tw=_A;;__DM zjmF z{R<7FT_b+Ft9?ii4jOQ6j((j&9-U_hsSJDf8nb_XJ#h-df%w%m8&C)b>wc?r#?zgjG?Cd?VxoU;kfPh0t#A)FXa8MlgSc4}vxEMm6;@$5?+PPa~TrW^vQ#kr$UrthA`T}QAN(%WJtb#e)x#M+2vv7nx>)gD4op~uk06B;K(1@H>uq2g>M$~d$vx&>ni3+FQB zvtEn2gbr9#JH_jVX$^U0MPwa{n@>^XX9_Sc8;LzQFP!HkI=cw77MV?|2e=Uf&K}f+ z*buxSPN_Vg$0|zq4eFSVSw2hoyXT~B4Lqndtn45T`O9rd67;EH9f4LFG0&E!rHPd; z$M9`jh|P!sv|ML?L{Uo9Scwb~c@?B(-rIW>#Lc3mG~Oo7$mE+Hx?CAD?gM%9HIljpmO&F7$*u!x}L%(p2xqaHEq*@=<-lyHc~MLDg`2?K>8usyIsQmO&sb}w%F2rp-45m>@&V6X>q zuIMm`oRE#ErnqsT8sA@B39B7nuo?jDDWW1W#v=h{jq|I_r`?nR+5ulPp($6)rF*v_ zOS5p;Iof(%OV5YGkST-fZG&$8q|l~7EY2#m!bTPJyRF(q1Ec8Hy`uCjI}gZTH+{mQ zLwqn$7;W|ef!n>+xV4(l3s^Ei$6z!a4Z(_KdX<@P-MN-Ic$76QpX5WrMtg*_G751e z!f!l+1>^XPZXC(Ku~X2PBFF@;#Y3(Qn{>GBHUQiF6>62WA%#xUNgi+5VqSzH8spKM zof7t1MIIv^E;iQwocWa+Xmf|1a$+LDSP0REUffTsNt~?v9Klf@$j3tXB088 zG6IIu;6#BhkktJy9QADaBVlaqXk~QGBpU-0ob5kSM=&O6Of8*_kme&o+@O*@o~8D$ zamkjZtOE>}Diew9jiZ!#$5hw&P<`dmCg}hL=~qj&{FXRRpXiZEt>Or4H#0erO=mGt zgt_cH7nk8s$s-_4^txmM?O0#PYu+Gl;u-Hc_RTUg`QcTY7{X==%Qxz= zDYB6+c}{EbFBlUoiQ|`^>@D9EutmsidvdMszOYye6*h&jSx{KoH0cs2oa17M|MBMe2*X@!8j+pfXz`A(lNnSpfMs& zE=mW*atOPW5^59)ckwg^hBG+uXo|6hmff)pK%~-y>70L+49~S<3O85^@}KVMA&`sxM4J127$? z46_hTt!M;gYtRTvMcxQn=+O+zGQldvsB+Fz1#ie#pbdGudhPt33YNnYt&y26AR6Ux z+*t$$W7Wy+<1dQ8?CQ2#a!hBwtAH9Mw-ZinS7B z$fUy*pzf&}mYFR&r-o&Vk`_$cI|5d9TAk3_noDoM%(=GH!G20l)CPtiMKGp8-#Oc3 zg`S|uITdTki98i8qDtMJV=niHa_b2c*8&7l5RKX2# z6*|Zwwu*%+VWeET%trf?D-{xA#>Zgo=39VO@)qFuh`YGA`ev0=Ov>{T(e+r%vdlca zN|1m}=PrW5(b-8st7)XCO)Ph6L2G(F&%YX=xyAIW0Z5Zz60~yQ=ba1I;#g}1O(WL1 zK|DCfrAZq>r89lRO_?A^=yLz#m@Lm*VIQ!sAiDja5s+O=*7G_85s!vwf|qn<-6@6q z%{W_El!V#TB1;l5r^w5s@ZJr8Z{5*%5?01ZhIXi(fZJ8(bHn^W4!092rpm<0gefYB z4{EeS?eDXuE{i6pRspn=#Vb|8Lb{X!sFH=2Q`-dcUOi!>Qk%fQ8G@=mr3~qL&`PM( zo1HL-%oVgfD{woUtH8uItw^o~eTY3hhLD)8=*IH2!T=@oB6o>$#7EF3fS+9 zLBr0&qQC2`GvLBS_pmJ8^KtPDW3w}xyhVp+jxAr1_$sb<>1MyK(6)i0=y6|vVnvdU zLr7r}9aOAoq6-i(Y1LjZ_#bb-xecVCuyOFeJN zLKdUSteI*{pA=`o7~d)oI^@tb=}EDyQ?|grN~Gz4xkmneah)roxS$~ijWnuIf6q;I zlxYMr%dEC|pUn7BigiSFn;=rI$TUmF>&Q|OZse;F)moh{WRuaY>TD)H5SS)+Y9F=xdji3D@)19qaXG&(dT`4B+_GaR(shGSED~glij2{Ksjs^5KSe^cZ;AL z(e9egZ;3iEor9$c_hE(K2VxD)Cot{LEQ421*Z{JOOb!N_IEP?aP=*RMMi512(f zjBl|i0oE@EXEi8NNl#3Gb8Idy@ru`T{!Vp}@O?c#ke)`ELJI>8I_OKjInG<6g;1xf zcfBAnRIYP zOy-6*{0cJ#4WIUN=22~aR9PM^FG4nCgUs_ij63HhA@&m9EaCG&Ycb>)yv-Pz=yj7X zx{x3S>ciZ>R79MYs_eF0b4#9JMLgL2=2u-phbd!Zl%v`DOgo%D!3nM(^?Xm$8X0YuQ1UZV8xiW;U+Tv7tt|(37DsjMH*-J!6c@Tsb^X zOD0QZZ9&P}Ugp%^0+q_9)Dj_bKCwH-T-Vz6v8HOeGU$3RDub(Mf0!(sB)7?KTk-gcOtwVXLNPsTG_YjcZ9B}$aC z*UfysG+FK!+r`OP$uyR39q+P!TkFh0mNH7A>A9M1IJIFI zQ96Q2E(Kq0!wZ^cp>705hi`z2m1|x*YwektiPe6_!c7o4bGdn7XPtD{J4u1>Vxn=3 z1Y_GwScJe{+6GRD84YXAC|pjsmDM7c(0_<(n^3DAj68IEPgY!%(}h*Inm2mxb3W{V z=$fdXSAb^ajrp3?O6%>IysZ^YxM5mgy-*Fe6W6x9q%Wr_w8KT0Y#Ci{1c0J|*C@8Y zjqkO{LH}C1TgH3pssT6H?4VL9(2Y=aGF1YPGvkabynub-TSf83#$^Jjp2gY5M+B-> zsnAYvZDBvoNjF~YpzBeVT4ZW_HnDH^h*Y!(;s=#07tLS?5au2xC#2j(We@G62>pWZ zSj*=MsLSQ@HvY;ne6&SS* z3OVF4vG5Gfw2l1`NElUTo0m?jnPwNl$8Lzjx0BN%DtzziuXp%h~r(?q+uwDX+Jh#C|o!W16XB;uHIpKVG@^GuBBIywyB zWl5SRwJHb=uSdAJ7TxM>XsMFO9YO!evPd+Jyg>)c?x5jqMCfRDZb5i`!~$R#g?e`} zV$jHAU+I45!O&Iaq);o^$|#++uKTSFmJ!{Zd)WFtoy^hcO2{I7l{Yh9kWieQNI-aI znip!8^+b(trVpA-m!NpV12O2gVj@#0-gJ5*U;OciVlFYz)`+>sDy#}o+?xpH5dXpdNdQ9I1s5L?3(hV^U6P+<=oOM$~aqrsQL`jLj4ZRI^oeS?FP!85w&rnHQ_t>QHVl%L5~R zlR)7uCTpf|bG6Y=s9bszx|>Rvg=W{rRzq8usu=N-icpL!uUNm(Pj^{fYO<`}Rvjg? z)*6JQfZILmum~lYJb9INrDzME@m3@*bHPcoWi)6(oVLhKCN=DZtOv`D+&xvhZ1P%& z)RZZpeeiax6X5A|H!xUaF|4tshS?F*)Ipydn>w(d948KmZJBsUIl*@8!a)d=G%5-8 z+HsQGw=SQ9_CZeyAdUW#&qa%PfRgt4<6s#xNyuB$Op9SKMl8`$U!EtNkA-S~;)r6s zE$NM5t(kbq?amwaXdtvx`f3=9lsM&PBPv5ZXG3->Dl>#)GU3b1IYvL`j79%!qPjH1 zSkuOe>M~vCL?1G+@fcfCjQNrtT|)1qzs&!5Koy(49Mn@BT)57KA8MHjbhk4d zwPAC+At1%5XM%g3vRv7ufdJZw`C-JgY0z&@BGlOM5Pxm%3!Y)-F@ zQs3!X75!RWUz0BVd6;@}j#d^Kr6C$+i;A!0FJHyP$~U;Ahv5zJtn?mxLJx zKPBMgv1th;&5!j;;AOXF3IqCX*-OJ?_EuvrKsXD5)F&ZBJ9xJQ#|xS(mVH|D8eWu%#B&q;h97;rN^B#Co~nbqVuI?3~=`8DI9H3 zI%FHCjEOtxXCa7@Z7e?+P_sH{gm-V4W7_2zuRWp)fFTeTlAV6VDf+`04InoD%16la zjAE-UsTgz$%UFcIj_Y#PE{+PB4ojG~UVC{%n`0mT>$`%okI@QW) z_2&-#c`5H*oWHKOeSUC{S{yk}A8TRnc|WvRl2MZVBG3>rgHF6d$|HJ_A0eomRHUD&Yij3T zw)q^@EJI%eQql#3pheT)@{k@%9Q4A#N`SR23&o&Rk<93wKiOBxN@3Q*F&Iyf6IcLo zgC1FQgN@CZLNY=ML73Ii%mZX*3urX;bcLU{F=Z~A6J=ZMZQ*y>+Fd}+`DJi0NlwBT zMcTNL+%@1P3+{{8%Ow(UTd^%hMt3mN9(X&qi)cv|{9LLTBwY)oFMZ0QAGBED`y%P# zSfp$(V9tCq8#mZ12vy0WlVbjLs1NpVZcqLdwWjdC{ChnQUBYHdyJZVv`os5Md#liO zgyIBP^h@XUCj?^gN_GixvpFIFBnZJF=l6CXe~tSx+iXFSuWW%q(p510z~3#*xhoK@ z#jhaIUn>jo>u!K5&o{g&nS?3o61ACD=&n&+-Ou>D#p0`!%wuG|SJAj-d3XtqpIapx zRbB)Z_$_nJ)iFCwULDTL(c;c_tE1dEyL3j{ClU0V&n`W6$#HIN^AYCM3mdB}A0r&2 zQ6=D2VJS_>3J;=WU3e1~b&OzUy+dn7FiX?2Q6t13E0`gICD1WMpSN4? zPji=>DR0O|`C|t^$77x%s*t@`Z|H~;h62t58t4QvKdZpXCBlMe>XdLJ1Jht88F=!V z2d`K5@pv<&pEoj|E#M+uvG3-gW|7Zw3ofr11@)8G_N2AFX?;&x-z!B;m#}{#1Y9BF zdNZE1hycbHn8V$R;>YPyUD1HOYqFBpwu_xc-zLYZg~R^Dl~fsrMb5|h-D+PwUlo_#$8 z3_jDCOV_#aX4omI5(F;@-4}9-2Sv_Y0+2r@1B;|pLK;6883%!R)+6X(^zrq@z@`Bx zs@VBy*AK~gCB6dPYMhI?Q3)2wv5zsJjEBZAs>>#*e&5iN-+Udoguro4_(b1EJnW=# zg_=4{w?}URl$Tz8Srt1E)B}hgehmqbNSfMI%W^zZBqlez8=`w$XK$k#FV{ypV!F+_-{(Oqq z{(MH?{+ym?#rOKr@q8TJsbv~XjmfSJ*;$F>| zXCB82D;DZ8Yz&;jGJ@d3)Q$+2J;tFpy$~_M85$gDo^MdKMx+F?ekEEbC7mWisy!=m z_mdlX=rw}AW<)$Qh=w~StD9#Wgc+TQh+LAPzR`SLSvcr?JVKuE)%AZYrl>)_!Z@fi zTQ|jk=B+t1p)k#|efRd-ok|K;LN&rV04OV`rq{1URrjk;S^mqOlN?_Cf1zENT_^b zU}=2|$aM?N4^^>dxN&&23C>jn9U&BkF^b#Bb+Z}71u6{tpuu&z7n0S_?D#Nhf6lsv z(%aH`fwSTokvJ2|m^UA5jpJs;-bc4!m;U#X)>h+Qcd_beH#>Jp{B-q16>ry;yQ|~! zTS!(C#I6%GLOoAKUx)xGU0K@Ul~Mr!XF!<0 zy@CuX4`D&XLnxF`EcslVk8ZLHfpwMJ+N8Nh&Ww|ou{5>%?b1TiP0@U8Rg^wFQBru^SCmz zSOzy4(eSnm7c$1m%Ud6a79we$ot^!!5EhcxxT0#mre*c4v&a|S%7XGYpRbj?6Ro)T zb;PY%7#{em4a?Ukxm;aqCSuVd<>ogAx&7z(iX zN;dC?u#aomH#b^DO+UsJ5>$^9!l^taViUpLT#lsO>%w>TNk2jsXcPh@&Z3ftS_XTbx7DQ(#YvjMI77o_xGKPvlz!gU?|X^j zy#c{EnHehfGdw`dk|D4LTObI@Vm1V^-gpbq?(mv-^<@-BaU^C#UT;!#ph`VIa^ zAxp=o)&;h}d_|)~CYhS9_!l=r{U=>4N!UY6%8&BApg&WqKAt$0SJy<@shOXk zNfflMRW;ax)txgb+Hhi9+N3o4);(6FnzLyiLx4g-$cirS+lgg)rEBo zcq9~Y)vT429*^en<5xIk=^+80Ue*=UN$-0igH*wYzBx!VTP6S{4KoQREGi>Cf5Qsy z0${@D5}br7D6I=Ui!$E5Wgk)yP7e!a(}lc@!;euvVmr*o*hO_S+ro;O*dA9d=!`NN zN-mK?jDvx^{E}tr*MO|(QW!%g$D2?pff#ko6n#NMvKO^f=&k|FZ>5fx4?#Zy7Dfa* zh6X#$qyb%p7Df^ib|#(e^eGm$Z)_t}YU1kMTp%Y+0*r_)Y(~H^ysEUSJM@tS#EjK^ zR#x!2`3H#+mdqXoLAZ~Q+G%0PdINyt^F79Z5v)V@IY0ZV+;a#af+huP5S9|AZ?S$7R3LUzzXJ-6AufX83Dff0 zLn9u|f%_2YMfHISlGU8<{+3yRenQA;Kk7Y!_BKI*u_MA4I5~-YSt5`1lcjU{H>WpZ z5gn>7mXHL$lqHp4`Y@^kQQ|EjmID%J&nN5FEj{G}Hd9H4Xdk%VFSJMm2$i9$7g&Gp z1=Awf>!|+WHyAl1Z_`A3dgP*N5k+#u&ukU?5$IZH2@wYq+67tIVRdxSR- z<`a8muOe2;dJdsG6dxuZW5ic3$XH7@)HexN%8)h5ZKG*D59<>IA%wut&;a$2RJl zi(5o?rZgT@M9MSeA}e93P$9oOxP5D~@7%-sJ3q}~lWE?X-B+v*d8a0$V(^#+d&-kR z(G$rTAc9Rso(+)iZq&fCQQ6)E`Fn19%^KPU)zv%*nokGM_azEm9T$^|MRLq5ZQ-1* zW~Ry$@!>_s?e{_HAHn>7a;Eag$NG ztbgwYNp7Y-BH8rVc-+%Wp%4@2XKhLjrQfQ8&oAgU!*Nd8#d_7|rYA9vc*gOWEvD$) z=sR^?m`4P$p0oD|WWP~&_U8d9AvZD>F7r;3HVN$|!~@w|mRHb)ws8(IcH{l=#q4ar|l7O+wP;{5Dko6 ze3ech_=dOiJQ;};@?|QKKD@iRcUB7&_48VIpl!76NEU{$P$9%@B2&a>Gcef{RiUsh z(%EpypPi0hX<1E;OJRMdA6Yfu6v(e;+(Jka`;>(W;f1MyY*CfKE<6o_yvGO*A$b)l z4^mmZYWmpqGP;XbT~QFK&dxWRNV{2Pp}Dh7D^#RN9S+-wXm+s0_$?g;r7_)<9gw{J zXv^8x=#uCq^@7$Lg!_VSdAA06dT}b+!y-Z~gtSLV#2skun0ECUk09e|KFUgq!DBzsRNE9g$^iJ(K)PLZk(sa;+o3c4R0wV%Nh%- zTZjce?#L06zKn^5u44qMZuju#s2D$UG#9hIN<4l|=`q51qK|@-bJF;VIpQhO9=jqY ztp{_u@1w{U8)Mks8uQS6DaUqQ5nP=l#;a% zWxxGx!cP1qa}Q<tjJ#Rj=m>g|;|)WJ5Zgt21R9?*Ovse1%`W|Fwkb7Ubx%4C3uRX*_*vNsN|}2{jJv z5U9m56{g9^VG0Tc`p`dJ2;cDV?x^m!Tbe2__vtePzco&k@k1S&LIvrGz{!F}qA*v@ z$>=+YeUuYVJ6g)X$-(l>qgQ8n1Jc*9D8{LRu>;Q__louy)j>i7U@m(@XZ+*RXr0}V zPMfV9qkf$b7t%#Z2stA(SDpLbEh-C6J=A~%m}(MB2+UQ##@9&qVSIT79Yf>5yS7tfAYN4m&Tta-7o>`xSoSfBz^nm*-I&m8K4f$hZmHxWS`qaLLJvGH#s1Oam|>FveD98HV_%iG@ImMWse4xPSEt@ zTK2R?dS7EsgYhjwjN=u3)!-)p#?hu|YbmJw!?`1FT^l4=2d!aX{^Cg{-Y;hzM+II%=HvEyLEXuGE z+-)drmsHAk;9#MPv_Q(yC=rz{oIBh@b*7Zj2c2L|D=Z;FCS!oWP8gaSP-NS&XkCvG zl<9^F!upVD(D=&IESKbS;%}KxHH5}17{;+PaL^X{5L62t9a`+TSyclXK0QiiAF?2s z6S9@WSw3yb7j%biJ}Wsh$Ae)L@Q29#XT$zUKC!3JkchhH%rDkKEc}s%RO1%y^1c^d zwOD3r?w!0&w3~6T4V$?|&S;D;z?mR)V~z10Z=Z!-8pW=&=i6{q0+%=>kAO**tOjIh zVRT#$U=BqXMgW@7Of|dP6q7Sll^Gm2ndFEZkO6^44<iOV0m$kV{B$EiD5~7yyTKlP+LVOW5??Ykn zr27Oz+HMX{9g{`FWOm*cL<+lWn>8T!05hl$y%3KlGTaK9u?xFRn#iRRGKpag;N^G= z;xjrZCT-X;Y8oP02kqD;fo#}!nl(LMz! zo^x7K8!llE4PsU8=pyMSat~sLCMJogFH3;#n2gAI$AXK&*-eaAV=9Eq8#WZVMPWE( zaXw3fj1rWENcxvtsgM`OGF28BTwAgf6Tk`Bc%|^- zuz-yh$%2BM7)>SS@)e(lQa_h1=K3z}VuMJ9sb12AE$_ z5U7gnXJYDrpTPE#zBd-(+J`-{YN14xpo5CE!dQ&9!WY@O5b`hp%wE$2+T+fZ4EhZC zqOUku+@mQo5rIndAXprR^gL3~i#E?NBl+%>z}RuqB5-~L-8)dWnWZT~8lNFHm_)vy zZ}}-aoV~%m4nbdbjp)Qr~kLOHd+tVza9^%n*qf)E#(xs9*|WL9pd zUWTc!a-{YGOQUssH)O(IxfP7vffp|7G_2kJTxi7T0zRUOZ9pj0>n}uo=MjTR3bYd9 zZe74i%VYx~+Ewp}Xr{LFWuMXQTZ~FMHb9z*>qROT1#c1CG*nwscho_h(%^**c1z8F zupCc@@ig%wc%Th}LNB1IBDp>OG;kYQ40<`ftRRxaMjCTr_VNOfPUNuNsm?d(fJv3D z5o#3*j$?U#qJpf9t!iO(FTihX1T!nP0+U)Ay)9C21tv8@`z;c;p)(tc;wrFqAv5qs z&3tCMaQ%&ZA;ly{Z9|ZzHX(}~edO2#t37vjGcudAZxK^4LMtzb6fSV~K?A?_rM|yW zeP6I)sO(F~i@O9Q1J3)aKr-4knkw;OjhSy}VhRSEYUW>*F^Y>OL~`(EcyUa66VsKO z!CsMDB-c#Z0$e5vyS7>7s6&*HYuk#+v{oec>}0P4OXozklWajs0ooJmbb&WopfF|S znvi^bPuwyr+9?Y2Ob>6|)u;ivNekGx$w)rDtjDZciPjXQ+LTmYnKW@qOT{&{jWK98%j^!qfJ{!6k1T_M9K znW7>t?q+>U@Hf_*gfW%9S+l5>G`;{gq}^s}l~B=Bv!4koD!**SRLG@*&?_>vnuBI) zaxH?aErG&Pg=(VY)Z_g1`96rZB=eA{UHep@vYbtR=Sr}VmHG%@l{(mZs|x5^3rHy_ zP$pzH870VoQ)itt6AaW?EZgqzjkQfh< z8M<%9J}nd_6@$LUV{VdB7i$L1NT&<=Zg@r0F`ddGnD`(Ftng>sWk@fiNqng*@N;a(UWsYEiey>8gyF&hCl7q-v7q@8J!VQX1of(QHzo^EN7x(C=(%NXd@$cs zIx>S9e@S3$)x`wWcaVTO)$Zs0?Du%^=BV#DBZaF4YGwrTOWDymW93j#WRcON8i4dK zq=eaqk}Zo9m#v1OSeFnzCJJ5H2vGGnGgZW^Sro^ZG=`=CH`Jnlx2jEvD{b;>FTmUb zEm*53N5Wkz zhYjsoBPn#FEX$cbGG#M$(n#su+8<`L9&wf^SAxk2INjhkBX@;sE;bZUq5X9|i&zLO zEb|&Os_`MF0yQX`GekT>DcxDGBN=!kEU5%fnXCz-I|pSa?~`L> zf09$#p5BaQMj$?d%ywKInq-4ykExswS53v0!n69#={u3Z-QZRRzY zGmxnenzpaFfh@x8jL0%Yrr4+bQixU0O53CvD!wAgYkt>+B?+!8{D zvQ8=+kER62_6jLfe&!>R*qB<-ILtV=QC7Zs8R-LIsP7?KG&>|}?jf&M-ZE=3p&b|AS_+*dG7al=#x@pa;kJDkPC_G{ z!1U^(j@6XJ%AoQKzKqPXSX=9SY8$||r*$Zd$&O3CIfAv&<76zWMMPC&v1LOguWxU^ zIw{}9EXDGICM1LkHH0I38`7+2!9$(q;-)kgTWM7Dc3A;wmL*8LtblAx(=JPCN?Ey$ zwGpr`y3!hERu>!L%KEnk@e2ILRIc6D;4Lz69ZKqKF{4}7BG&?_J}iK$+yZEo+5~R; zG)jaBVDr>ChW4Xg%n_qhTgT`dt^PhYPL{FFtPjSV1XRj&K?IW--9R_zL@v>~7*3JdX84r|6cOVjiXgR5>v= zu)0P&n9(9s%Aq*lU*bw8K_O}8X%pl0zaOUPuI69VwtA=nc+~_E>)Dv0$OI^f;f19} zp9S&47SA3iq*)y{P?zL7s@+Azp|s2t8FEK*9RS;t{nKNM#VhdM8eF^r@2kOhMO#79 zvZ03wya-w2FDMRSUmq*m!&MGs9hu_YYwtcm_pxnq(puXq%Y<2GTVHlwT|dwW*S%4v znV#gV!3yL`4f0lh{dIbGJCnkKU`c>zrrc^+)Mc6a3iY1Mi<-y&@&$Pa=46DyY=BGR z0}h~y5Xh?wnio-``C2YDXS-okVTP47KP15gX>h|~pv9Ig(%!ujX2#YS=j%cnx$5Xj zqZm=eX6R*X!^F%DQ_itnibQGDYByWHwB&5E+5=_J>;+94*)~*rC&pCdkX40wKg}?m zBri&bTwK_jZLBOk>+6n9&G?}}3FJT-5LR5zA&p@K=*Zv+see|hi}qeX<&q0v5OtH1 z)Y>MVSGks05(W_jv@nQ)$V?h2J?BwbLfGOYLvwin68AdKMRC=%gxG=Z5{3}HEn-bv zihhZaEI*Fh*~7Lz;lpk{ zsac*oqK;oz2Db5u4y$7>DyGrM(u1YZ$Y2ekq&%HtuxH#Utjj(S$FEu+Lb4k<6<8f6=KZ||KQeUALu)g$Ja2iqt$g7=ws=vfEmLvr;mGK2*m%Jy34IZt;CdNl4VGXo@ zB4DhW%mc(+7hyg>mPZ}r?jca$A>na><5#gULHOL?urN3H3 zYQ28RivEx_5)}%0hPyDlR5^RFP6(f+7n5t^sqBSVp#~B_)+}V$5awGmvznxYJRx{a zB;SIX(&;-!4u9%k(x%OQGydzVGdSs@8#J_yVrRi-zUsf6^hR|uv9QXbQQVE!cQLXq zp7T2LcY*^1{&Ps}IspbbV6Eij_+G7+@x8h>V0<3}8PKTyOr=9ypZOBqo_PrCD}AP~ zsz5q^Xe4%zb!S-zi8;yoV|*u1@W=@&@{;hRMeSObmrSxb2FPaMDM%9yWiB8cVWGJi zBM@Pe6AGr0kR>0VlXU0k$Ze+?SCN6o7Z(oM{3V^VL`Hyvp5zBcyebY%UObHif-Hzh zw@j`dV71N2llG(IaXHG2KAlYRDEC7rkBo938x+=ZI%u)nC>-wO!Uo`hqfwv6-`n)g z&9GiyJjfEQeTGL?n=|j09*6{7IIX+hovZ5T%g`ct26=k;8hvxh3vZNz&?&s9xPcMt zIA?I0-mCJjAh}}F;iQ8i!-mJ^>DdYUZ4_5EHCn!J;z;P~hUp{qQ#yFSedjL@pomZI z??c%f*kdr95l`<{_W9G$B>J!bbFqP%PDo-x0RxW2s2U&Z^^;jEatSCC>$YEB!CENcuz`mp|$&M-Co5nhu;fcF^5A>LW0^$H#SIzWPy8J|BdN;pVFW~J_>hGy0a5w` zz@v(^R#u16Q#zoot#bH?5KURlY54ejBQPrc$aEbUdEY+i=Kx&lC~^$y3)XPm4#71p zr=#Er522csjua}?^{YbBj8l{1DbsMSjx(wKktT;*foN-vIbnn2;mkoq`(n;%uv>6O zNFu%#>;(G?TG=0rt!Et_2zHbIM?;1yapN#jmxX2h@ZX#5JH} zO>Z30VY*hdA&0m%ygy&9QNtL|3vI|@&R9r~TtPKTv&^DahPR!eFsTq)&_i6I3pv?N zI%;WaE8Rgxu?4stPPzryFC9O)x&=H2o6rJ`E=+N)>_od459)GUT3zO-XjV={Bh}oGjN-V2Z>XA$ZjdIX^a_%vrQb~C{KQh!xdw6Ye7NpyPm9^Fa zdg?$LZ3(P;lYyUUXI7)P)|U(ld+iqO8kMM*UALWDZ*2CbHUQyAF0_)J9N*Zdh9v)u zF}|J4FIwLOt~ztnIIy{#OT=)0ZOac>3?Z#=#U9cNJpJ)NZq4kP5eLWS&yOuvoz{#x zp!GSi(o@kE6>}` zZ0bKvXMfpBSQWm-2;W{9iPjglV_n(;Xc3-<|69h~p%rnf2#ncHPTsY-E{2#m zwC|LqHffZa9YAnlk2k}-ZnvoMAw-s#dM=(zF)1b|q7JQUF{87U;3dE*ZzHA%nM^!Uf$961<{jEEKJ0H6_jd4P4teAOo)y*-gi?V09R)hS4QLPOzQ0q!ESE^W|0si z;hFx$YBKePts@_i8M$x*g-ckKgi#NGQAOYt>D4K&<%K+&SU=fImH!|#{K=j$ayoVE z(NuPg=#%9%dO+O>#aA-yvX*&M3|ebqJrmjZMlDq*^(gn=$Z)`zB|bfqPf(}Z7nU&0 z+<(4*5acv&e0=1{@Z#ij9p10k9UWsbI4If^l8@B{1vQpbtC654jDt>f1~j~)Y?Ywu zvSvi=iR)F|0S*B8>nOS5Eqn55n3dr$FEKu-M$)0vYyqZVz2q4YC|dsdG+-g!MdTL# zqM&gncWDKprBEe{DUohFpFKwxBj=-w(ZwYc(IpgP7@;#6IMR(Y-J>!?gxx9Osw_ZI zgZW{b$!-O|jrVQ(lR@ECK=Zbil=}`b31xsDQHKwoIeZuflGm!r3gUyKarsaRbOU-hS)XQ$Arjjh z*->`{+g`)U_2@B)L#rhc^VYR!t|q?#7T;&q>#zmB&J*k+F?3+!TxT;C7v?mBmU6*j zuaRHoP&JowuI^a#`vUAe)W~k{q2N)R#LSI-2PB2H@1(BpIB?=Ph7ico;0Q#<_#3lI zPIFY~uPQXMf=})pLY6#gBL>56<#lU&z{CmX10+kMx{wl10&+OM6o(b0KU`<EfjD%Fb+E8$@u6tk|L0picp);&y`;7o1KiG zU|v`I<@feJtiXyFj-H!~;Hb{fRj?6hNOI`3-v7Ssc=E4n6JBk&PLg_#Vr$Tu#ne-K z4el@c5r0ohAF$eIb3t_xpo0v8-X$b|q(sC?Ocp;SH4rGfEs+W-v!F^< zQdO!N^i=Uvnllj*bH{lCIqKK@^nrUSH@EsYhn@Pu+)=$js5V}hlQi!@9Hm#UMm9zz zv15vlU$hL#zF7By9rR$zS0TsbB%V+RPHfL&hzMd>Z%v5;?);`!~i7o!xpoj2Le@LWlv(iiNhYT}S*nrjn5Ssk$oxhG&YKEvXD88Y|) zr<>z*(11Pu2=YF@%gCxlyfquyj*#eZxdl45$|6Igy@5YIzy%C!HmcA@4!Cp#LbVNY zg4^{_TJGFdA=M|R(Rs{`wON_ta}04N%aWxLkZ4TTrIqrl<1(zr^OFKpSiV8{W#K68 z&XnLmU4R7zhn%rj7<6xb0jfz#uP;QR`c*aX9x!=lY81Je9>%FADgwIe!9UQcIi?jT zvgg!0ev9`7JUlPRHD5vD2ElC95P&M*2CO8m3J_{$5TL2H0Ht0XecG0b)M%ZDq~~jR z>fM6y7a>J|ya|-7W`6ScElpVIswRwSJ3e!oM*~YWQty-2{tX+>j80 zaf_6dT(+5KG<;KkWZ}Q1Ma|@~o`yxK?N}<&-T&Mc66pUA_1&| zMl)=ZO~Cx}ESp0G8Igs&(`gKsLU&WwIR>*?F=U0MucBF5lm(ese|Fm$k&Sh@nl12P zHB{qtc(ehC=IuXuCSa@c2Y4DeX4S!^^+i-mNWG$e(f@f^EDE5okPnqmvXBFLJY2OB!Wr) z1E|wW*Z~hffn<@EH?IP#KF`!dfgf6hTnKSXmBg8ijvTWjtkK3OzXGTdRbb?_1sIb3 zRQVX9>Sjq}V@lj|Na()15A|zqRz+lKr_i7#1dNBqgK}Q9PF#Rxdn*E~201wxIm}N4 z=adst8%}8;O>=Ap*bnKYi@OR#;Cu{ntW7Ffkzr(^WV5z;Gt#Jo&3v3d*-S`VT4M5) zF%cTF9#Yrv@9;Sj6iC$72Gp&tB8|G#jyF>D#{wets+d{ zZxxZpeygZZHf|Loq3%|2t7%ZYY!!FXH&$&G6QZoGVhUZnRh)M>9*y2A?lh}-OE|n7 zD#NX!0=peqi^XLO9)W6x96EJ-77hkOp`Ez(l?M#6y9GSLj^6@^FKq!v`dpydf}BDH z*{DHMV8chF7*w@C(q7d1>Xfgj$}+LDL0DXvJG+!;yc>EJC9ZGMsu0D6x!ZkZR#LvXp}MkR0v!~GV3t1h5A8iiXzRtxz|k{&6GGaU&mFF^Nbb(w*KM^@*p@MwS! z4tJpl&>}3x7U^^ak{mjycjA}B9~@rz%_?MgkPJ?aSqqmmOke_qq3Kda<`F<{AcGMx zw%N7_+0@d=x>&C+XjZRkABk-TxoD8^c)VMuTSiHOCWbK(9RL|Gy0q9(rQ;L(Css}) zkJWDP9p{v8;n5Z#&(Vdk+5NWhDiDX5u%V%@5(d9=B6MDLTAzL!^#QE{JYt&-g_sDG z>CGtY2V(oLlrzKoZ#}&qxsM%{o@CE2ed1&^J?+r19=J$ABoVd9Y_A(i1ro`IJr^RZHHF8Z-j zU%Cc)#{K`O!N!!2U4xCO+cgjZw_#6r-=RzO_||DQm%4Sjc6oukFVV;ik(*1{kf5FQ zK`9v#An3%>B?{Ow$>w6VOx7;!bY|5qQP8I7&4p}A(k=iYY`lKOG1BZk6vX{wCSik$_x>IuRpuYc?T}UFH&3L4V~{R3vccdQ3*BCS zJkW}%r2W0|{ao__33dz1d{XF>I+nu_b^t^|9#| z#ap3UY66%%r1yhF)#)TOSQTkp`&9O+aaby19-`GF$9(7B701H&ks)9Dk3ZyiVjsl&ga z{BL{Gzm#^Qt8qj4FXwkhn!u&BGu@r8WrbjQkE*0V17}IMBi+>D6W8v6^zp~Zum5(rrmF2wx{|VP=C5kb z$LZr~M@Q{T1Ny$SOc2%nYSP`D*3yoFw@~|s(>=IHDEV%_JI3#W+aF0crE>$j1|CTd z5pxf}ivw%vQF7ifa4aoeh6lL*yAO8{HQ7#z`_lcLode46?)0G{9BJ0b=e~5#Uwo1l ze++kTdL{qvC6!9vnLe5BAGmKwsUIf)N7KFAAElpnQrBGr^f{>>rB_lq%fDTO7(D*_ zIO%s%-;bw{4eX$&-a>2XXSY`IiB?`W@F3spWQ1;V{C%{0*Ja0c+)VkZ^C!2TCGIVJ z^8optb-h$KjphjbbRW6w#D8p1eYi8dWpHtD$H3iuxr35#PK%eTR%_}0L6xWfjuG?d z(5`ep{_CjG2zAo8+Xt8z>863}2vP6tYs4$f;I!bT41*N^7LQ zZ{VJE_0ZMy$}aNSkscg8r+R$x*>v^bIm*4Cn%_5YGxbticHti0x0C*jI_nGdlKvYZ z5B;~sS3CIbL5Ftwujb|s&BTEnq`5imB-Kv-sSa22cYAthFb!=VdT8(wejgrunBQ<}cX< zux@DUgN%u0E*W_;I)lN!M{+eBDd?$Agf5R_mz)Kp1G*<#1eub6tZo_wYBC1Djrak&kGwzd= zxP9P0dU+@13y(h10jtsDY9s$H+esg5oOaTKDUF^!b?}Etae3;votLM9^rSS1OSpmb zMBrE8o(SxT%(^Gz@1)Cck0ZBE+J=7{{-+RM{M+z7m9*M*hSKA4PryAfJ&nKHfNcY| z4cIndR~mLX{wLwCz&$xVoqVq%zpIFQhQHiF%v10w*Qe3nPrzw?4)FI%Tzrw7`fC0@ zlXRbg`&4{Sz$Kja$7ea`PWm(;pN{(s+)mPcChoKNVjC{ul<#L#<~5{O+G~mb9C8~X zhtCDB)%I*+cJuobT9I%A_@6_r&n5JEE@y}oL+SHzUx2#~w+FWuw~xACPkuM>&5eBf zJidE8{+p=L^ZEV-xGx0uMbz$vq<<0leKGE4YO4O+k2^s4Anp+U!_@u=zh6K&>igM@ z$u;RH;bXYtxGQm2k?)IX&C_ve)c~!!g%+KlSBG)8HqxKOKZ1J+?iB7S+$cR1<2IB| zdZZi2ag{RPtB;Fby+U(am$24>5>#D631jX34|CV$&WZ)O&4XRN;on9BcV+_&J~LYd!M<$t5s>9+y@ zcHDR1z7zLd%)Dn-eH#7s-Tr197-U=e9^Cihz7O~PxF5j%Anu27_v7A*dz<_Hn)G&N z`a7u04>J}&LJNKr_W*5vCo}wAwBsp^+>cSGcjJB>_Y=6EB+Y}kpThk#?md+KGyJ}S zxSz$%6aG2e&*L7#UB&7c^a^=C{qutK3#^hMoK}n0tmeymSv5nrgd6bs5lpM-g}`2z z)4eGDA}dNb>X&fu!~HVuS6E@&a0xd+{JZd}UJn!Zetv(IF}XI+lNYcGe~oqb>$LYb zSfRhk?{87ZM{vJQdaai?8SZhoZMf~Y$K#%Wdm`>~+>>xu;GT@zfqM$>sko=%uEaeZcNOj#xT|r` z#C;0xQ*qD2eH!l5ai4+PiTh04XW>2@cMa}Z+~?pv7q<)dY~1dFBdqy$y`Ka8<@Dc6 z!H%>dJ$ImkADqnu`h46M;I8AlJp;$U(1G{%^36Wn^`w1s?w=d*-#9Roo;RR=9)b$) zr03&a!1rG`@Wk{*q<8%@Q>n7<8H&9!QGBKi@O8&Qrw-mm*HMc9bSR!;$Ddx z!}W0ExCxx*toDgX(oW%~ap!P-+W9Kn3~6R@bGUijgS1=wY`vbrwWMF9Zg&j`cfB|* z4G1p_m#Lka^Wa`SQUmy&;<9qmO`f0q3ZoK2dI^Z3hvy#{wT?zOnr;a-n>1MbUlUxE8d+&#Fj z!hJRQFSDzD4Slc|{|6|$;L5Kh{_AjGkNXDV@5Oy1?v1!Nk@n5FZ^Hl0#D5F!Ex2#R zeH-v^2mT$n?{uGa(s$ukUwt?6_u;+=_r0|5`}qBS+z;S>5cfm4`*Ck||EkY#r8m{* zDZP#KZ^yl3;3T+yPx|2j!MCL!A^nfy9-s`3`#br47w*S^y&L!AxSycxBmDj(?m^s7 z;eHzT9^B91ewKV+jr%$LKhN(&r0+vDJiF2F8q-%$UbD}&27dwfUfeI@-bDN_@%uj9 zFXMg%_b_>>?eC{f>W5#&y>p<DPe$I_@`czlr-T+#`IW@%U}>_#OPet1#~O zaKBH#d~o23^db7<4+dVso~ZREdP@|JXsw;;4+ln}UPghAvIm?_e?*>zcIuOF(7WI{ z{)uk=W6Jq3?jyL5;vU8Q2~O1HpYr=>1GhnMm34hZ`g6+v3*29Nj7QR6Q7`q;$0+Bo zaeqVjI^5s#t=5L{vew7n;r<@?57h4;!7m>l_+0kXZRwwY{WI=gaM$zQOUUbA`TYd$ z-*Eqq`y}o^aQ{h~|HAz@?tcjXFYaMzK(*!nfJHtXzUz=a^6Nw4U4=_H(a`FP)KR?V z%}^TX3@OOCchVr9A>3uS$Kkf&ws+VIa8KwIS`%&p_~m@}q|WW+0Ea4p!nx5LZCOr*zCGP1R;h5d&s*d`z^jp+b{rL>?znXlWiTf1F z{Zzuw!u>UKR(RvnfPXsfGk}#g?8N_>r2Q=1XH&OpaM$8K2lp_Qy});$%Qw4l&&KV> zJqPz(+~?swANK{g>-b*#%^rUD;`ZS_g?3)wxij5BUN?4LlAedV3HN;53p!`h7n0_S za4)2PUWEH%@>!s~oB7?(HwSPBafb*W?r1K&j1pd+j&xq0jynILbgc6V=3O@(C;r8q zSEgH>ZY-VPcbGJ{^8Jy4$ETB>$EOk8OK_(;FH57HUOL?wPqz_&26sE|Ebb0mA1eH% zoh}q<8H;AWPo_INQ|V=$>Gbl>x%7%oKXp5=O0Vq9q_GZo$GK#G455k8Qd&xj{1$zqIrH7aErLRa7(yl+8pabYejRAFm+qSi9R~d?*-f% zZrx+dzQ8wMLikH@uO^Q#!@Z`nobINN-UY|79UA_%qt zyXNFq0Q<@ue-CZB4Cq(!%~u2a8r(g^Uj=6(_}2noB3$P6``}h2pZz-EU*DOu|DDXg zD;YPzzXABYofXy?bbe=*-=Xxz&Ut;F-o&3bcP=>HH&M@j8CZZ0zL36|Z@vY06&wny z4SAdV=j{Jh`}^dQz`gv(IvBt`XJFlBd|OA{%uxDv(tQVcekZ@*)%g@Z}{usMPE-pxj=?6M@ryuORF8vU3nmd{|^L*2X z8@!*fzJh;mWgPDZ_BPzxaqqxM`tc*UAH_X@y9%yQ^Kpjuyp!;|I&WZhyn%Aw;ChRm z*Pi!d9_vneH)+Io{5a`;g0;09Zckj_Pj(Kc2fzG^0dt3S=fA{s28R0)B{9(RPAAE%0 zkK!K1{R!?*aes#UbKGCx{u1|B)ca$&_fg_sli%Oq{+96H;qDsvhV=KHdvhDVG5tg5 zjp-jdZ%Q90%|B7jl~C}}#!HQl`gkb)v-4xE;s004RG)vM^G%)$8jpV?{s{Bn-^urr zxc|WYC+@$1$5?9|zd5I8&n1ulA?|;1|3^AHgZjct4JvPq0XyN~w=nOcPX-+yaYKV| z0Y}BS{5@?R9Ms;t4f%ueh%{Hi?MHuJs$JKl%c?ws?|U5iY{PBGX%BonzfZtD5qCN6 zNw_Qc=B@!r4pe?8J(>FM0Cr?Ra}qwzd478}Z!}Mz3QYYa+V~UX`!w>o(s_zz9!gIi z1Wyru2I-!GG-AMce<$>m%J?oc0pFdTIe1_Cl)>*wpE`I&de-3grcWdNr{g|@Z+G(h znSB!&@!~3YcKa!48*JHTjq<=B)7Tk$k&b3Hi$~rzu&BU8(j%i)~E8{Utez)QzM>@&x z2<|2H$tm3E-~+kM$oTTyyp8xX#Qh`E9O1&BWDeg>_$*F%^A3JriaYQA5e^eB(;Rsh zr7Hc8r8@`To!7tCpkyvTp2z!|^s>Qg(#!e&6_nGZ%va(c!}SP{^E-i?#9fQjDB6Eb znj)<9)3|f^U(HomL5-$6XlwsR{?I5xt$#(fRnel70nsO#6` zz5#bH?i+D$#Hn1#&E7j{t*X7bU;+q+ZZz1hlaNjzp^%{8U zm&oVaa{jTde=a?x>f28GcFL2SkQ49fn0<)e1Ly7N$@PDANODCb8h^MU_oJ+`HH zlIC4*gXCYokn!+)(~k}QVtP0ENKV~HS{Qq`_R5gO{y6n}J+!{(hP2TUp{mkI6@TKM|d^O+y9BJ=z-0mHi8(e=qJ6o%iQ;1#eB-UwX{68rz4{`v!kC-_!(?iINAbbhTGKh;D1^qatc3x}35{Wk7*aPMGlO!ND@xChAd1LXO8 zxZlTp5ceVA??sv#`&>$YK;8ckH$>bYf&2bAxAPO7-^e&1=4Ep&f7ofXUNu&~nekZA zD<1(Cd!_p5xAHu>CViAL9>x6$?oV-lhWm5M{0rP);{J;7wEjK@{I7BE0QPQ=i)f!m zDt!2N9pSM;OLaVVuce)mrTh)wNDsG+WH`q7chcXgKDfWfJ%DuhANc*p!4FX9q4aUe z*@u&?>Yw=bpK<>(_(>y{3GO_h0p82=XX*E_n*0s|BK)M z#{Caz{}=avII5lohM;4Hpiyu`xXW;l!)?QDANrj1_@TlxiVqb1`N!!AL*ijS%)0n6 znS2=B9W=G((-Vh20{kQRKC%IRn)=dS%2T>!OUpls@1vxF zCS_)3X67wZ%9t`UGq>BO%ts=`Mx=LWVs}kZa<06TWml%>jQsf3PJDvw!f-#dpa!6t1|DH-| zxu;TXy@gwpVb}Ic3P_14T?10mgET`*w}5mEDIzU7G($*tNVfIb-@XKff9l*TbIfw8c)N5X=FWlFv-Rw-{$zMqIl<0{*G3(dW78|@%M|mytUtcOS)B%g`2#yC2dLH2@>Z)$EL}u{8iH>z1=WZ3k(t{K z3d07-7EyM_X=iq&KnuI({B$w+xkT?mn3CmwmaPB&Y~`8v-C*kHiv@k%te?rr)<0i6 z8yH-Xv#izzyN%KazbMLIFWRw)bLuqkUK20&NuXJ9$+t%W?E0!jDXrSHcGBxjK5JzD zl{v6xIWfNxHEZtL!}{9M!X{X9xyaolO}K^Af;~@pBVn=?N<*zsEPjE?{o+}NDH)nH zm<%tJi2J2%$0ss$b1+q8C=NFfQ-?(=nkM+W%!a%1kL?+2M+DMHocNagO0z%kZm1`* z^@eJpvodrvq8h05E@jt~n|5*Cn2Y<)SVIg*az&BCT^s8hxp|t(^C3bXdiN(;;Pbcj zU;2u=bW!iQ^Bi{g>=7;l zMClLAJsKU@M z=y2;6z5Y3j)ua_5iA7uK&!y`_Zq#vh>ND~k#X*E~KWReAC98whPeiaC2Xz^(zM{Ez z@c55JIp_sup47Xr=(d^$9`8bqMr)aEzBTI^rH;XBcBb_ZR> zqP&!%m*2B(&5ytGwOAOtla&vLyugqK$`i0+GmmI1?v>E-!O7lJ4A#UFj7k4H*iX)` zgmaSYErBCrnMd~6*HtUoAbPHo@?pstvpMUz@DbzzudCEt@bkCKE8&_;4cso$G;rK& zI&tUcS3MsvecFCVwVHU=wZ7c7eQ(%fgQM9UYmj=Gs%I~=P7&S(u+qtYnUCSNEtlM# zShYALQu69zO#K=4fRxbK+Y#4)Q6RCjd#MH1p1o}Pxq2(DeXgDOAfS!k1}Y%U=AVkW z_}Lhzwt*R^L7e${#e{OFowiaRv!t>0`^UbpTgk~au?gl3Z>;o@A{D;nM!>tkp3&~u zlG8G7lduIV7+)_3hQ5EKT21$_*EJ#hqa>H{>S%KT$ZF8GdwT@~Tvv;^TtxbZgVyAq z>B7dB<&@LFn2F4pyMkw|8L!yP$BY1R>$a*48?2JE+63)t9IoxNZ2j=-X-^AcgDWKhoR$1 za--=IsYbk}pWF+Zoua8R?0o}hgJ$1jtSp%_3FSJEjiBkx5oV6!B-i@y^sK8=1(h5L zt&)w*lYKH{q9E)=6?LzSl2@j)Ub_q^xMPmYE0*i5aE|=>JlF>BqF5lSSdCc7hVk2R z01FpQLK|rK+hsd1#(+OnTFED^bO%J7ah>W{TdDA+Q~%b#l#>mtC-N1^m>#OL$vzmh za7~3T1l+35tlLhz<&V%<>;?C4RTJvnuAoqzCGi1(sxJ!&u7&xgNODt3F2REg-9q7> zc&CCH{t2y;JBbb`>vUkF#29k9)_};toH}3`K+xHB6WIN9%SAz|C1wG znfu~Z2)+8Na7iZ?RL0y_HYq*q=$5|*|KzH9{xFTo)voCod#ll!)KDJy_@$@1RDbQm z2wBitqwoY>>q5o%8Cyxat!-kpovNQfq9qZ2rXQ^)BVm&=A3;9|@yl?KLYnx$1)d@Z zvHP4EgldtLnr=WJ|cDd9wp#(5@sOo6iuKE_5Q7+}zP?}L< zFQOuWvyLUw8}MO_5X!SoIx;+IQx?3S6>c=yPL|G?5FYO2T_VEo0pX>PK)6J8F z#!Wf!uEz*dm72}u6Jb@dD}lc`0crd(H=yiF+_K}^;EyV|lG)}&K$5PPwjb6{ zs}<9jFJrCUpVIw|wc_p^ckru#j)QaHday zjn;cWgvdV}w8&62ej9@^Tg<*oijPx&im@`D?lWWu#m~iH|*2qYUM+0)}!j^k3@i`-8 znbo<;#3NpUUTN@?VUFMq)isyq*lVmerlX}i#uz^*mm}-5h0Bkgx;_1~N7k6%FANBN zu>!x(I)5IDWA-HM_!3~C^eMP`D%CEB<6u8_eknPk(X!5^v_y&eOG9n@W11?>F9xS5 zU%?Y(Q#G$8d3X!ZSz31m7LpVd{g^2ExHziP8P3ic;jKUTh{yG?bPlxgbOre8Y>~wI zx({Yf^wFe(@Tm*z*QdR}2*o9@`RAWn%gXn$fKTp6vEb9=<&*519^mQ-(32IEHF9ucz|vtsjt{ z8!b@I1Us1;R{oUl&&R*W)Na>(ox@ADVJjMvn#$&;+>l891xvmXx3to7v8!sW!mw2T zkZvt%%g-lXMdi6^W`ctISFLrO6wDdoESjJc+xF@fs7t_>1H4YIvu3Nfau{<;60 z%YgL;w>ceVJmAtmFlCN-5^nW4F1${2nhbR_indM|)RJGNW# z9b58!JZI^KUTYk-YeDOTq`EIZF@|)z^o>Pgvd^-)OpR-_M_)czixrCXYNq5QX}AwM zvMl5N<7pGr>6Gm-p`y2KnCbnon92h|e1Pr^Rf8(Ezx^a)13$OELNi&R^bVzM zk1(>yxt&D%7!&jG;p_2y+g;OqLjgb4@qaQDhnN_tSb4uO;maj? z52&u?Da70LN8S?tz&+0qd*>fAicKL+{OS2nju2D=_Y=;qoLUp=U-(uApRo3G(Z4H?J6+L`W1qOV2C3vO}q4e^1o=X^%0e^I>OkjHE{aq*;1zncIf31 zs#xcfRxw%rpG;3?$loLNN%XanO%kDQJ879Iy7&>+e|rZ3jKKd-eh!)?YXVr9`8qE;VHeoemcp;(<7 zb!3wi{M!q*dH>i#bP$Di84nZr#GrWHq-FEi(`^%JV%}AO=X(;hRr!u*VZhmFI93H> z(QVsLS^t~q;PqX)Oh^gbmGNt7WFO(@_^esl@=jN6zqB?BL`w9Jj1kWguu%tCM<1fyw#n+9J z?ppy(-nN(Bd%=DVgJPqkH`a2fa?Htmd&CS*EfGaRUA4%=sgv9yjLb??}v3 zTD4`DZk3JmA1i{p1u;|vj33+Hc%vZ$%im$}iZiJkTOqBl#mdWO1xddawY&eE_H-L(aM#1SiNh2Z#`o2pO`aEN52+>uZ+SF+AgzA$M-Unk(>e0V~JN` z&;Qa1cT4yx-&tlyoEexqQBz(DZc$ASi#D|Iu8(DYZDgo#`g_n_e`%^(fHe(*9ViHk zN_Q);oQyIPS((YBDIDW=;)PharB&<|GS*)faqOtvErp2cJ!W=}<`N$GI3AkAir1hU(%uIW~!}hV&~cU0lpg6IPRY6u=zHG5*`C>lEH2SK|YINM1%7uMx6ig+Yx{3WxP8GYRGG;>m zWl^qAS@IxI(N<>emqK+^%L}$|J;c6&$!VD(4Uvl>#0_pg>X6#QOx&9!L&pxYO`iUD z_0m?i^0Rve9b*1G(b1`yqiwuUDblmu=ZlVq1fadOnk+XxbkpIr^QwXR%*ZZy)~Bhi z7sW9?c~k!)^>l_PiVXPLI}V`Fw#E`7S)*uT6LG`aj?v~C9-CI zWiiahyLOa+{CL2d1p%db7d8tHQ#pAFW}Ki%V5)7l>TNd}tNke$mHo{y zz3!K6@EKO}a@~KbOaMZsAHultwrTIoq6Ny_r>i-J3A|81U`3_Vioiwmk#xxrG{F7EfGOks8Q=_Ak{`mJ3W-SW(` zT6WPI#AChaNNcW5{&;Rj3Q^v5#X9lZ=NKX{9 zUa37P=S3&@r}{$n0z0Y$!NDdCi_w^Xv=^VR6qcgj-zZ$^Z81P>3)Ei!+590}YSx_~ z%+ob`J;P;R#>l@Ay=JxKm!=kP%@un1EzGf~c`~FqA0O&KHK|A#_N<-z-949 z=w?y;6z2bxBz}qh3X&F{aXP_~;&6~RU{uqnlzvprAtXa}%1)DckyM=-W@5NFZiZs=MplLsv!iVA1FY`M|2`aqdtoE#lAbT`{6B3pX*(Y?~1a(jHC z$t_X6+R;Cyd3F<0E+@Yx0`yz%k)kRT@+9_;NjQC;sgcB`+YypZdli(~$B!kLsaA`! z3TxkISMw#49;el)pp9#jlYKzTjNywp4IshH6K-)WeN{U(p8_F?yWG@&S6rqPD`b`u ze(<$~44UF<#lvGx?j&P<6S6g$;rnxsHYNzf+1({B=EX#prN_xt1Kmo*2F+IIGBSxP z(6O!rat8s(--N!39lL&R6RAz^Y092>g5%O0?YK4%_4-uz+adDuiqCi>)T1fQNtalG zJoVuaF0jjxxzcmXD150gH~nt-5{6Y$GfbFD&Ld9W>zzCeK4~bZ~c7(gd42b6f|k`yQ(0E|T`}wL2>~ zv3Q&a%HSc*-_zU2quYSivU%o3Zz^}zVjZvE^b=nD)NF`43X-aGU}8M5fbn(@7fjOQ zgT|MK?L;^uY96Vr{o`TTaIn=65_r(VEp(2YE8}IAb9fwmLeGq!{QBV zZ?x1V5g<1JQ;bkLJLbMyXcvf$`6_5IrY#!TXE!FKnv$Y(9M;A|35Y$a_{M-hl}-5 z^%%fvqw_UhL*!1WCqRECDeG7@|z$g>?NxdZXs=$~G_<#2H%4 z_Q9NC&5?insEQ)VqXf>P`arSo%(h#JY*gC)=vOShSjO}6d%k!yv6T1jv!&X}EXnrf zgF3#9zx1jCh!__P_Q5mIIByI!Gmja%RJ!b{cqJ`h+lC|lY1{*8+`x=IOB{`F^g9t5 z1r~L|elP9o;y%9I@!!fT6*3%f*Cg|VOGQkCip8CE>6{#JkIW|Uo-6)n=+2azG1DKz zZjcNSLXa!_Sl~rpJ@Jwi5_8Ug!pg|q^-8ploo{vbm8}*=uHm->X;dxb3hl;(yoPFE zQOIgNNdY17d#J`u^I92eU(+5RHv$uKWFVJ%KCA^dHF{NLiFlN?{+u7(%)4iU2NDn- zr`TY!7Vc8Jk$?y?_InormwU)#l71)1S3NB_Mb*r8#S@+=_ za`Q;x#a+*-)=?|Bq)k!Tm+hgM!M)8$JHK~CA#nD=CJ<2qKG1u{@e-va(ErMB4vp?` zLT1tpD*T7qjP&S@5l$-s#fE%omUuTE#R*B5=?w!1==Sr^j0aCG1qo9xj>D^=zwO;{ zohss|_&SYxn*%(F?ZItPo>-@x8z5_ieX$L65A2v_9$&S{lPgv6sasX6J&bzms{ye( z^o#f%A-kxq3u%SxR!0)xU}{DqqxIU5WwX{romx8=R1*d?^;7WH%RhfkUa^AT9PIu= zwe<=AGZ@`4%!qebfAxHCbQO608d-*LfIddwV^wnN+T;YO`EMyA2%G*l;eq@~;_=Dx zJyHaZaZz+66xKr|j{dhn_D@C4;Gi*`MBdF)A-VYNZ!=}IGW_Y^avmI`C4V|bw^02B zQ+yF?q$W@CEjE)ZKGurc2;x@06~@`}gZSY>q}m)i13q2j8l{{weADBLJfE1M>;v+xwYL1$)grStZ)P zU`OTrV>+;bhfJ%PbZP8q+7c`dZUZT{Pxy8@b`tz8MiK;Tu$95jJOYOpU}Y6Ac)8zsk=ax(kwCb%sAHvfiDTssDMwRV{f*9X zD@z}^QJ!_?_&3+20XbQ%ljV)fvce4I(}e`-vg34)o1_+L0*9qnYO`oB>G;@l{Ato1 zU3KO7sV@67oSE225Te40LUcVL^OURr@auqlfnZwEzVChs_T z&(p}%G00`GHQ&%j?iumUtIVbt>%E@Lcb;1!ee}M0a{?sK%$`5p)s$DepjFB;oG2+V zjG!6!RWP}*SBYV%AQx4OKN$2kYs?s7*?jl(ju-Y7u~iopc2EO1;YJslE_OsLhuyIm zvS#^~$&hP!TYJeN>gCN#Bqx3>Xs{|}7SAB<%z|B^hbWmy%_=$B{NoC!63vAXr~`oT zT|5EQ#{P$-+L`GNFF^@}9r}jse?Tfc13IV;Z}MQ9o^Nm3^bxtBt|WE?_zCOX?S>kmvuOr&60y0ZVXZG}HcVWP$Gb^ClkzlwB6Na6# z3^YV-;L_o#=x~Lh9>Zt_;(Tx$cs+jFo3{ns3f!#x$JOD1;(T!=tvu^+xCnU++wJZ4 zl=9XKy-D2t2V&cmJTU(ob_7pU>xsVqM`9p-f(~ou#r>n+d|6%3TzQ-4ZRmUuprps` zVR;3EGcHaN z-DmTpC7I5L{kSZb@&d~#fG8gR8Wok(C-#K*l!!r}#`2LCLpi7?6L2H}*QzVk?vCLyj$gB0F(LQ}VNXkOAyq zvHI|XjS=Sg*KQ>)CnL+VNKT=q(z2{?XFzF^GvC5rT6#v++B~XtiPr`~E_9T>vyyX3 zJBCV`@D5kS9Zl82e^_6_7>;$Fivst=#N@%11kb>0FGYjs4M~H|)TtCD6j(3|bMX>` zosos2p0h(<-=7=mynj@Dq1d3+Qx@o_{o~nW?z0J~zB=U&3@NmEiw7}UoesJgMWK*l z_=KmD!}^ZF&}A&4#nXp=ykJo#BUc*TolLXN|g;KsK(Uesoo+ZYu7oB~jCq8jm-ocJzJM21k zAXKK(Jbb;~xNB=@*=&JB^6T9|hLeP8))DqYY70z_;+GwUokKy_Fb)Y)J(u6HM-iqx z2TG=_zTPgSmRDT8#>%C^S*~ZF&L}^^_@4%HNu}RgDa6SSZzEu~l>=VQwcUE5kqS@a z4#yUvC(b1O(bFel;HRDIeXfJcB;z5F6AlTev!J?8^x-v%yjyx$XMcM5Qer2jWZDgR z;GbEE+t38Q5I<>$^l3|ikVZ__?3c)vs+6+`b;3&`;6D=&@J!`< z&1{6fReaCcMQJh&PO9AS_zYW$J#0dBJ%Cy?;nW1oqYUqpw2Fx5~D`|cdzA9AocT-T50-!bV7_uRE) zrC#=zjRQ;PBJLhVc{PXW;$5!vAC!b3;|X3?zSilk1BSg4M2>0(GRXb5-5-O$2ZS_= zqwZ=SQiP8#oj=L8@Qfc%1@M1jU*81LYHi#MUK0w@o>i9aJ6y_V5my{6RRIwZ0u9W- zzkFw(-oj&dpe-psF?Yh|v>~DOQYEYQlPDIm(Vs~p9Z1jSgS%0SoIdfg zn5ciKENE3E`@xCp7y4xj`o2`?qf*o8Uk^FARpTlWZ9n8rfp`dW`Ea5p);+6pHbCd= zkeZ*J|KA?hf<>Hl5!t&TQBmdv+}M-k3r^^G#k6B%Z3a6B_|1b-3e0sqE6v%2=z))l zFN;xgn6mTxwmjuw(l|b~iV$rqgWaTo^hftO+;Ja6=&cAcm{Qn@vm7wMmXEd03SQ5u zT;im^gLI}_4~`=k@8rsdcrpB95WIP!gIDpi4c5So$G*`*o%f!1oQKmVTSJf_Vu<`E zt~X|N*_aK&(C0^-&$-sm?1#n4Z1+}gIs8#S*fVtvBSjb9AXG6^RFj1=QtyK~K8Lg+a4q6N() zW<>mTkT9CwMTLl6`_exQi&wEIgV)$oYcH7+t#yWNo|}8WBl7(Qz&TI=z*W-55QIG( zX^PvL;ZOK;J ze2Hg!|Kla1^NB4>tzu$N)f}&CSpRw&2dDT}M&#vYvFR9-=R*-Z&$3`a7^1ZPe)`H= zV1wS?e$SKMV%;@n>QvJA?Co-L!btB62jv1|Odl*~1lB4)cW2Zw?TKcXHz~gb+!&g7 zridif3J21z4~^(9{%NeHhY#6r%^GkiHk%#f={wbz9ba5f_Z~M#N>an@#7c5y;-A{`z@N;%R`pw zo0bg+Ht$iUHL-7Or+5|4-8@}zn*(p&*xu*rL~k34UvmBuHZKFE$b!!z8SwPlC??mnTWccszBKrz61Ile$Ws*QYhKEUe3;8|kr z88Ky?vAKIHja)E*7cXru&V68^Nr`X0)p~PtK-BgOXb`l(^|u9Z z!9O|$$`%%+)jP%4+m{y*v#ZWvM9@I6R^8sMdUg#6jG!HCqph;jMeAfm(iU0aAw)H+ zh_4U!vPwfga2i#*2iqSUdtHV#!ykfa;QI=mXU<#&_MW}TBmf9e6)9jI^2nl%_=EI} zTbidXurT~Zczi=&N?YT~zVZ;By}g8JNfchew=G|!l<**@jP)!KgHmo8TNR8`UP{un zf#|HGbvEAlY9?Pl(C%@zSGVf|z~z5UXv2qFYF-bL|B~o^Et`hV;LrueSbyCEF&<`3 zyyb}(%%^vKH(e&|qqxO`e^1`l<%b6+JJwY;#~0oZjNRN4UaJGT1G9GjUyCw@aws`B zF|}2LP9E{S@_?W(mL0n_cDY%GebNK-xmDfmx9{k!kt@)KgdhJ$<(l%aKXtNr$(kE% zu}x!kS$eRi+cCRLDxXbt3ZJn0te?*a_(%SMr)5$8cIb5A{Sxr262ES(6}?B3M1;qE zrH=B#6cN$zPrPC?iaTeGO773+q$6QXmY=4yF{Pt$!{_O#~q&ycM1ur z!EhZH{socTf^*tee+t@xxexm2gbMrk7I?D;00u`4Y)GF|22LT zf?1a(6M90AzKqrbq}sD#*+!j5k;wKeVrNPF&Vj~-v&aB>$_%Xf=2*fLY1GxSt_r#f zDJX}mbZyedB5u!QcR8ZLZ>cgo5k&ln6zh@7x7OdU@x|jTr$A}DeUcfgaM4~a*Sk9o z`B}A2GOFz)=m&KQizqMh!6%Q&U<;F#h@t#Ir!%@merN{Y2eBirxi?eke|3%zdP^kq zU!zZtKI4JoP_z`1*1h@pVOc+azl4f~Ex*OCxNOmsfM!_Y^;lD%i}S#YNKN#yydG(d zaRmjO!ffoP>QX^wIC_;;ldLH~s;rRKL?wautWH9u^PeSw*)ixrZu#Xe&IxHZpRcwk z_{Rpl--bGp@x2%pD6#DG@Lvn`U^Ea+@}CYN)m3ojBb^f(Qa50s61WTct4!v>zhL%H zT`NaT(8VaKaC}|lz}L>DCx1&sTsacVaA_F`^P7O@h8ejzQ&MARKWAn<<*m^ft#eK!{g3sEatoa zqN<-2{V{WgMR_rqtNIjs6+c?N4K$O0*D*WSu6S_hZm8RV!c{UIB$J#T!)A!$uh%lUd2_aW&vnC%oKWV4F zdEWWG@c`IahPN;zuxr1%x1qlio*1*hwgJU%`nMbq0D+A_u+7_6Q^+!>kKD4c zkM*(`sKrV6T5K$_`eL2*OV$sgfRonK=*LrEdi?G56zf)_xmeIvCFw4@30AXc5PPmwuIOccueVB^7=4=T{Z@xS*TgnHu5fa9pz9>pS$Q3Y*?3>?Zw`+Qk z=*EJym+C?je)Y}cdf;s6W=-J!mG)}RV2)|3(H`6Mi)t2td*B|T5mJn33A*X=ybdzK zKGE)*gml8W-7cH>lQJWUrFTkMDqmJPP5E!MtHM1`gg#V_*YIxO`@8ZhH6m`MkPv% z^c(8;$MwV}{rVfy(7I#uN`%%?Qf9qjRf_N9cBJjRoTdJ{~C9byqn^+7^W9NWNA0mF}RhAnQ7rI#%|_52P0HKlIFoO}DhuOr+x-+?*G!3oUv>PzZ$p~krO#9rM$Lfh@vX*2gI;tuQ&6EUrxrxtH))%=J72Y<#!bR6}W58R~oxy?uZG zou+`}z9zUC_w$i)vJ>kmRp-bJW$Uq*SL;@bsN~sVkcTZM4^2tw(iULl#`GfC7tU6Y zwzlxcbMvg1WA*ZV4QkD_4YdjDX2sU|>z3ASbSEVRgiq|Ih>zIyBNF^=bz0Fc&+<19u<6l_B1J zPXS{hg|_HS0=8R!GOfF;yW)&bvVuy*0rQ~b+% zrr6YYjN?7_^awVyxxw0dwf=%P)=_6;7I%853WZ;@XzX!5Ww5=BaX3gVYraH%%WN-e zGTAG-8`E;P(DhGjk)J1t3Ee2&@&)gf(3l|mxE8NprXW^AYK8%#93g`ay{o+F zE=9dN=EGQUUqemz?fgNhp6rFgS#B((`%x$C(ghH?`y|2$s>&_JwmJQSyKP2VjF54z zg=!OKcPZ^37E(OF$eWIkP1Cz$4x*7i>V9kJU)x;k5l?i3@Ky1fKH^^TRg(PAzL?~& z#v-(VysrIYOk0dLqXyTg(ZN0c*aCqFoi1ZhjD?t=NF0ZGRTH;+*T2aCc0DaQ)-oNH z6ryYEYq$Aj+v>@bI%ZYVa-AEG;rSqRt9=ugaW0d+&0h7i41hF35#qh0pH7MWBUIuYz#|Lw36%DA z4R#VZKg4*Cys9%Blv)eL-J*3b!%P_R77iX&Hh+i_ifVYf>8Vgp{a`Dr zxTrH28&;sv4U#i?$aQkCP#!C!@kCCOhkQ zGUrGq5>JldYU_rVm_OO9v08cXE@xo9Js|!At4uEigThnWsE77)_b#J;+5Sfo);`br z3hdMG))!ICkq7`G!zMVgBz2wBrO)B-8a1>Fa)m-0b*3y-C0i|K9(ppc0F-^eX%lJ= peOpbl%nNRF3MJ=W9Hb%|ugZ>wVV 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/icons/clean.png b/blender/arm/lightmapper/icons/clean.png new file mode 100644 index 0000000000000000000000000000000000000000..c9bdd9db2b529086df7a3c106a48ead90665729c GIT binary patch literal 3075 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 literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/icons/explore.png b/blender/arm/lightmapper/icons/explore.png new file mode 100644 index 0000000000000000000000000000000000000000..e5486558f0c97acc35bedca0ab2eff0abf1ddd07 GIT binary patch literal 2967 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 1 or dim[1] > 1: + + if active_image.TLM_ImageProperties.tlm_image_scale_method == "Nearest": + interp = cv2.INTER_NEAREST + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Area": + interp = cv2.INTER_AREA + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Linear": + interp = cv2.INTER_LINEAR + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Cubic": + interp = cv2.INTER_CUBIC + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Lanczos": + interp = cv2.INTER_LANCZOS4 + + resized = cv2.resize(basefile, dim, interpolation = interp) + + #resizedFile = os.path.join(dir_path, basename + "_" + str(width) + "_" + str(height) + extension) + resizedFile = os.path.join(dir_path, basename + extension) + + cv2.imwrite(resizedFile, resized) + + active_image.filepath_raw = resizedFile + bpy.ops.image.reload() + + print(newfile) + print(img_path) + + else: + + print("Please save image") + + print("Upscale") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchUp(bpy.types.Operator): + bl_idname = "tlm.image_switchup" + bl_label = "Quickswitch Up" + bl_description = "Switches to a cached upscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch up") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchDown(bpy.types.Operator): + bl_idname = "tlm.image_switchdown" + bl_label = "Quickswitch Down" + bl_description = "Switches to a cached downscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch Down") + + return {'RUNNING_MODAL'} \ No newline at end of file diff --git a/blender/arm/lightmapper/operators/installopencv.py b/blender/arm/lightmapper/operators/installopencv.py new file mode 100644 index 0000000000..52ea02affb --- /dev/null +++ b/blender/arm/lightmapper/operators/installopencv.py @@ -0,0 +1,81 @@ +import bpy, math, os, platform, subprocess, sys, re, shutil + +def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'): + + def draw(self, context): + self.layout.label(text=message) + + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + +class TLM_Install_OpenCV(bpy.types.Operator): + """Install OpenCV""" + bl_idname = "tlm.install_opencv_lightmaps" + bl_label = "Install OpenCV" + bl_description = "Install OpenCV" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + cycles = bpy.data.scenes[scene.name].cycles + + print("Module OpenCV") + + if (2, 91, 0) > bpy.app.version: + pythonbinpath = bpy.app.binary_path_python + else: + pythonbinpath = sys.executable + + if platform.system() == "Windows": + pythonlibpath = os.path.join(os.path.dirname(os.path.dirname(pythonbinpath)), "lib") + else: + pythonlibpath = os.path.join(os.path.dirname(os.path.dirname(pythonbinpath)), "lib", os.path.basename(pythonbinpath)) + + ensurepippath = os.path.join(pythonlibpath, "ensurepip") + + cmda = [pythonbinpath, ensurepippath, "--upgrade", "--user"] + pip = subprocess.run(cmda) + cmdc = [pythonbinpath, "-m", "pip", "install", "--upgrade", "pip"] + pipc = subprocess.run(cmdc) + + if pip.returncode == 0: + print("Sucessfully installed pip!\n") + else: + + try: + import pip + module_pip = True + except ImportError: + #pip + module_pip = False + + if not module_pip: + print("Failed to install pip!\n") + if platform.system() == "Windows": + ShowMessageBox("Failed to install pip - Please start Blender as administrator", "Restart", 'PREFERENCES') + else: + ShowMessageBox("Failed to install pip - Try starting Blender with SUDO", "Restart", 'PREFERENCES') + return{'FINISHED'} + + cmdb = [pythonbinpath, "-m", "pip", "install", "opencv-python"] + + #opencv = subprocess.run(cmdb, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + opencv = subprocess.run(cmdb) + + if opencv.returncode == 0: + print("Successfully installed OpenCV!\n") + else: + print("Failed to install OpenCV!\n") + + if platform.system() == "Windows": + ShowMessageBox("Failed to install opencv - Please start Blender as administrator", "Restart", 'PREFERENCES') + else: + ShowMessageBox("Failed to install opencv - Try starting Blender with SUDO", "Restart", 'PREFERENCES') + + return{'FINISHED'} + + module_opencv = True + print("Sucessfully installed OpenCV!\n") + ShowMessageBox("Please restart blender to enable OpenCV filtering", "Restart", 'PREFERENCES') + + return{'FINISHED'} diff --git a/blender/arm/lightmapper/operators/tlm.py b/blender/arm/lightmapper/operators/tlm.py new file mode 100644 index 0000000000..01046dfda3 --- /dev/null +++ b/blender/arm/lightmapper/operators/tlm.py @@ -0,0 +1,1731 @@ +import bpy, os, time, blf, webbrowser, platform, numpy, bmesh +import math, subprocess, multiprocessing +from .. utility import utility +from .. utility import build +from .. utility.cycles import cache +from .. network import server + +def setObjectLightmapByWeight(minimumRes, maximumRes, objWeight): + + availableResolutions = [32,64,128,256,512,1024,2048,4096,8192] + + minRes = minimumRes + minResIdx = availableResolutions.index(minRes) + maxRes = maximumRes + maxResIdx = availableResolutions.index(maxRes) + + exampleWeight = objWeight + + if minResIdx == maxResIdx: + pass + else: + + increment = 1.0/(maxResIdx-minResIdx) + + assortedRange = [] + + for a in numpy.arange(0.0, 1.0, increment): + assortedRange.append(round(a, 2)) + + assortedRange.append(1.0) + nearestWeight = min(assortedRange, key=lambda x:abs(x - exampleWeight)) + return (availableResolutions[assortedRange.index(nearestWeight) + minResIdx]) + +class TLM_BuildLightmaps(bpy.types.Operator): + bl_idname = "tlm.build_lightmaps" + bl_label = "Build Lightmaps" + bl_description = "Build Lightmaps" + bl_options = {'REGISTER', 'UNDO'} + + def modal(self, context, event): + + #Add progress bar from 0.15 + + print("MODAL") + + return {'PASS_THROUGH'} + + def invoke(self, context, event): + + if not bpy.app.background: + + build.prepare_build(self, False) + + else: + + print("Running in background mode. Contextual operator not available. Use command 'thelightmapper.addon.build.prepare_build()'") + + return {'RUNNING_MODAL'} + + def cancel(self, context): + pass + + def draw_callback_px(self, context, event): + pass + +class TLM_CleanLightmaps(bpy.types.Operator): + bl_idname = "tlm.clean_lightmaps" + bl_label = "Clean Lightmaps" + bl_description = "Clean Lightmaps" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + if not bpy.context.scene.TLM_SceneProperties.tlm_keep_baked_files: + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for vertex_layer in obj.data.vertex_colors: + if vertex_layer.name == "TLM": + obj.data.vertex_colors.remove(vertex_layer) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + + atlas = obj.TLM_ObjectProperties.tlm_postatlas_pointer + atlas_resize = False + + for atlasgroup in scene.TLM_PostAtlasList: + if atlasgroup.name == atlas: + atlas_resize = True + + if atlas_resize: + + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(rotate=False, margin=0.001) + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Resized for obj: " + obj.name) + + if "Lightmap" in obj: + del obj["Lightmap"] + + if bpy.context.scene.TLM_SceneProperties.tlm_repartition_on_clean: + + mats = bpy.data.materials + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + print("Repartitioning materials") + + for slt in obj.material_slots: + print("Repartitioning material: " + str(slt.name)) + part = slt.name.rpartition('.') + if part[2].isnumeric() and part[0] in mats: + slt.material = mats.get(part[0]) + + for slt in obj.material_slots: + if slt.name.endswith(tuple(["001","002","003","004","005","006","007","008","009"])): #Do regex instead + if not slt.name[:-4] in mats: + slt.material.name = slt.name[:-4] + + return {'FINISHED'} + +class TLM_ExploreLightmaps(bpy.types.Operator): + bl_idname = "tlm.explore_lightmaps" + bl_label = "Explore Lightmaps" + bl_description = "Explore Lightmaps" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + cycles = scene.cycles + + if not bpy.data.is_saved: + self.report({'INFO'}, "Please save your file first") + return {"CANCELLED"} + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + if platform.system() != "Linux": + + if os.path.isdir(dirpath): + webbrowser.open('file://' + dirpath) + else: + os.mkdir(dirpath) + webbrowser.open('file://' + dirpath) + else: + + if os.path.isdir(dirpath): + os.system('xdg-open "%s"' % dirpath) + #webbrowser.open('file://' + dirpath) + else: + os.mkdir(dirpath) + os.system('xdg-open "%s"' % dirpath) + #webbrowser.open('file://' + dirpath) + + return {'FINISHED'} + +class TLM_EnableSet(bpy.types.Operator): + """Enable for set""" + bl_idname = "tlm.enable_set" + bl_label = "Enable for set" + bl_description = "Enable for set" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + weightList = {} #ObjName : [Dimension,Weight] + max = 0 + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + print("Enabling for scene: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + print("Enabling for selection: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + print("Enabling for designated: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + print("") + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + return{'FINISHED'} + +class TLM_DisableSelection(bpy.types.Operator): + """Disable for set""" + bl_idname = "tlm.disable_selection" + bl_label = "Disable for set" + bl_description = "Disable for selection" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + weightList = {} #ObjName : [Dimension,Weight] + max = 0 + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + + return{'FINISHED'} + +class TLM_RemoveLightmapUV(bpy.types.Operator): + """Remove Lightmap UV for set""" + bl_idname = "tlm.remove_uv_selection" + bl_label = "Remove Lightmap UV" + bl_description = "Remove Lightmap UV for set" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + return{'FINISHED'} + +class TLM_SelectLightmapped(bpy.types.Operator): + """Select all objects for lightmapping""" + bl_idname = "tlm.select_lightmapped_objects" + bl_label = "Select lightmap objects" + bl_description = "Remove Lightmap UV for selection" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + obj.select_set(True) + + return{'FINISHED'} + +class TLM_GroupListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "tlm_grouplist.new_item" + bl_label = "Add a new lightmap group" + bl_description = "Create a new lightmap group" + + def execute(self, context): + scene = context.scene + scene.TLM_GroupList.add() + scene.TLM_GroupListItem = len(scene.TLM_GroupList) - 1 + + scene.TLM_GroupList[len(scene.TLM_GroupList) - 1].name = "LightmapGroup" + +class TLM_AtlasListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "tlm_atlaslist.new_item" + bl_label = "Add a new item" + bl_description = "Create a new AtlasGroup" + + def execute(self, context): + scene = context.scene + scene.TLM_AtlasList.add() + scene.TLM_AtlasListItem = len(scene.TLM_AtlasList) - 1 + + scene.TLM_AtlasList[len(scene.TLM_AtlasList) - 1].name = "AtlasGroup" + + return{'FINISHED'} + +class TLM_PostAtlasListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "tlm_postatlaslist.new_item" + bl_label = "Add a new item" + bl_description = "Create a new AtlasGroup" + bl_description = "" + + def execute(self, context): + scene = context.scene + scene.TLM_PostAtlasList.add() + scene.TLM_PostAtlasListItem = len(scene.TLM_PostAtlasList) - 1 + + scene.TLM_PostAtlasList[len(scene.TLM_PostAtlasList) - 1].name = "AtlasGroup" + + return{'FINISHED'} + +class TLM_AtlastListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "tlm_atlaslist.delete_item" + bl_label = "Deletes an item" + bl_description = "Delete an AtlasGroup" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + scene = context.scene + return len(scene.TLM_AtlasList) > 0 + + def execute(self, context): + scene = context.scene + list = scene.TLM_AtlasList + index = scene.TLM_AtlasListItem + + for obj in bpy.context.scene.objects: + + atlasName = scene.TLM_AtlasList[index].name + + if obj.TLM_ObjectProperties.tlm_atlas_pointer == atlasName: + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "SmartProject" + + list.remove(index) + + if index > 0: + index = index - 1 + + scene.TLM_AtlasListItem = index + return{'FINISHED'} + +class TLM_PostAtlastListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "tlm_postatlaslist.delete_item" + bl_label = "Deletes an item" + bl_description = "Delete an AtlasGroup" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + scene = context.scene + return len(scene.TLM_PostAtlasList) > 0 + + def execute(self, context): + scene = context.scene + list = scene.TLM_PostAtlasList + index = scene.TLM_PostAtlasListItem + + for obj in bpy.context.scene.objects: + + atlasName = scene.TLM_PostAtlasList[index].name + + if obj.TLM_ObjectProperties.tlm_atlas_pointer == atlasName: + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "SmartProject" + + list.remove(index) + + if index > 0: + index = index - 1 + + scene.TLM_PostAtlasListItem = index + return{'FINISHED'} + +class TLM_AtlasListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "tlm_atlaslist.move_item" + bl_label = "Move an item in the list" + bl_description = "Move an item in the list" + direction: bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + scene = context.scene + index = scene.TLM_AtlasListItem + list_length = len(scene.TLM_AtlasList) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + scene.TLM_AtlasList.move(index, new_index) + scene.TLM_AtlasListItem = new_index + + def execute(self, context): + scene = context.scene + list = scene.TLM_AtlasList + index = scene.TLM_AtlasListItem + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class TLM_PostAtlasListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "tlm_postatlaslist.move_item" + bl_label = "Move an item in the list" + bl_description = "Move an item in the list" + direction: bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + scene = context.scene + index = scene.TLM_PostAtlasListItem + list_length = len(scene.TLM_PostAtlasList) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + scene.TLM_PostAtlasList.move(index, new_index) + scene.TLM_PostAtlasListItem = new_index + + def execute(self, context): + scene = context.scene + list = scene.TLM_PostAtlasList + index = scene.TLM_PostAtlasListItem + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class TLM_StartServer(bpy.types.Operator): + bl_idname = "tlm.start_server" + bl_label = "Start Network Server" + bl_description = "Start Network Server" + bl_options = {'REGISTER', 'UNDO'} + + def modal(self, context, event): + + #Add progress bar from 0.15 + + print("MODAL") + + return {'PASS_THROUGH'} + + def invoke(self, context, event): + + server.startServer() + + return {'RUNNING_MODAL'} + +class TLM_BuildEnvironmentProbes(bpy.types.Operator): + bl_idname = "tlm.build_environmentprobe" + bl_label = "Build Environment Probes" + bl_description = "Build all environment probes from reflection cubemaps" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for obj in bpy.context.scene.objects: + + if obj.type == "LIGHT_PROBE": + if obj.data.type == "CUBEMAP": + + cam_name = "EnvPCam_" + obj.name + camera = bpy.data.cameras.new(cam_name) + camobj_name = "EnvPCamera_" + obj.name + cam_obj = bpy.data.objects.new(camobj_name, camera) + bpy.context.collection.objects.link(cam_obj) + cam_obj.location = obj.location + camera.angle = math.radians(90) + + prevResx = bpy.context.scene.render.resolution_x + prevResy = bpy.context.scene.render.resolution_y + prevCam = bpy.context.scene.camera + prevEngine = bpy.context.scene.render.engine + bpy.context.scene.camera = cam_obj + + bpy.context.scene.render.engine = bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_engine + bpy.context.scene.render.resolution_x = int(bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_resolution) + bpy.context.scene.render.resolution_y = int(bpy.context.scene.TLM_SceneProperties.tlm_environment_probe_resolution) + + savedir = os.path.dirname(bpy.data.filepath) + directory = os.path.join(savedir, "Probes") + + t = 90 + + inverted = bpy.context.scene.TLM_SceneProperties.tlm_invert_direction + + if inverted: + + positions = { + "xp" : (math.radians(t), 0, math.radians(0)), + "zp" : (math.radians(t), 0, math.radians(t)), + "xm" : (math.radians(t), 0, math.radians(t*2)), + "zm" : (math.radians(t), 0, math.radians(-t)), + "yp" : (math.radians(t*2), 0, math.radians(t)), + "ym" : (0, 0, math.radians(t)) + } + + else: + + positions = { + "xp" : (math.radians(t), 0, math.radians(t*2)), + "zp" : (math.radians(t), 0, math.radians(-t)), + "xm" : (math.radians(t), 0, math.radians(0)), + "zm" : (math.radians(t), 0, math.radians(t)), + "yp" : (math.radians(t*2), 0, math.radians(-t)), + "ym" : (0, 0, math.radians(-t)) + } + + + + cam = cam_obj + image_settings = bpy.context.scene.render.image_settings + image_settings.file_format = "HDR" + image_settings.color_depth = '32' + + for val in positions: + cam.rotation_euler = positions[val] + + filename = os.path.join(directory, val) + "_" + camobj_name + ".hdr" + bpy.context.scene.render.filepath = filename + print("Writing out: " + val) + bpy.ops.render.render(write_still=True) + + cmft_path = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_cmft_path)) + + output_file_irr = camobj_name + ".hdr" + + posx = directory + "/" + "xp_" + camobj_name + ".hdr" + negx = directory + "/" + "xm_" + camobj_name + ".hdr" + posy = directory + "/" + "yp_" + camobj_name + ".hdr" + negy = directory + "/" + "ym_" + camobj_name + ".hdr" + posz = directory + "/" + "zp_" + camobj_name + ".hdr" + negz = directory + "/" + "zm_" + camobj_name + ".hdr" + output = directory + "/" + camobj_name + + if platform.system() == 'Windows': + envpipe = [cmft_path, + '--inputFacePosX', posx, + '--inputFaceNegX', negx, + '--inputFacePosY', posy, + '--inputFaceNegY', negy, + '--inputFacePosZ', posz, + '--inputFaceNegZ', negz, + '--output0', output, + '--output0params', + 'hdr,rgbe,latlong'] + + else: + envpipe = [cmft_path + '--inputFacePosX' + posx + + '--inputFaceNegX' + negx + + '--inputFacePosY' + posy + + '--inputFaceNegY' + negy + + '--inputFacePosZ' + posz + + '--inputFaceNegZ' + negz + + '--output0' + output + + '--output0params' + 'hdr,rgbe,latlong'] + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Calling CMFT with:" + str(envpipe)) + + if bpy.context.scene.TLM_SceneProperties.tlm_create_spherical: + subprocess.call(envpipe, shell=True) + + input2 = output + ".hdr" + output2 = directory + "/" + camobj_name + + if platform.system() == 'Windows': + envpipe2 = [cmft_path, + '--input', input2, + '--filter', 'shcoeffs', + '--outputNum', '1', + '--output0', output2] + + else: + envpipe2 = [cmft_path + + '--input' + input2 + + '-filter' + 'shcoeffs' + + '--outputNum' + '1' + + '--output0' + output2] + + if bpy.context.scene.TLM_SceneProperties.tlm_write_sh: + subprocess.call(envpipe2, shell=True) + + if bpy.context.scene.TLM_SceneProperties.tlm_write_radiance: + + use_opencl = 'false' + cpu_count = 2 + + # 4096 = 256 face + # 2048 = 128 face + # 1024 = 64 face + target_w = int(512) + face_size = target_w / 8 + if target_w == 2048: + mip_count = 9 + elif target_w == 1024: + mip_count = 8 + else: + mip_count = 7 + + output_file_rad = directory + "/" + camobj_name + "_rad.hdr" + + if platform.system() == 'Windows': + + envpipe3 = [ + cmft_path, + '--input', input2, + '--filter', 'radiance', + '--dstFaceSize', str(face_size), + '--srcFaceSize', str(face_size), + '--excludeBase', 'false', + # '--mipCount', str(mip_count), + '--glossScale', '8', + '--glossBias', '3', + '--lightingModel', 'blinnbrdf', + '--edgeFixup', 'none', + '--numCpuProcessingThreads', str(cpu_count), + '--useOpenCL', use_opencl, + '--clVendor', 'anyGpuVendor', + '--deviceType', 'gpu', + '--deviceIndex', '0', + '--generateMipChain', 'true', + '--inputGammaNumerator', '1.0', + '--inputGammaDenominator', '1.0', + '--outputGammaNumerator', '1.0', + '--outputGammaDenominator', '1.0', + '--outputNum', '1', + '--output0', output_file_rad, + '--output0params', 'hdr,rgbe,latlong' + ] + + subprocess.call(envpipe3) + + else: + + envpipe3 = cmft_path + \ + ' --input "' + input2 + '"' + \ + ' --filter radiance' + \ + ' --dstFaceSize ' + str(face_size) + \ + ' --srcFaceSize ' + str(face_size) + \ + ' --excludeBase false' + \ + ' --glossScale 8' + \ + ' --glossBias 3' + \ + ' --lightingModel blinnbrdf' + \ + ' --edgeFixup none' + \ + ' --numCpuProcessingThreads ' + str(cpu_count) + \ + ' --useOpenCL ' + use_opencl + \ + ' --clVendor anyGpuVendor' + \ + ' --deviceType gpu' + \ + ' --deviceIndex 0' + \ + ' --generateMipChain true' + \ + ' --inputGammaNumerator ' + '1.0' + \ + ' --inputGammaDenominator 1.0' + \ + ' --outputGammaNumerator 1.0' + \ + ' --outputGammaDenominator 1.0' + \ + ' --outputNum 1' + \ + ' --output0 "' + output_file_rad + '"' + \ + ' --output0params hdr,rgbe,latlong' + + subprocess.call([envpipe3], shell=True) + + for obj in bpy.context.scene.objects: + obj.select_set(False) + + cam_obj.select_set(True) + bpy.ops.object.delete() + bpy.context.scene.render.resolution_x = prevResx + bpy.context.scene.render.resolution_y = prevResy + bpy.context.scene.camera = prevCam + bpy.context.scene.render.engine = prevEngine + + print("Finished building environment probes") + + + return {'RUNNING_MODAL'} + +class TLM_CleanBuildEnvironmentProbes(bpy.types.Operator): + bl_idname = "tlm.clean_environmentprobe" + bl_label = "Clean Environment Probes" + bl_description = "Clean Environment Probes" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + savedir = os.path.dirname(bpy.data.filepath) + dirpath = os.path.join(savedir, "Probes") + + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + + return {'FINISHED'} + +class TLM_MergeAdjacentActors(bpy.types.Operator): + bl_idname = "tlm.merge_adjacent_actors" + bl_label = "Merge adjacent actors" + bl_description = "Merges the adjacent faces/vertices of selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + return {'FINISHED'} + +class TLM_PrepareUVMaps(bpy.types.Operator): + bl_idname = "tlm.prepare_uvmaps" + bl_label = "Prepare UV maps" + bl_description = "Prepare UV lightmaps for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + return {'FINISHED'} + +class TLM_LoadLightmaps(bpy.types.Operator): + bl_idname = "tlm.load_lightmaps" + bl_label = "Load Lightmaps" + bl_description = "Load lightmaps from selected folder" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + utility.transfer_load() + + print("Transfer finished") + + build.finish_assemble(self, 1, 1) + + return {'FINISHED'} + +class TLM_ToggleTexelDensity(bpy.types.Operator): + bl_idname = "tlm.toggle_texel_density" + bl_label = "Toggle Texel Density" + bl_description = "Toggle visualize lightmap texel density for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + uv_layers = obj.data.uv_layers + + #if the object has a td_vis in the uv maps, toggle off + #else toggle on + + if obj.TLM_ObjectProperties.tlm_use_default_channel: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_Lightmap': + uv_layers.active_index = i + break + else: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == obj.TLM_ObjectProperties.tlm_uv_channel: + uv_layers.active_index = i + break + + #filepath = r"C:\path\to\image.png" + + #img = bpy.data.images.load(filepath) + + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + space_data = area.spaces.active + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + new_window = context.window_manager.windows[-1] + + area = new_window.screen.areas[-1] + area.type = 'VIEW_3D' + #bg = space_data.background_images.new() + print(bpy.context.object) + bpy.ops.object.bake_td_uv_to_vc() + + #bg.image = img + break + + + #set active uv_layer to + + + print("TLM_Viz_Toggle") + + return {'FINISHED'} + +class TLM_DisableSpecularity(bpy.types.Operator): + bl_idname = "tlm.disable_specularity" + bl_label = "Disable specularity" + bl_description = "Disables specularity from set" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + for slot in obj.material_slots: + + mat = slot.material + + if mat.node_tree: + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Specular": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + for slot in obj.material_slots: + + mat = slot.material + + if mat.node_tree: + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Specular": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + for slot in obj.material_slots: + + mat = slot.material + + if mat.node_tree: + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Specular": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + return{'FINISHED'} + +class TLM_DisableMetallic(bpy.types.Operator): + bl_idname = "tlm.disable_metallic" + bl_label = "Disable metallic" + bl_description = "Disables metallic from set" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + for slot in obj.material_slots: + + mat = slot.material + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Metallic": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + for slot in obj.material_slots: + + mat = slot.material + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Metallic": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + for slot in obj.material_slots: + + mat = slot.material + + for node in mat.node_tree.nodes: + + if node.type == "BSDF_PRINCIPLED": + + for inp in node.inputs: + + if inp.name == "Metallic": + + inp.default_value = 0.0 + + if inp.links and bpy.context.scene.TLM_SceneProperties.tlm_remove_met_spec_link: + + mat.node_tree.links.remove(inp.links[0]) + + return{'FINISHED'} + +class TLM_RemoveEmptyImages(bpy.types.Operator): + + bl_idname = "tlm.remove_empty_images" + bl_label = "Remove Empty Images" + bl_description = "Removes empty images from scene materials" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + for mat in bpy.data.materials: + + nodetree = mat.node_tree + + if nodetree: + + for node in nodetree.nodes: + + if node.name == "Baked Image": + + print(node.name) + + nodetree.nodes.remove(node) + + return{'FINISHED'} + + +class TLM_PostAtlasSpecialsMenu(bpy.types.Menu): + bl_label = "Lightmap" + bl_idname = "TLM_MT_PostAtlasListSpecials" + + def draw(self, context): + layout = self.layout + layout.operator("tlm.add_collections_post") + layout.operator("tlm.add_selected_collections_post") + +class TLM_AddCollectionsPost(bpy.types.Operator): + bl_idname = "tlm.add_collections_post" + bl_label = "Add collections" + bl_description = "Adds all collections to atlases" + bl_options = {'REGISTER', 'UNDO'} + + resolution : bpy.props.EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="Atlas lightmap resolution", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + unwrap : bpy.props.EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + + margin : bpy.props.FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + + for collection in bpy.context.scene.collection.children: + + #Add a new atlas with collection name + #Traverse before adding + scene = bpy.context.scene + scene.TLM_PostAtlasList.add() + scene.TLM_PostAtlasListItem = len(scene.TLM_PostAtlasList) - 1 + + scene.TLM_PostAtlasList[len(scene.TLM_PostAtlasList) - 1].name = collection.name + scene.TLM_PostAtlasList[collection.name].tlm_atlas_lightmap_unwrap_mode = self.unwrap + scene.TLM_PostAtlasList[collection.name].tlm_atlas_lightmap_resolution = self.resolution + scene.TLM_PostAtlasList[collection.name].tlm_atlas_unwrap_margin = self.margin + + for obj in collection.objects: + if obj.type == "MESH": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + obj.TLM_ObjectProperties.tlm_postpack_object = True + obj.TLM_ObjectProperties.tlm_postatlas_pointer = collection.name + + return{'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.prop(self, "unwrap", text="Unwrap mode") + row.prop(self, "resolution", text="Resolution") + row.prop(self, "margin", text="Margin") + +class TLM_AddSelectedCollectionsPost(bpy.types.Operator): + bl_idname = "tlm.add_selected_collections_post" + bl_label = "Add selected collections" + bl_description = "Add the collections of the selected objects" + bl_options = {'REGISTER', 'UNDO'} + + resolution : bpy.props.EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="Atlas lightmap resolution", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + unwrap : bpy.props.EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + + margin : bpy.props.FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + + collections = [] + + for obj in bpy.context.selected_objects: + + obj_collection = obj.users_collection[0] + + if obj_collection.name not in collections: + + collections.append(obj_collection.name) + + print("Collections:" + str(collections)) + + for collection in bpy.context.scene.collection.children: + + if collection.name in collections: + + #Add a new atlas with collection name + #Traverse before adding + scene = bpy.context.scene + scene.TLM_PostAtlasList.add() + scene.TLM_PostAtlasListItem = len(scene.TLM_PostAtlasList) - 1 + + scene.TLM_PostAtlasList[len(scene.TLM_PostAtlasList) - 1].name = collection.name + scene.TLM_PostAtlasList[collection.name].tlm_atlas_lightmap_unwrap_mode = self.unwrap + scene.TLM_PostAtlasList[collection.name].tlm_atlas_lightmap_resolution = self.resolution + scene.TLM_PostAtlasList[collection.name].tlm_atlas_unwrap_margin = self.margin + + for obj in collection.objects: + if obj.type == "MESH": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + obj.TLM_ObjectProperties.tlm_postpack_object = True + obj.TLM_ObjectProperties.tlm_postatlas_pointer = collection.name + + return{'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.prop(self, "unwrap", text="Unwrap mode") + row.prop(self, "resolution", text="Resolution") + row.prop(self, "margin", text="Margin") + +class TLM_AtlasSpecialsMenu(bpy.types.Menu): + bl_label = "Lightmap" + bl_idname = "TLM_MT_AtlasListSpecials" + + def draw(self, context): + layout = self.layout + layout.operator("tlm.add_collections") + layout.operator("tlm.add_selected_collections") + +class TLM_AddCollections(bpy.types.Operator): + bl_idname = "tlm.add_collections" + bl_label = "Add all collections" + bl_description = "Adds all collections to atlases" + bl_options = {'REGISTER', 'UNDO'} + + resolution : bpy.props.EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="Atlas lightmap resolution", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + unwrap : bpy.props.EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + + margin : bpy.props.FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + + for collection in bpy.context.scene.collection.children: + + #Add a new atlas with collection name + #Traverse before adding + scene = bpy.context.scene + scene.TLM_AtlasList.add() + scene.TLM_AtlasListItem = len(scene.TLM_AtlasList) - 1 + + scene.TLM_AtlasList[len(scene.TLM_AtlasList) - 1].name = collection.name + scene.TLM_AtlasList[collection.name].tlm_atlas_lightmap_unwrap_mode = self.unwrap + scene.TLM_AtlasList[collection.name].tlm_atlas_lightmap_resolution = self.resolution + scene.TLM_AtlasList[collection.name].tlm_atlas_unwrap_margin = self.margin + + for obj in collection.objects: + if obj.type == "MESH": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "AtlasGroupA" + obj.TLM_ObjectProperties.tlm_atlas_pointer = collection.name + + return{'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.prop(self, "unwrap", text="Unwrap mode") + row.prop(self, "resolution", text="Resolution") + row.prop(self, "margin", text="Margin") + +class TLM_AddSelectedCollections(bpy.types.Operator): + bl_idname = "tlm.add_selected_collections" + bl_label = "Add the collections of the selected objects" + bl_description = "Add the collections of the selected objects" + bl_options = {'REGISTER', 'UNDO'} + + resolution : bpy.props.EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="Atlas lightmap resolution", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + unwrap : bpy.props.EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + + margin : bpy.props.FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + + collections = [] + + for obj in bpy.context.selected_objects: + + obj_collection = obj.users_collection[0] + + if obj_collection.name not in collections: + + collections.append(obj_collection.name) + + print("Collections:" + str(collections)) + + for collection in bpy.context.scene.collection.children: + + if collection.name in collections: + + #Add a new atlas with collection name + #Traverse before adding + scene = bpy.context.scene + scene.TLM_AtlasList.add() + scene.TLM_AtlasListItem = len(scene.TLM_AtlasList) - 1 + + scene.TLM_AtlasList[len(scene.TLM_AtlasList) - 1].name = collection.name + scene.TLM_AtlasList[collection.name].tlm_atlas_lightmap_unwrap_mode = self.unwrap + scene.TLM_AtlasList[collection.name].tlm_atlas_lightmap_resolution = self.resolution + scene.TLM_AtlasList[collection.name].tlm_atlas_unwrap_margin = self.margin + + for obj in collection.objects: + if obj.type == "MESH": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = "AtlasGroupA" + obj.TLM_ObjectProperties.tlm_atlas_pointer = collection.name + + return{'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.prop(self, "unwrap", text="Unwrap mode") + row.prop(self, "resolution", text="Resolution") + row.prop(self, "margin", text="Margin") + +#Atlas disable objects + +class TLM_Reset(bpy.types.Operator): + bl_idname = "tlm.reset" + bl_label = "Resets all UI and settings" + bl_description = "Reset UI and objects" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + self.report({'INFO'}, "YES!") + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + +# class TLM_Reset2(bpy.types.Operator): +# bl_idname = "tlm.reset2" +# bl_label = "Do you really want to do that?" +# bl_options = {'REGISTER', 'INTERNAL'} + +# prop1: bpy.props.BoolProperty() +# prop2: bpy.props.BoolProperty() + +# @classmethod +# def poll(cls, context): +# return True + +# def execute(self, context): +# self.report({'INFO'}, "YES!") +# return {'FINISHED'} + +# def invoke(self, context, event): +# return context.window_manager.invoke_props_dialog(self) + +# def draw(self, context): +# row = self.layout +# row.prop(self, "prop1", text="Property A") +# row.prop(self, "prop2", text="Property B") + +def TLM_DoubleResolution(): + pass + +def TLM_HalfResolution(): + pass + +def TLM_DivideLMGroups(): + pass + +class TLM_CalcTexDex(bpy.types.Operator): + bl_idname = "tlm.calctexdex" + bl_label = "Calculate Texel Density" + bl_description = "Calculates Texel Density of selected object" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + return {'FINISHED'} + +class TLM_AddGLTFNode(bpy.types.Operator): + bl_idname = "tlm.add_gltf_node" + bl_label = "Add GLTF Node" + bl_description = "Add to GLTF node to active material and connect lightmap if present" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + cycles = scene.cycles + material = bpy.context.active_object.active_material + + nodes = material.node_tree.nodes + # create group data + gltf_settings = bpy.data.node_groups.get('glTF Settings') + if gltf_settings is None: + bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree') + + # add group to node tree + gltf_settings_node = nodes.get('glTF Settings') + if gltf_settings_node is None: + gltf_settings_node = nodes.new('ShaderNodeGroup') + gltf_settings_node.name = 'glTF Settings' + gltf_settings_node.node_tree = bpy.data.node_groups['glTF Settings'] + + # create group inputs + if gltf_settings_node.inputs.get('Occlusion') is None: + gltf_settings_node.inputs.new('NodeSocketFloat','Occlusion') + + #return gltf_settings_node + + return {'FINISHED'} + +class TLM_ShiftMultiplyLinks(bpy.types.Operator): + bl_idname = "tlm.shift_multiply_links" + bl_label = "Shift multiply links" + bl_description = "Shift multiply links for active material" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + cycles = scene.cycles + material = bpy.context.active_object.active_material + + nodes = material.node_tree.nodes + + LM_Node = nodes.get("TLM_Lightmap") + Multi_Node = nodes.get("Lightmap_Multiplication") + Base_Node = nodes.get("Lightmap_BasecolorNode_A") + + material.node_tree.links.remove(LM_Node.outputs[0].links[0]) + material.node_tree.links.remove(Base_Node.outputs[0].links[0]) + + material.node_tree.links.new(LM_Node.outputs[0], Multi_Node.inputs[2]) + material.node_tree.links.new(Base_Node.outputs[0], Multi_Node.inputs[1]) + + return {'FINISHED'} diff --git a/blender/arm/lightmapper/panels/__init__.py b/blender/arm/lightmapper/panels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/panels/image.py b/blender/arm/lightmapper/panels/image.py new file mode 100644 index 0000000000..929907e2fc --- /dev/null +++ b/blender/arm/lightmapper/panels/image.py @@ -0,0 +1,66 @@ +import bpy, os, math, importlib + +from bpy.types import Menu, Operator, Panel, UIList + +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, +) + +class TLM_PT_Imagetools(bpy.types.Panel): + bl_label = "TLM Imagetools" + bl_space_type = "IMAGE_EDITOR" + bl_region_type = 'UI' + bl_category = "TLM Imagetools" + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.label(text ="Image Tools") + + def draw(self, context): + layout = self.layout + + activeImg = None + + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + activeImg = area.spaces.active.image + + if activeImg is not None and activeImg.name != "Render Result" and activeImg.name != "Viewer Node": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text ="OpenCV not installed.") + else: + + row = layout.row(align=True) + row.label(text ="Method") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_engine") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_cache_switch") + row = layout.row(align=True) + row.operator("tlm.image_upscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch up.") + row = layout.row(align=True) + row.operator("tlm.image_downscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch down.") + if activeImg.TLM_ImageProperties.tlm_image_scale_engine == "OpenCV": + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_method") + + else: + row = layout.row(align=True) + row.label(text ="Select an image") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/light.py b/blender/arm/lightmapper/panels/light.py new file mode 100644 index 0000000000..fd576af1ae --- /dev/null +++ b/blender/arm/lightmapper/panels/light.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_LightMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "light" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/object.py b/blender/arm/lightmapper/panels/object.py new file mode 100644 index 0000000000..fcca524869 --- /dev/null +++ b/blender/arm/lightmapper/panels/object.py @@ -0,0 +1,126 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_ObjectMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + if obj.type == "MESH": + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + row = layout.row() + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + else: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") + row = layout.row() + + + if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_filter_override: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") + row = layout.row(align=True) + if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + else: + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + + #If UV Packer installed + if "UV-Packer" in bpy.context.preferences.addons.keys(): + row.prop(obj.TLM_ObjectProperties, "tlm_use_uv_packer") + if obj.TLM_ObjectProperties.tlm_use_uv_packer: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_padding") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_packing_engine") + +class TLM_PT_MaterialMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + mat = bpy.context.material + if mat == None: + return + + if obj.type == "MESH": + + row = layout.row() + row.prop(mat, "TLM_ignore") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/scene.py b/blender/arm/lightmapper/panels/scene.py new file mode 100644 index 0000000000..6bf1581239 --- /dev/null +++ b/blender/arm/lightmapper/panels/scene.py @@ -0,0 +1,756 @@ +import bpy, importlib, math +from bpy.props import * +from bpy.types import Menu, Panel +from .. utility import icon +from .. properties.denoiser import oidn, optix + +class TLM_PT_Panel(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + +class TLM_PT_Groups(bpy.types.Panel): + bl_label = "Lightmap Groups" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "TLM_PT_Panel" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + rows = 2 + #if len(atlasList) > 1: + # rows = 4 + + row = layout.row(align=True) + row.label(text="Lightmap Group List") + row = layout.row(align=True) + row.template_list("TLM_UL_GroupList", "Lightmap Groups", scene, "TLM_GroupList", scene, "TLM_GroupListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_atlaslist.new_item", icon='ADD', text="") + #col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") + #col.menu("TLM_MT_AtlasListSpecials", icon='DOWNARROW_HLT', text="") + +class TLM_PT_Settings(bpy.types.Panel): + bl_label = "Settings" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + + #We list LuxCoreRender as available, by default we assume Cycles exists + row.prop(sceneProperties, "tlm_lightmap_engine") + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + #CYCLES SETTINGS HERE + engineProperties = scene.TLM_EngineProperties + + row = layout.row(align=True) + row.label(text="General Settings") + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_apply_on_unwrap") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_headless") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_on_finish") + + if sceneProperties.tlm_alert_on_finish: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_sound") + + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_compile_statistics") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_bg_color") + if sceneProperties.tlm_override_bg_color: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_color") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_reset_uv") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_apply_modifiers") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_keep_baked_files") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_repartition_on_clean") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_save_preprocess_lightmaps") + + row = layout.row(align=True) + try: + if bpy.context.scene["TLM_Buildstat"] is not None: + row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0])) + except: + pass + + row = layout.row(align=True) + row.label(text="Cycles Settings") + + row = layout.row(align=True) + row.prop(engineProperties, "tlm_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_quality") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_resolution_scale") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_bake_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_target") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lighting_mode") + # if scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectao": + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_premultiply_ao") + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row = layout.row(align=True) + row.label(text="Warning! Background mode is currently unstable", icon_value=2) + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_render") + if sceneProperties.tlm_network_render: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_paths") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_network_dir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_caching_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_directional_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_dilation_margin") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_exposure_multiplier") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_setting_supersample") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_metallic_clamp") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_interpolation") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_extrapolation") + + + + # elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + # engineProperties = scene.TLM_Engine2Properties + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_luxcore_dir") + # row = layout.row(align=True) + # row.operator("tlm.build_lightmaps") + # #LUXCORE SETTINGS HERE + # #luxcore_available = False + + # #Look for Luxcorerender in the renderengine classes + # # for engine in bpy.types.RenderEngine.__subclasses__(): + # # if engine.bl_idname == "LUXCORE": + # # luxcore_available = True + # # break + + # # row = layout.row(align=True) + # # if not luxcore_available: + # # row.label(text="Please install BlendLuxCore.") + # # else: + # # row.label(text="LuxCoreRender not yet available.") + + elif sceneProperties.tlm_lightmap_engine == "OctaneRender": + + engineProperties = scene.TLM_Engine3Properties + + #LUXCORE SETTINGS HERE + octane_available = True + + + + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + +class TLM_PT_Denoise(bpy.types.Panel): + bl_label = "Denoise" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_denoise_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_denoise_use + + row = layout.row(align=True) + + #row.prop(sceneProperties, "tlm_denoiser", expand=True) + #row = layout.row(align=True) + row.prop(sceneProperties, "tlm_denoise_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_denoise_engine == "Integrated": + row.label(text="No options for Integrated.") + elif sceneProperties.tlm_denoise_engine == "OIDN": + denoiseProperties = scene.TLM_OIDNEngineProperties + row.prop(denoiseProperties, "tlm_oidn_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_threads") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_maxmem") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_affinity") + # row = layout.row(align=True) + # row.prop(denoiseProperties, "tlm_denoise_ao") + elif sceneProperties.tlm_denoise_engine == "Optix": + denoiseProperties = scene.TLM_OptixEngineProperties + row.prop(denoiseProperties, "tlm_optix_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_maxmem") + #row = layout.row(align=True) + #row.prop(denoiseProperties, "tlm_denoise_ao") + +class TLM_PT_Filtering(bpy.types.Panel): + bl_label = "Filtering" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_filtering_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_filtering_use + #row = layout.row(align=True) + #row.label(text="TODO MAKE CHECK") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_filtering_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_filtering_engine == "OpenCV": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") + row = layout.row(align=True) + if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") + + +class TLM_PT_Encoding(bpy.types.Panel): + bl_label = "Encoding" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_encoding_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_encoding_use + + sceneProperties = scene.TLM_SceneProperties + row = layout.row(align=True) + + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row.label(text="Encoding options disabled in background mode") + row = layout.row(align=True) + + else: + + row.prop(sceneProperties, "tlm_encoding_device", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_encoding_device == "CPU": + row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True) + else: + row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True) + + if sceneProperties.tlm_encoding_device == "CPU": + if sceneProperties.tlm_encoding_mode_a == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_encoding_mode_a == "RGBD": + pass + if sceneProperties.tlm_encoding_mode_a == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + else: + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + + if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_decoder_setup: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_split_premultiplied") + if sceneProperties.tlm_encoding_mode_b == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + +class TLM_PT_Utility(bpy.types.Panel): + bl_label = "Utilities" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + row.label(text="Enable Lightmaps for set") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_utility_context") + row = layout.row(align=True) + + if sceneProperties.tlm_utility_context == "SetBatching": + + row.operator("tlm.enable_set") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_utility_set") + row = layout.row(align=True) + #row.label(text="ABCD") + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row = layout.row() + row.prop(sceneProperties, "tlm_postpack_object") + row = layout.row() + + if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_weight") + + if sceneProperties.tlm_resolution_weight == "Single": + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + else: + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_min") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_max") + + row = layout.row() + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.operator("tlm.select_lightmapped_objects") + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + + elif sceneProperties.tlm_utility_context == "EnvironmentProbes": + + row.label(text="Environment Probes") + row = layout.row() + row.operator("tlm.build_environmentprobe") + row = layout.row() + row.operator("tlm.clean_environmentprobe") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_engine") + row = layout.row() + row.prop(sceneProperties, "tlm_cmft_path") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_create_spherical") + + if sceneProperties.tlm_create_spherical: + + row = layout.row() + row.prop(sceneProperties, "tlm_invert_direction") + row = layout.row() + row.prop(sceneProperties, "tlm_write_sh") + row = layout.row() + row.prop(sceneProperties, "tlm_write_radiance") + + elif sceneProperties.tlm_utility_context == "LoadLightmaps": + + row = layout.row(align=True) + row.label(text="Load lightmaps") + row = layout.row() + row.prop(sceneProperties, "tlm_load_folder") + row = layout.row() + row.operator("tlm.load_lightmaps") + row = layout.row() + row.prop(sceneProperties, "tlm_load_atlas") + + elif sceneProperties.tlm_utility_context == "MaterialAdjustment": + + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_utility_set") + row = layout.row(align=True) + row.operator("tlm.disable_specularity") + row.operator("tlm.disable_metallic") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_remove_met_spec_link") + row = layout.row(align=True) + row.operator("tlm.remove_empty_images") + row = layout.row(align=True) + + elif sceneProperties.tlm_utility_context == "NetworkRender": + + row.label(text="Network Rendering") + row = layout.row() + row.operator("tlm.start_server") + layout.label(text="Atlas Groups") + + elif sceneProperties.tlm_utility_context == "TexelDensity": + + row.label(text="Texel Density Utilies") + row = layout.row() + + elif sceneProperties.tlm_utility_context == "GLTFUtil": + + row.label(text="GLTF material utilities") + row = layout.row() + row.operator("tlm.add_gltf_node") + row = layout.row() + row.operator("tlm.shift_multiply_links") + +class TLM_PT_Selection(bpy.types.Panel): + bl_label = "Selection" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + row.operator("tlm.enable_selection") + row = layout.row(align=True) + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_object_settings") + + if sceneProperties.tlm_override_object_settings: + + row = layout.row(align=True) + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + row = layout.row() + row.prop(sceneProperties, "tlm_postpack_object") + row = layout.row() + + if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) + row.operator("tlm.select_lightmapped_objects") + # row = layout.row(align=True) + # for addon in bpy.context.preferences.addons.keys(): + # if addon.startswith("Texel_Density"): + # row.operator("tlm.toggle_texel_density") + # row = layout.row(align=True) + +class TLM_PT_Additional(bpy.types.Panel): + bl_label = "Additional" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + atlasListItem = scene.TLM_AtlasListItem + atlasList = scene.TLM_AtlasList + postatlasListItem = scene.TLM_PostAtlasListItem + postatlasList = scene.TLM_PostAtlasList + + row = layout.row() + row.prop(sceneProperties, "tlm_atlas_mode", expand=True) + + if sceneProperties.tlm_atlas_mode == "Prepack": + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_atlaslist.new_item", icon='ADD', text="") + col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") + col.menu("TLM_MT_AtlasListSpecials", icon='DOWNARROW_HLT', text="") + + if atlasListItem >= 0 and len(atlasList) > 0: + item = atlasList[atlasListItem] + layout.prop(item, "tlm_atlas_lightmap_unwrap_mode") + layout.prop(item, "tlm_atlas_lightmap_resolution") + layout.prop(item, "tlm_atlas_unwrap_margin") + + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: + amount = amount + 1 + + layout.label(text="Objects: " + str(amount)) + layout.prop(item, "tlm_atlas_merge_samemat") + + layout.prop(item, "tlm_use_uv_packer") + layout.prop(item, "tlm_uv_packer_padding") + layout.prop(item, "tlm_uv_packer_packing_engine") + + else: + + layout.label(text="Postpacking is unstable.") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + + else: + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_postatlaslist.new_item", icon='ADD', text="") + col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="") + col.menu("TLM_MT_PostAtlasListSpecials", icon='DOWNARROW_HLT', text="") + + if postatlasListItem >= 0 and len(postatlasList) > 0: + item = postatlasList[postatlasListItem] + layout.prop(item, "tlm_atlas_lightmap_resolution") + + #Below list object counter + amount = 0 + utilized = 0 + atlasUsedArea = 0 + atlasSize = item.tlm_atlas_lightmap_resolution + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: + amount = amount + 1 + + atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 + + row = layout.row() + row.prop(item, "tlm_atlas_repack_on_cleanup") + + #TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY! + cv2 = True + + if cv2: + row = layout.row() + row.prop(item, "tlm_atlas_dilation") + layout.label(text="Objects: " + str(amount)) + + utilized = atlasUsedArea / (int(atlasSize) ** 2) + layout.label(text="Utilized: " + str(utilized * 100) + "%") + + if (utilized * 100) > 100: + layout.label(text="Warning! Overflow not yet supported") diff --git a/blender/arm/lightmapper/panels/world.py b/blender/arm/lightmapper/panels/world.py new file mode 100644 index 0000000000..b3c5d294ec --- /dev/null +++ b/blender/arm/lightmapper/panels/world.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_WorldMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/__init__.py b/blender/arm/lightmapper/preferences/__init__.py new file mode 100644 index 0000000000..94cdfaeaeb --- /dev/null +++ b/blender/arm/lightmapper/preferences/__init__.py @@ -0,0 +1,16 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import addon_preferences +#from . import build, clean, explore, encode, installopencv + +classes = [ + addon_preferences.TLM_AddonPreferences +] + +def register(): + for cls in classes: + register_class(cls) + +def unregister(): + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/addon_preferences.py b/blender/arm/lightmapper/preferences/addon_preferences.py new file mode 100644 index 0000000000..a127052541 --- /dev/null +++ b/blender/arm/lightmapper/preferences/addon_preferences.py @@ -0,0 +1,106 @@ +import bpy, platform +from os.path import basename, dirname +from bpy.types import AddonPreferences +from bpy.props import * +from .. operators import installopencv +from . import addon_preferences +import importlib + +class TLM_AddonPreferences(AddonPreferences): + + bl_idname = __name__.split(".")[0] + + tlm_ui_mode: EnumProperty( + items=[('simple', 'Simple', 'Simple UI'), + ('advanced', 'Advanced', 'Advanced UI')], + name='UI mode', default='simple', description='Choose UI mode') + + def draw(self, context): + + layout = self.layout + + box = layout.box() + row = box.row() + + row.label(text="UI Mode") + row.prop(self, "tlm_ui_mode") + row = box.row() + row.label(text="Simple: Only the basic setup for Blender/Eevee baking with non-experimental features.") + row = box.row() + row.label(text="Full set of options available.") + row = box.row() + + row.label(text="OpenCV") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is not None: + row.label(text="OpenCV installed") + else: + if platform.system() == "Windows": + row.label(text="OpenCV not found - Install as administrator!", icon_value=2) + else: + row.label(text="OpenCV not found - Click to install!", icon_value=2) + row = box.row() + row.operator("tlm.install_opencv_lightmaps", icon="PREFERENCES") + + box = layout.box() + row = box.row() + row.label(text="Blender Xatlas") + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + row.label(text="Blender Xatlas installed and available") + else: + row.label(text="Blender Xatlas not installed", icon_value=2) + row = box.row() + row.label(text="Github: https://github.com/mattedicksoncom/blender-xatlas") + + box = layout.box() + row = box.row() + row.label(text="RizomUV Bridge") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="UVPackmaster") + row.label(text="Coming soon") + + uvpacker_addon = False + for addon in bpy.context.preferences.addons.keys(): + if addon.startswith("UV-Packer"): + uvpacker_addon = True + + box = layout.box() + row = box.row() + row.label(text="UV Packer") + if uvpacker_addon: + row.label(text="UV Packer installed and available") + else: + row.label(text="UV Packer not installed", icon_value=2) + row = box.row() + row.label(text="Github: https://www.uv-packer.com/blender/") + + texel_density_addon = False + for addon in bpy.context.preferences.addons.keys(): + if addon.startswith("Texel_Density"): + texel_density_addon = True + + box = layout.box() + row = box.row() + row.label(text="Texel Density Checker") + if texel_density_addon: + row.label(text="Texel Density Checker installed and available") + else: + row.label(text="Texel Density Checker", icon_value=2) + row.label(text="Coming soon") + row = box.row() + row.label(text="Github: https://github.com/mrven/Blender-Texel-Density-Checker") + + box = layout.box() + row = box.row() + row.label(text="LuxCoreRender") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="OctaneRender") + row.label(text="Coming soon") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/__init__.py b/blender/arm/lightmapper/properties/__init__.py new file mode 100644 index 0000000000..258a1896bc --- /dev/null +++ b/blender/arm/lightmapper/properties/__init__.py @@ -0,0 +1,62 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import scene, object, atlas, image +from . renderer import cycles, luxcorerender, octanerender +from . denoiser import oidn, optix + +classes = [ + scene.TLM_SceneProperties, + object.TLM_ObjectProperties, + cycles.TLM_CyclesSceneProperties, + luxcorerender.TLM_LuxCoreSceneProperties, + octanerender.TLM_OctanerenderSceneProperties, + oidn.TLM_OIDNEngineProperties, + optix.TLM_OptixEngineProperties, + atlas.TLM_AtlasListItem, + atlas.TLM_UL_AtlasList, + atlas.TLM_PostAtlasListItem, + atlas.TLM_UL_PostAtlasList, + image.TLM_ImageProperties, + scene.TLM_UL_GroupList, + scene.TLM_GroupListItem +] + +def register(): + for cls in classes: + register_class(cls) + + bpy.types.Scene.TLM_SceneProperties = bpy.props.PointerProperty(type=scene.TLM_SceneProperties) + bpy.types.Object.TLM_ObjectProperties = bpy.props.PointerProperty(type=object.TLM_ObjectProperties) + bpy.types.Scene.TLM_EngineProperties = bpy.props.PointerProperty(type=cycles.TLM_CyclesSceneProperties) + bpy.types.Scene.TLM_Engine2Properties = bpy.props.PointerProperty(type=luxcorerender.TLM_LuxCoreSceneProperties) + bpy.types.Scene.TLM_Engine3Properties = bpy.props.PointerProperty(type=octanerender.TLM_OctanerenderSceneProperties) + bpy.types.Scene.TLM_OIDNEngineProperties = bpy.props.PointerProperty(type=oidn.TLM_OIDNEngineProperties) + bpy.types.Scene.TLM_OptixEngineProperties = bpy.props.PointerProperty(type=optix.TLM_OptixEngineProperties) + bpy.types.Scene.TLM_AtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) + bpy.types.Scene.TLM_AtlasList = bpy.props.CollectionProperty(type=atlas.TLM_AtlasListItem) + bpy.types.Scene.TLM_PostAtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) + bpy.types.Scene.TLM_PostAtlasList = bpy.props.CollectionProperty(type=atlas.TLM_PostAtlasListItem) + bpy.types.Image.TLM_ImageProperties = bpy.props.PointerProperty(type=image.TLM_ImageProperties) + bpy.types.Scene.TLM_GroupListItem = bpy.props.IntProperty(name="Index for my_list", default=0) + bpy.types.Scene.TLM_GroupList = bpy.props.CollectionProperty(type=scene.TLM_GroupListItem) + + bpy.types.Material.TLM_ignore = bpy.props.BoolProperty(name="Skip material", description="Ignore material for lightmapped object", default=False) + +def unregister(): + for cls in classes: + unregister_class(cls) + + del bpy.types.Scene.TLM_SceneProperties + del bpy.types.Object.TLM_ObjectProperties + del bpy.types.Scene.TLM_EngineProperties + del bpy.types.Scene.TLM_Engine2Properties + del bpy.types.Scene.TLM_Engine3Properties + del bpy.types.Scene.TLM_OIDNEngineProperties + del bpy.types.Scene.TLM_OptixEngineProperties + del bpy.types.Scene.TLM_AtlasListItem + del bpy.types.Scene.TLM_AtlasList + del bpy.types.Scene.TLM_PostAtlasListItem + del bpy.types.Scene.TLM_PostAtlasList + del bpy.types.Image.TLM_ImageProperties + del bpy.types.Scene.TLM_GroupListItem + del bpy.types.Scene.TLM_GroupList \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/atlas.py b/blender/arm/lightmapper/properties/atlas.py new file mode 100644 index 0000000000..354e0f3795 --- /dev/null +++ b/blender/arm/lightmapper/properties/atlas.py @@ -0,0 +1,166 @@ +import bpy +from bpy.props import * + +class TLM_PostAtlasListItem(bpy.types.PropertyGroup): + obj: PointerProperty(type=bpy.types.Object, description="The object to bake") + tlm_atlas_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="TODO", + default='256') + + tlm_atlas_repack_on_cleanup : BoolProperty( + name="Repack on cleanup", + description="Postpacking adjusts the UV's. Toggle to resize back to full scale on cleanup.", + default=True) + + tlm_atlas_dilation : BoolProperty( + name="Dilation", + description="Adds a blurred background layer that acts as a dilation map.", + default=False) + + tlm_atlas_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + tlm_atlas_merge_samemat : BoolProperty( + name="Merge materials", + description="Merge objects with same materials.", + default=True) + + tlm_postatlas_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + +class TLM_UL_PostAtlasList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + custom_icon = 'OBJECT_DATAMODE' + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + + #In list object counter + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: + amount = amount + 1 + + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.label(text=item.tlm_atlas_lightmap_resolution) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=str(amount)) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class TLM_AtlasListItem(bpy.types.PropertyGroup): + obj: PointerProperty(type=bpy.types.Object, description="The object to bake") + tlm_atlas_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Atlas Lightmap Resolution", + description="TODO", + default='256') + + tlm_atlas_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + tlm_atlas_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="Atlas unwrapping method", + default='SmartProject') + + tlm_atlas_merge_samemat : BoolProperty( + name="Merge materials", + description="Merge objects with same materials.", + default=True) + + tlm_use_uv_packer : BoolProperty( + name="Use UV Packer", + description="UV Packer will be utilized after initial UV mapping for optimized packing.", + default=False) + + tlm_uv_packer_padding : FloatProperty( + name="Padding", + default=2.0, + min=0.0, + max=100.0, + subtype='FACTOR') + + tlm_uv_packer_packing_engine : EnumProperty( + items = [('OP0', 'Efficient', 'Best compromise for speed and space usage.'), + ('OP1', 'High Quality', 'Slowest, but maximum space usage.')], + name = "Packing Engine", + description="Which UV Packer engine to use.", + default='OP0') + +class TLM_UL_AtlasList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + custom_icon = 'OBJECT_DATAMODE' + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: + amount = amount + 1 + + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.label(text=item.tlm_atlas_lightmap_resolution) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=str(amount)) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/integrated.py b/blender/arm/lightmapper/properties/denoiser/integrated.py new file mode 100644 index 0000000000..165de7f784 --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/integrated.py @@ -0,0 +1,4 @@ +import bpy +from bpy.props import * + +class TLM_IntegratedDenoiseEngineProperties(bpy.types.PropertyGroup): \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/oidn.py b/blender/arm/lightmapper/properties/denoiser/oidn.py new file mode 100644 index 0000000000..9ccc0221a0 --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/oidn.py @@ -0,0 +1,40 @@ +import bpy, os +from ...utility import utility +from bpy.props import * + +class TLM_OIDNEngineProperties(bpy.types.PropertyGroup): + tlm_oidn_path : StringProperty( + name="OIDN Path", + description="The path to the OIDN binaries", + default="", + subtype="FILE_PATH") + + tlm_oidn_verbose : BoolProperty( + name="Verbose", + description="TODO") + + tlm_oidn_threads : IntProperty( + name="Threads", + default=0, + min=0, + max=64, + description="Amount of threads to use. Set to 0 for auto-detect.") + + tlm_oidn_maxmem : IntProperty( + name="Tiling max Memory", + default=0, + min=512, + max=32768, + description="Use tiling for memory conservation. Set to 0 to disable tiling.") + + tlm_oidn_affinity : BoolProperty( + name="Set Affinity", + description="TODO") + + tlm_oidn_use_albedo : BoolProperty( + name="Use albedo map", + description="TODO") + + tlm_oidn_use_normal : BoolProperty( + name="Use normal map", + description="TODO") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/denoiser/optix.py b/blender/arm/lightmapper/properties/denoiser/optix.py new file mode 100644 index 0000000000..6b55875b96 --- /dev/null +++ b/blender/arm/lightmapper/properties/denoiser/optix.py @@ -0,0 +1,21 @@ +import bpy +from bpy.props import * + +class TLM_OptixEngineProperties(bpy.types.PropertyGroup): + + tlm_optix_path : StringProperty( + name="Optix Path", + description="TODO", + default="", + subtype="FILE_PATH") + + tlm_optix_verbose : BoolProperty( + name="Verbose", + description="TODO") + + tlm_optix_maxmem : IntProperty( + name="Tiling max Memory", + default=0, + min=512, + max=32768, + description="Use tiling for memory conservation. Set to 0 to disable tiling.") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/filtering.py b/blender/arm/lightmapper/properties/filtering.py new file mode 100644 index 0000000000..b153fe2bae --- /dev/null +++ b/blender/arm/lightmapper/properties/filtering.py @@ -0,0 +1,4 @@ +import bpy +from bpy.props import * + +class TLM_FilteringProperties(bpy.types.PropertyGroup): \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/image.py b/blender/arm/lightmapper/properties/image.py new file mode 100644 index 0000000000..a81169c404 --- /dev/null +++ b/blender/arm/lightmapper/properties/image.py @@ -0,0 +1,26 @@ +import bpy +from bpy.props import * + +class TLM_ImageProperties(bpy.types.PropertyGroup): + tlm_image_scale_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'TODO')], + name = "Scaling engine", + description="TODO", + default='OpenCV') + + #('Native', 'Native', 'TODO'), + + tlm_image_scale_method : EnumProperty( + items = [('Nearest', 'Nearest', 'TODO'), + ('Area', 'Area', 'TODO'), + ('Linear', 'Linear', 'TODO'), + ('Cubic', 'Cubic', 'TODO'), + ('Lanczos', 'Lanczos', 'TODO')], + name = "Scaling method", + description="TODO", + default='Lanczos') + + tlm_image_cache_switch : BoolProperty( + name="Cache for quickswitch", + description="Caches scaled images for quick switching", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/object.py b/blender/arm/lightmapper/properties/object.py new file mode 100644 index 0000000000..7aa1a40747 --- /dev/null +++ b/blender/arm/lightmapper/properties/object.py @@ -0,0 +1,182 @@ +import bpy +from bpy.props import * + +class TLM_ObjectProperties(bpy.types.PropertyGroup): + + addon_keys = bpy.context.preferences.addons.keys() + + tlm_atlas_pointer : StringProperty( + name = "Atlas Group", + description = "", + default = "") + + tlm_postatlas_pointer : StringProperty( + name = "Atlas Group", + description = "Atlas Lightmap Group", + default = "") + + tlm_uvchannel_pointer : StringProperty( + name = "UV Channel", + description = "Select UV Channel to bake to", + default = "") + + tlm_uvchannel_pointer : BoolProperty( + name="Enable Lightmapping", + description="TODO", + default=False) + + tlm_mesh_lightmap_use : BoolProperty( + name="Enable Lightmapping", + description="TODO", + default=False) + + tlm_material_ignore : BoolProperty( + name="Skip material", + description="Ignore material for lightmapped object", + default=False) + + tlm_mesh_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Lightmap Resolution", + description="TODO", + default='256') + + unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'), + ('SmartProject', 'Smart Project', 'TODO'), + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB + name="Postpack object", + description="Postpack object into an AtlasGroup", + default=False) + + if "blender_xatlas" in addon_keys: + unwrap_modes.append(('Xatlas', 'Xatlas', 'TODO')) + + tlm_mesh_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, + name = "Unwrap Mode", + description="TODO", + default='SmartProject') + + tlm_mesh_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + tlm_mesh_filter_override : BoolProperty( + name="Override filtering", + description="Override the scene specific filtering", + default=False) + + #FILTERING SETTINGS GROUP + tlm_mesh_filtering_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'Make use of OpenCV based image filtering (Requires it to be installed first in the preferences panel)'), + ('Numpy', 'Numpy', 'Make use of Numpy based image filtering (Integrated)')], + name = "Filtering library", + description="Select which filtering library to use.", + default='Numpy') + + #Numpy Filtering options + tlm_mesh_numpy_filtering_mode : EnumProperty( + items = [('Blur', 'Blur', 'Basic blur filtering.')], + name = "Filter", + description="TODO", + default='Blur') + + #OpenCV Filtering options + tlm_mesh_filtering_mode : EnumProperty( + items = [('Box', 'Box', 'Basic box blur'), + ('Gaussian', 'Gaussian', 'Gaussian blurring'), + ('Bilateral', 'Bilateral', 'Edge-aware filtering'), + ('Median', 'Median', 'Median blur')], + name = "Filter", + description="TODO", + default='Median') + + tlm_mesh_filtering_gaussian_strength : IntProperty( + name="Gaussian Strength", + default=3, + min=1, + max=50) + + tlm_mesh_filtering_iterations : IntProperty( + name="Filter Iterations", + default=5, + min=1, + max=50) + + tlm_mesh_filtering_box_strength : IntProperty( + name="Box Strength", + default=1, + min=1, + max=50) + + tlm_mesh_filtering_bilateral_diameter : IntProperty( + name="Pixel diameter", + default=3, + min=1, + max=50) + + tlm_mesh_filtering_bilateral_color_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_mesh_filtering_bilateral_coordinate_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_mesh_filtering_median_kernel : IntProperty( + name="Median kernel", + default=3, + min=1, + max=5) + + tlm_use_default_channel : BoolProperty( + name="Use default UV channel", + description="Will either use or create the default UV Channel 'UVMap_Lightmap' upon build.", + default=True) + + tlm_uv_channel : StringProperty( + name = "UV Channel", + description = "Use any custom UV Channel for the lightmap", + default = "UVMap") + + tlm_use_uv_packer : BoolProperty( + name="Use UV Packer", + description="UV Packer will be utilized after initial UV mapping for optimized packing.", + default=False) + + tlm_uv_packer_padding : FloatProperty( + name="Padding", + default=2.0, + min=0.0, + max=100.0, + subtype='FACTOR') + + tlm_uv_packer_packing_engine : EnumProperty( + items = [('OP0', 'Efficient', 'Best compromise for speed and space usage.'), + ('OP1', 'High Quality', 'Slowest, but maximum space usage.')], + name = "Packing Engine", + description="Which UV Packer engine to use.", + default='OP0') + + #Padding + #Type + #Rescale + #Pre-rotate \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/cycles.py b/blender/arm/lightmapper/properties/renderer/cycles.py new file mode 100644 index 0000000000..f4ac212abb --- /dev/null +++ b/blender/arm/lightmapper/properties/renderer/cycles.py @@ -0,0 +1,115 @@ +import bpy +from bpy.props import * + +class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): + + tlm_mode : EnumProperty( + items = [('CPU', 'CPU', 'Use the processor to bake textures'), + ('GPU', 'GPU', 'Use the graphics card to bake textures')], + name = "Device", + description="Select whether to use the CPU or the GPU for baking", + default="CPU") + + tlm_quality : EnumProperty( + items = [('0', 'Exterior Preview', 'Best for fast exterior previz'), + ('1', 'Interior Preview', 'Best for fast interior previz with bounces'), + ('2', 'Medium', 'Best for complicated interior preview and final for isometric environments'), + ('3', 'High', 'Best used for final baking for 3rd person games'), + ('4', 'Production', 'Best for first-person and Archviz'), + ('5', 'Custom', 'Uses the cycles sample settings provided the user')], + name = "Quality", + description="Select baking quality", + default="0") + + targets = [('texture', 'Image texture', 'Build to image texture')] + if (2, 92, 0) >= bpy.app.version: + targets.append(('vertex', 'Vertex colors', 'Build to vertex colors')) + + tlm_target : EnumProperty( + items = targets, + name = "Build Target", + description="Select target to build to", + default="texture") + + tlm_resolution_scale : EnumProperty( + items = [('1', '1/1', '1'), + ('2', '1/2', '2'), + ('4', '1/4', '4'), + ('8', '1/8', '8')], + name = "Resolution scale", + description="Select resolution scale", + default="2") + + tlm_setting_supersample : EnumProperty( + items = [('none', 'None', 'No supersampling'), + ('2x', '2x', 'Double supersampling'), + ('4x', '4x', 'Quadruple supersampling')], + name = "Supersampling", + description="Supersampling scale", + default="none") + + tlm_bake_mode : EnumProperty( + items = [('Background', 'Background', 'More overhead; allows for network.'), + ('Foreground', 'Foreground', 'Direct in-session bake')], + name = "Baking mode", + description="Select bake mode", + default="Foreground") + + caching_modes = [('Copy', 'Copy', 'More overhead; allows for network.')] + + #caching_modes.append(('Cache', 'Cache', 'Cache in separate blend'),('Node', 'Node restore', 'EXPERIMENTAL! Use with care')) + + tlm_caching_mode : EnumProperty( + items = caching_modes, + name = "Caching mode", + description="Select cache mode", + default="Copy") + + tlm_directional_mode : EnumProperty( + items = [('None', 'None', 'No directional information'), + ('Normal', 'Baked normal', 'Baked normal maps are taken into consideration')], + name = "Directional mode", + description="Select directional mode", + default="None") + + tlm_lightmap_savedir : StringProperty( + name="Lightmap Directory", + description="TODO", + default="Lightmaps", + subtype="FILE_PATH") + + tlm_dilation_margin : IntProperty( + name="Dilation margin", + default=4, + min=1, + max=64, + subtype='PIXEL') + + tlm_exposure_multiplier : FloatProperty( + name="Exposure Multiplier", + default=0, + description="0 to disable. Multiplies GI value") + + tlm_metallic_handling_mode : EnumProperty( + items = [('ignore', 'Ignore', 'No directional information'), + ('clamp', 'Clamp', 'Clamp to value 0.9'), + ('zero', 'Zero', 'Temporarily set to 0 during baking, and reapply after')], + name = "Metallic handling", + description="Set metallic handling mode to prevent black-baking.", + default="ignore") + + tlm_lighting_mode : EnumProperty( + items = [('combined', 'Combined', 'Bake combined lighting'), + ('combinedao', 'Combined+AO', 'Bake combined lighting with Ambient Occlusion'), + ('indirect', 'Indirect', 'Bake indirect lighting'), + ('indirectao', 'Indirect+AO', 'Bake indirect lighting with Ambient Occlusion'), + ('ao', 'AO', 'Bake only Ambient Occlusion'), + ('complete', 'Complete', 'Bake complete map')], + name = "Lighting mode", + description="TODO.", + default="combined") + + tlm_premultiply_ao : BoolProperty( + name="Premultiply AO", + description="Ambient Occlusion will be premultiplied together with lightmaps, requiring less textures.", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/luxcorerender.py b/blender/arm/lightmapper/properties/renderer/luxcorerender.py new file mode 100644 index 0000000000..2b6a531efa --- /dev/null +++ b/blender/arm/lightmapper/properties/renderer/luxcorerender.py @@ -0,0 +1,11 @@ +import bpy +from bpy.props import * + +class TLM_LuxCoreSceneProperties(bpy.types.PropertyGroup): + + #Luxcore specific here + tlm_luxcore_dir : StringProperty( + name="Luxcore Directory", + description="Standalone path to your LuxCoreRender binary.", + default="", + subtype="FILE_PATH") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/octanerender.py b/blender/arm/lightmapper/properties/renderer/octanerender.py new file mode 100644 index 0000000000..8c66cf13a6 --- /dev/null +++ b/blender/arm/lightmapper/properties/renderer/octanerender.py @@ -0,0 +1,10 @@ +import bpy +from bpy.props import * + +class TLM_OctanerenderSceneProperties(bpy.types.PropertyGroup): + + tlm_lightmap_savedir : StringProperty( + name="Lightmap Directory", + description="TODO", + default="Lightmaps", + subtype="FILE_PATH") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/radeonrays.py b/blender/arm/lightmapper/properties/renderer/radeonrays.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/properties/scene.py b/blender/arm/lightmapper/properties/scene.py new file mode 100644 index 0000000000..740899d0d1 --- /dev/null +++ b/blender/arm/lightmapper/properties/scene.py @@ -0,0 +1,585 @@ +import bpy, os +from bpy.props import * +from .. utility import utility + +def transfer_load(): + load_folder = bpy.context.scene.TLM_SceneProperties.tlm_load_folder + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + #transfer_assets(True, load_folder, lightmap_folder) + +class TLM_SceneProperties(bpy.types.PropertyGroup): + + engines = [('Cycles', 'Cycles', 'Use Cycles for lightmapping')] + + #engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) + #engines.append(('OctaneRender', 'Octane Render', 'Use Octane Render for lightmapping')) + + tlm_atlas_pointer : StringProperty( + name = "Atlas Group", + description = "Atlas Lightmap Group", + default = "") + + tlm_postatlas_pointer : StringProperty( + name = "Atlas Group", + description = "Atlas Lightmap Group", + default = "") + + tlm_lightmap_engine : EnumProperty( + items = engines, + name = "Lightmap Engine", + description="Select which lightmap engine to use.", + default='Cycles') + + #SETTINGS GROUP + tlm_setting_clean_option : EnumProperty( + items = [('Clean', 'Full Clean', 'Clean lightmap directory and revert all materials'), + ('CleanMarked', 'Clean marked', 'Clean only the objects marked for lightmapping')], + name = "Clean mode", + description="The cleaning mode, either full or partial clean. Be careful that you don't delete lightmaps you don't intend to delete.", + default='Clean') + + tlm_setting_keep_cache_files : BoolProperty( + name="Keep cache files", + description="Keep cache files (non-filtered and non-denoised)", + default=True) + + tlm_keep_baked_files : BoolProperty( + name="Keep bake files", + description="Keep the baked lightmap files when cleaning", + default=False) + + tlm_repartition_on_clean : BoolProperty( + name="Repartition on clean", + description="Repartition material names on clean", + default=False) + + tlm_setting_renderer : EnumProperty( + items = [('CPU', 'CPU', 'Bake using the processor'), + ('GPU', 'GPU', 'Bake using the graphics card')], + name = "Device", + description="Select whether to use the CPU or the GPU", + default="CPU") + + tlm_setting_scale : EnumProperty( + items = [('8', '1/8', '1/8th of set scale'), + ('4', '1/4', '1/4th of set scale'), + ('2', '1/2', 'Half of set scale'), + ('1', '1/1', 'Full scale')], + name = "Lightmap Resolution scale", + description="Lightmap resolution scaling. Adjust for previewing.", + default="1") + + tlm_setting_supersample : EnumProperty( + items = [('2x', '2x', 'Double the sampling resolution'), + ('4x', '4x', 'Quadruple the sampling resolution')], + name = "Lightmap Supersampling", + description="Supersamples the baked lightmap. Increases bake time", + default="2x") + + tlm_setting_savedir : StringProperty( + name="Lightmap Directory", + description="Your baked lightmaps will be stored here.", + default="Lightmaps", + subtype="FILE_PATH") + + tlm_setting_exposure_multiplier : FloatProperty( + name="Exposure Multiplier", + default=0, + description="0 to disable. Multiplies GI value") + + tlm_alert_on_finish : BoolProperty( + name="Alert on finish", + description="Play a sound when the lightmaps are done.", + default=False) + + tlm_setting_apply_scale : BoolProperty( + name="Apply scale", + description="Apply the scale before unwrapping.", + default=True) + + tlm_play_sound : BoolProperty( + name="Play sound on finish", + description="Play sound on finish", + default=False) + + tlm_compile_statistics : BoolProperty( + name="Compile statistics", + description="Compile time statistics in the lightmap folder.", + default=True) + + tlm_apply_on_unwrap : BoolProperty( + name="Apply scale", + description="TODO", + default=False) + + tlm_save_preprocess_lightmaps : BoolProperty( + name="Save preprocessed lightmaps", + description="TODO", + default=False) + + #DENOISE SETTINGS GROUP + tlm_denoise_use : BoolProperty( + name="Enable denoising", + description="Enable denoising for lightmaps", + default=False) + + tlm_denoise_engine : EnumProperty( + items = [('Integrated', 'Integrated', 'Use the Blender native denoiser (Compositor; Slow)'), + ('OIDN', 'Intel Denoiser', 'Use Intel denoiser (CPU powered)'), + ('Optix', 'Optix Denoiser', 'Use Nvidia Optix denoiser (GPU powered)')], + name = "Denoiser", + description="Select which denoising engine to use.", + default='Integrated') + + #FILTERING SETTINGS GROUP + tlm_filtering_use : BoolProperty( + name="Enable denoising", + description="Enable denoising for lightmaps", + default=False) + + tlm_filtering_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'Make use of OpenCV based image filtering (Requires it to be installed first in the preferences panel)'), + ('Shader', 'Shader', 'Make use of GPU offscreen shader to filter')], + name = "Filtering library", + description="Select which filtering library to use.", + default='OpenCV') + + #Numpy Filtering options + tlm_numpy_filtering_mode : EnumProperty( + items = [('Blur', 'Blur', 'Basic blur filtering.')], + name = "Filter", + description="TODO", + default='Blur') + + #OpenCV Filtering options + tlm_filtering_mode : EnumProperty( + items = [('Box', 'Box', 'Basic box blur'), + ('Gaussian', 'Gaussian', 'Gaussian blurring'), + ('Bilateral', 'Bilateral', 'Edge-aware filtering'), + ('Median', 'Median', 'Median blur')], + name = "Filter", + description="TODO", + default='Median') + + tlm_filtering_gaussian_strength : IntProperty( + name="Gaussian Strength", + default=3, + min=1, + max=50) + + tlm_filtering_iterations : IntProperty( + name="Filter Iterations", + default=5, + min=1, + max=50) + + tlm_filtering_box_strength : IntProperty( + name="Box Strength", + default=1, + min=1, + max=50) + + tlm_filtering_bilateral_diameter : IntProperty( + name="Pixel diameter", + default=3, + min=1, + max=50) + + tlm_filtering_bilateral_color_deviation : IntProperty( + name="Color deviation", + default=75, + min=1, + max=100) + + tlm_filtering_bilateral_coordinate_deviation : IntProperty( + name="Coordinate deviation", + default=75, + min=1, + max=100) + + tlm_filtering_median_kernel : IntProperty( + name="Median kernel", + default=3, + min=1, + max=5) + + tlm_clamp_hdr : BoolProperty( + name="Enable HDR Clamp", + description="Clamp HDR Value", + default=False) + + tlm_clamp_hdr_value : IntProperty( + name="HDR Clamp value", + default=10, + min=0, + max=20) + + #Encoding properties + tlm_encoding_use : BoolProperty( + name="Enable encoding", + description="Enable encoding for lightmaps", + default=False) + + tlm_encoding_device : EnumProperty( + items = [('CPU', 'CPU', 'Todo'), + ('GPU', 'GPU', 'Todo.')], + name = "Encoding Device", + description="TODO", + default='CPU') + + encoding_modes_1 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), + ('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] + + encoding_modes_2 = [('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), + ('LogLuv', 'LogLuv', '8-bit HDR encoding. Different.'), + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] + + tlm_encoding_mode_a : EnumProperty( + items = encoding_modes_1, + name = "Encoding Mode", + description="TODO", + default='HDR') + + tlm_encoding_mode_b : EnumProperty( + items = encoding_modes_2, + name = "Encoding Mode", + description="RGBE 32-bit Radiance HDR File", + default='HDR') + + tlm_encoding_range : IntProperty( + name="Encoding range", + description="Higher gives a larger HDR range, but also gives more banding.", + default=6, + min=1, + max=255) + + tlm_decoder_setup : BoolProperty( + name="Use decoder", + description="Apply a node for decoding.", + default=False) + + tlm_split_premultiplied : BoolProperty( + name="Split for premultiplied", + description="Some game engines doesn't support non-premultiplied files. This splits the alpha channel to a separate file.", + default=False) + + tlm_encoding_colorspace : EnumProperty( + items = [('XYZ', 'XYZ', 'TODO'), + ('sRGB', 'sRGB', 'TODO'), + ('NonColor', 'Non-Color', 'TODO'), + ('ACES', 'Linear ACES', 'TODO'), + ('Linear', 'Linear', 'TODO'), + ('FilmicLog', 'Filmic Log', 'TODO')], + name = "Color Space", + description="TODO", + default='Linear') + + tlm_compression : IntProperty( + name="PNG Compression", + description="0 = No compression. 100 = Maximum compression.", + default=0, + min=0, + max=100) + + tlm_format : EnumProperty( + items = [('RGBE', 'HDR', '32-bit RGBE encoded .hdr files. No compression available.'), + ('EXR', 'EXR', '32-bit OpenEXR format.')], + name = "Format", + description="Select default 32-bit format", + default='RGBE') + + tlm_override_object_settings : BoolProperty( + name="Override settings", + description="TODO", + default=False) + + tlm_mesh_lightmap_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Lightmap Resolution", + description="TODO", + default='256') + + tlm_mesh_lightmap_unwrap_mode : EnumProperty( + items = [('Lightmap', 'Lightmap', 'TODO'), + ('SmartProject', 'Smart Project', 'TODO'), + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.'), + ('Xatlas', 'Xatlas', 'TODO')], + name = "Unwrap Mode", + description="TODO", + default='SmartProject') + + tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB + name="Postpack object", + description="Postpack object into an AtlasGroup", + default=False) + + tlm_mesh_unwrap_margin : FloatProperty( + name="Unwrap Margin", + default=0.1, + min=0.0, + max=1.0, + subtype='FACTOR') + + tlm_headless : BoolProperty( + name="Don't apply materials", + description="Headless; Do not apply baked materials on finish.", + default=False) + + tlm_atlas_mode : EnumProperty( + items = [('Prepack', 'Pre-packing', 'Todo.'), + ('Postpack', 'Post-packing', 'Todo.')], + name = "Atlas mode", + description="TODO", + default='Prepack') + + tlm_alert_sound : EnumProperty( + items = [('dash', 'Dash', 'Dash alert'), + ('noot', 'Noot', 'Noot alert'), + ('gentle', 'Gentle', 'Gentle alert'), + ('pingping', 'Ping', 'Ping alert')], + name = "Alert sound", + description="Alert sound when lightmap building finished.", + default="gentle") + + tlm_metallic_clamp : EnumProperty( + items = [('ignore', 'Ignore', 'Ignore clamping'), + ('skip', 'Skip', 'Skip baking metallic materials'), + ('zero', 'Zero', 'Set zero'), + ('limit', 'Limit', 'Clamp to 0.9')], + name = "Metallic clamping", + description="TODO.", + default="ignore") + + tlm_texture_interpolation : EnumProperty( + items = [('Smart', 'Smart', 'Bicubic when magnifying.'), + ('Cubic', 'Cubic', 'Cubic interpolation'), + ('Closest', 'Closest', 'No interpolation'), + ('Linear', 'Linear', 'Linear')], + name = "Texture interpolation", + description="Texture interpolation.", + default="Linear") + + tlm_texture_extrapolation : EnumProperty( + items = [('REPEAT', 'Repeat', 'Repeat in both direction.'), + ('EXTEND', 'Extend', 'Extend by repeating edge pixels.'), + ('CLIP', 'Clip', 'Clip to image size')], + name = "Texture extrapolation", + description="Texture extrapolation.", + default="EXTEND") + + tlm_verbose : BoolProperty( + name="Verbose", + description="Verbose console output", + default=False) + + tlm_compile_statistics : BoolProperty( + name="Compile statistics", + description="Compile lightbuild statistics", + default=False) + + tlm_override_bg_color : BoolProperty( + name="Override background", + description="Override background color, black by default.", + default=False) + + tlm_override_color : FloatVectorProperty(name="Color", + description="Background color for baked maps", + subtype='COLOR', + default=[0.5,0.5,0.5]) + + tlm_reset_uv : BoolProperty( + name="Remove Lightmap UV", + description="Remove existing UV maps for lightmaps.", + default=False) + + tlm_apply_modifiers : BoolProperty( + name="Apply modifiers", + description="Apply all modifiers to objects.", + default=True) + + tlm_batch_mode : BoolProperty( + name="Batch mode", + description="Batch collections.", + default=False) + + tlm_network_render : BoolProperty( + name="Enable network rendering", + description="Enable network rendering (Unstable).", + default=False) + + tlm_network_paths : PointerProperty( + name="Network file", + description="Network instruction file", + type=bpy.types.Text) + + tlm_network_dir : StringProperty( + name="Network directory", + description="Use a path that is accessible to all your network render devices.", + default="", + subtype="FILE_PATH") + + tlm_cmft_path : StringProperty( + name="CMFT Path", + description="The path to the CMFT binaries", + default="", + subtype="FILE_PATH") + + tlm_create_spherical : BoolProperty( + name="Create spherical texture", + description="Merge cubemap to a 360 spherical texture.", + default=False) + + tlm_write_sh : BoolProperty( + name="Calculate SH coefficients", + description="Calculates spherical harmonics coefficients to a file.", + default=False) + + tlm_write_radiance : BoolProperty( + name="Write radiance images", + description="Writes out the radiance images.", + default=False) + + tlm_invert_direction : BoolProperty( + name="Invert direction", + description="Inverts the direction.", + default=False) + + tlm_environment_probe_resolution : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO'), + ('8192', '8192', 'TODO')], + name = "Probe Resolution", + description="TODO", + default='256') + + tlm_environment_probe_engine : EnumProperty( + items = [('BLENDER_EEVEE', 'Eevee', 'TODO'), + ('CYCLES', 'Cycles', 'TODO')], + name = "Probe Render Engine", + description="TODO", + default='BLENDER_EEVEE') + + tlm_load_folder : StringProperty( + name="Load Folder", + description="Load existing lightmaps from folder", + subtype="DIR_PATH") + + tlm_load_atlas : BoolProperty( + name="Load lightmaps based on atlas sets", + description="Use the current Atlas list.", + default=False) + + tlm_utility_set : EnumProperty( + items = [('Scene', 'Scene', 'Set for all objects in the scene.'), + ('Selection', 'Selection', 'Set for selected objects.'), + ('Enabled', 'Enabled', 'Set for objects that has been enabled for lightmapping.')], + name = "Set", + description="Utility selection set", + default='Scene') + + tlm_resolution_weight : EnumProperty( + items = [('Single', 'Single', 'Set a single resolution for all objects.'), + ('Dimension', 'Dimension', 'Distribute resolutions based on object dimensions.'), + ('Surface', 'Surface', 'Distribute resolutions based on mesh surface area.'), + ('Volume', 'Volume', 'Distribute resolutions based on mesh volume.')], + name = "Resolution weight", + description="Method for setting resolution value", + default='Single') + #Todo add vertex color option + + tlm_resolution_min : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Minimum resolution", + description="Minimum distributed resolution", + default='32') + + tlm_resolution_max : EnumProperty( + items = [('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Maximum resolution", + description="Maximum distributed resolution", + default='256') + + tlm_remove_met_spec_link : BoolProperty( + name="Remove image link", + description="Removes the connected node on metallic or specularity set disable", + default=False) + + tlm_utility_context : EnumProperty( + items = [('SetBatching', 'Set Batching', 'Set batching options. Allows to set lightmap options for multiple objects.'), + ('EnvironmentProbes', 'Environment Probes', 'Options for rendering environment probes. Cubemaps and panoramic HDRs for external engines'), + ('LoadLightmaps', 'Load Lightmaps', 'Options for loading pre-built lightmaps.'), + ('NetworkRender', 'Network Rendering', 'Distribute lightmap building across multiple machines.'), + ('MaterialAdjustment', 'Material Adjustment', 'Allows adjustment of multiple materials at once.'), + ('TexelDensity', 'Texel Density', 'Allows setting texel densities of the UV.'), + ('GLTFUtil', 'GLTF Utilities', 'GLTF related material utilities.')], + name = "Utility Context", + description="Set Utility Context", + default='SetBatching') + + tlm_addon_uimode : EnumProperty( + items = [('Simple', 'Simple', 'TODO'), + ('Advanced', 'Advanced', 'TODO')], + name = "UI Mode", + description="TODO", + default='Simple') + +class TLM_GroupListItem(bpy.types.PropertyGroup): + obj: PointerProperty(type=bpy.types.Object, description="The object to bake") + +class TLM_UL_GroupList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + custom_icon = 'OBJECT_DATAMODE' + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: + amount = amount + 1 + + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.label(text=item.tlm_atlas_lightmap_resolution) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=str(amount)) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/__init__.py b/blender/arm/lightmapper/utility/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/utility/build.py b/blender/arm/lightmapper/utility/build.py new file mode 100644 index 0000000000..074d8bfcff --- /dev/null +++ b/blender/arm/lightmapper/utility/build.py @@ -0,0 +1,1361 @@ +import bpy, os, subprocess, sys, platform, aud, json, datetime, socket + +from . import encoding, pack, log +from . cycles import lightmap, prepare, nodes, cache +from . luxcore import setup +from . octane import configure, lightmap2 +from . denoiser import integrated, oidn, optix +from . filtering import opencv +from . gui import Viewport +from .. network import client + +from os import listdir +from os.path import isfile, join +from time import time, sleep +from importlib import util + +previous_settings = {} +postprocess_shutdown = False +logging = True + +def prepare_build(self=0, background_mode=False, shutdown_after_build=False): + + global tlm_log + tlm_log = log.TLM_Logman() + bpy.app.driver_namespace["logman"] = tlm_log + tlm_log.append("Preparing build") + + if shutdown_after_build: + postprocess_shutdown = True + + print("Building lightmaps") + + if bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + scene = bpy.context.scene + + if not "tlm_plus_mode" in bpy.app.driver_namespace or bpy.app.driver_namespace["tlm_plus_mode"] == 0: + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + bpy.app.driver_namespace["tlm_plus_mode"] = 1 + print("Plus Mode") + + if bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode == "indirectao": + + scene = bpy.context.scene + + if not "tlm_plus_mode" in bpy.app.driver_namespace or bpy.app.driver_namespace["tlm_plus_mode"] == 0: + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + bpy.app.driver_namespace["tlm_plus_mode"] = 1 + print("Plus Mode") + + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Foreground" or background_mode==True: + + global start_time + start_time = time() + bpy.app.driver_namespace["tlm_start_time"] = time() + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if not background_mode and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao" and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "indirectao": + #pass + setGui(1) + + if check_save(): + print("Please save your file first") + self.report({'INFO'}, "Please save your file first") + setGui(0) + return{'FINISHED'} + + if check_denoiser(): + print("No denoise OIDN path assigned") + self.report({'INFO'}, "No denoise OIDN path assigned. Check that it points to the correct executable.") + setGui(0) + return{'FINISHED'} + + if check_materials(): + print("Error with material") + self.report({'INFO'}, "Error with material") + setGui(0) + return{'FINISHED'} + + if opencv_check(): + if sceneProperties.tlm_filtering_use: + print("Error:Filtering - OpenCV not installed") + self.report({'INFO'}, "Error:Filtering - OpenCV not installed") + setGui(0) + return{'FINISHED'} + + setMode() + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if not os.path.isdir(dirpath): + os.mkdir(dirpath) + + #Naming check + naming_check() + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + prepare.init(self, previous_settings) + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + setup.init(self, previous_settings) + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + configure.init(self, previous_settings) + + begin_build() + + else: + + print("Baking in background") + + filepath = bpy.data.filepath + + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + + start_time = time() + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + #Timer start here bound to global + if check_save(): + print("Please save your file first") + self.report({'INFO'}, "Please save your file first") + return{'FINISHED'} + + if check_denoiser(): + print("No denoise OIDN path assigned") + self.report({'INFO'}, "No denoise OIDN path assigned") + return{'FINISHED'} + + if check_materials(): + print("Error with material") + self.report({'INFO'}, "Error with material") + return{'FINISHED'} + + if opencv_check(): + if sceneProperties.tlm_filtering_use: + print("Error:Filtering - OpenCV not installed") + self.report({'INFO'}, "Error:Filtering - OpenCV not installed") + return{'FINISHED'} + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if not os.path.isdir(dirpath): + os.mkdir(dirpath) + + #Naming check + naming_check() + + if scene.TLM_SceneProperties.tlm_network_render: + + print("NETWORK RENDERING") + + if scene.TLM_SceneProperties.tlm_network_paths != None: + HOST = bpy.data.texts[scene.TLM_SceneProperties.tlm_network_paths.name].lines[0].body # The server's hostname or IP address + else: + HOST = '127.0.0.1' # The server's hostname or IP address + + PORT = 9898 # The port used by the server + + client.connect_client(HOST, PORT, bpy.data.filepath, 0) + + finish_assemble() + + else: + + print("Background driver process") + + bpy.app.driver_namespace["alpha"] = 0 + + bpy.app.driver_namespace["tlm_process"] = False + + if os.path.exists(os.path.join(dirpath, "process.tlm")): + os.remove(os.path.join(dirpath, "process.tlm")) + + bpy.app.timers.register(distribute_building) + +def distribute_building(): + + print("Distributing lightmap building") + + #CHECK IF THERE'S AN EXISTING SUBPROCESS + + if not os.path.isfile(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir, "process.tlm")): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("No process file - Creating one...") + tlm_log.append("No process file - Creating one...") + + write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + blendPath = bpy.data.filepath + + process_status = [blendPath, + {'bake': 'all', + 'completed': False + }] + + with open(os.path.join(write_directory, "process.tlm"), 'w') as file: + json.dump(process_status, file, indent=2) + + if (2, 91, 0) > bpy.app.version: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + tlm_log.append("Started process: " + str(bpy.app.driver_namespace["tlm_process"]) + " at " + str(datetime.datetime.now())) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Started process: " + str(bpy.app.driver_namespace["tlm_process"]) + " at " + str(datetime.datetime.now())) + + else: + + write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + + if process_status[1]["completed"]: + + tlm_log.append("Baking finished from process. Status: Completed.") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Baking finished") + + bpy.app.timers.unregister(distribute_building) + + finish_assemble() + + else: + + #Open the json and check the status! + tlm_log.append("Process check: Baking in progress.") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Baking in progress") + + process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + + tlm_log.append(process_status) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(process_status) + + return 1.0 + + +def finish_assemble(self=0, background_pass=0, load_atlas=0): + + print("Finishing assembly") + + tlm_log = log.TLM_Logman() + tlm_log.append("Preparing build") + + if load_atlas: + print("Assembly in Atlas load mode") + tlm_log.append("Assembly in Atlas load mode") + + tlm_log.append("Background baking finished") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Background baking finished") + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + prepare.init(self, previous_settings) + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + pass + + if not 'start_time' in globals(): + global start_time + start_time = time() + + if background_pass: + manage_build(True, load_atlas) + else: + manage_build(False, load_atlas) + +def begin_build(): + + print("Beginning build") + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + lightmap.bake() + + # try: + # lightmap.bake() + # except Exception as e: + + # print("An error occured during lightmap baking. See the line below for more detail:") + # print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + # tlm_log.append("An error occured during lightmap baking. See the line below for more detail:") + # tlm_log.append(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + # if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + # print("Turn on verbose mode to get more detail.") + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + lightmap2.bake() + + #Denoiser + if sceneProperties.tlm_denoise_use: + + if sceneProperties.tlm_denoise_engine == "Integrated": + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + tlm_log.append(baked_image_array) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(baked_image_array) + + denoiser = integrated.TLM_Integrated_Denoise() + + denoiser.load(baked_image_array) + + denoiser.setOutputDir(dirpath) + + denoiser.denoise() + + elif sceneProperties.tlm_denoise_engine == "OIDN": + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + oidnProperties = scene.TLM_OIDNEngineProperties + + denoiser = oidn.TLM_OIDN_Denoise(oidnProperties, baked_image_array, dirpath) + + try: + denoiser.denoise() + except Exception as e: + + print("An error occured during denoising. See the line below for more detail:") + print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + tlm_log.append("An error occured during denoising. See the line below for more detail:") + tlm_log.append(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Turn on verbose mode to get more detail.") + + denoiser.clean() + + del denoiser + + else: + + baked_image_array = [] + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + optixProperties = scene.TLM_OptixEngineProperties + + denoiser = optix.TLM_Optix_Denoise(optixProperties, baked_image_array, dirpath) + + denoiser.denoise() + + denoiser.clean() + + del denoiser + + #Filtering + if sceneProperties.tlm_filtering_use: + + if sceneProperties.tlm_denoise_use: + useDenoise = True + else: + useDenoise = False + + filter = opencv.TLM_CV_Filtering + + try: + filter.init(dirpath, useDenoise) + + except Exception as e: + + print("An error occured during filtering. See the line below for more detail:") + print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + tlm_log.append("An error occured during filtering. See the line below for more detail:") + tlm_log.append(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Turn on verbose mode to get more detail.") + + #Encoding + if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background": + + if sceneProperties.tlm_encoding_device == "CPU": + + if sceneProperties.tlm_encoding_mode_a == "HDR": + + if sceneProperties.tlm_format == "EXR": + + tlm_log.append("EXR Format") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "OPEN_EXR" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".exr") + + if sceneProperties.tlm_encoding_mode_a == "RGBM": + + tlm_log.append("ENCODING RGBM") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("ENCODING RGBM") + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + tlm_log.append("Encoding:" + str(file)) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Encoding:" + str(file)) + encoding.encodeImageRGBMCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + + if sceneProperties.tlm_encoding_mode_a == "RGBD": + + tlm_log.append("ENCODING RGBD") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("ENCODING RGBD") + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + tlm_log.append("Encoding:" + str(file)) + print("Encoding:" + str(file)) + encoding.encodeImageRGBDCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + + if sceneProperties.tlm_encoding_mode_a == "SDR": + + tlm_log.append("EXR Format") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + + else: + + if sceneProperties.tlm_encoding_mode_b == "HDR": + + if sceneProperties.tlm_format == "EXR": + + tlm_log.append("EXR Format") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "OPEN_EXR" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".exr") + + if sceneProperties.tlm_encoding_mode_b == "LogLuv": + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #CHECK FOR ATLAS MAPS! + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + encoding.encodeLogLuvGPU(img, dirpath, 0) + + if sceneProperties.tlm_split_premultiplied: + + image_name = img.name + + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + image_name = image_name + '_encoded.png' + + print("SPLIT PREMULTIPLIED: " + image_name) + encoding.splitLogLuvAlpha(os.path.join(dirpath, image_name), dirpath, 0) + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + + tlm_log.append("ENCODING RGBM") + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print("ENCODING RGBM") + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Encoding:" + str(file)) + encoding.encodeImageRGBMGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + + if sceneProperties.tlm_encoding_mode_b == "RGBD": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ENCODING RGBD") + + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Encoding:" + str(file)) + encoding.encodeImageRGBDGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + + if sceneProperties.tlm_encoding_mode_b == "PNG": + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + + manage_build() + +def manage_build(background_pass=False, load_atlas=0): + + print("Managing build") + + if load_atlas: + print("Managing in load atlas mode") + + scene = bpy.context.scene + sceneProperties = scene.TLM_SceneProperties + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + if background_pass: + print("In background pass") + + try: + + nodes.apply_lightmaps() + + except Exception as e: + + print("An error occured during lightmap application. See the line below for more detail:") + print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + tlm_log.append("An error occured during lightmap application. See the line below for more detail:") + tlm_log.append(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Turn on verbose mode to get more detail.") + + + try: + nodes.apply_materials(load_atlas) #From here the name is changed... + + except Exception as e: + + print("An error occured during material application. See the line below for more detail:") + print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + tlm_log.append("An error occured during material application. See the line below for more detail:") + tlm_log.append(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Turn on verbose mode to get more detail.") + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + formatEnc = ".hdr" + + if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background": + + if sceneProperties.tlm_encoding_device == "CPU": + + print("CPU Encoding") + + if sceneProperties.tlm_encoding_mode_a == "HDR": + + if sceneProperties.tlm_format == "EXR": + + formatEnc = ".exr" + + if sceneProperties.tlm_encoding_mode_a == "RGBM": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode_a == "RGBD": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode_a == "SDR": + + formatEnc = ".png" + + else: + + print("GPU Encoding") + + if sceneProperties.tlm_encoding_mode_b == "HDR": + + if sceneProperties.tlm_format == "EXR": + + formatEnc = ".exr" + + if sceneProperties.tlm_encoding_mode_b == "LogLuv": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode_b == "RGBD": + + formatEnc = "_encoded.png" + + if sceneProperties.tlm_encoding_mode_b == "SDR": + + formatEnc = ".png" + + if not background_pass: + nodes.exchangeLightmapsToPostfix("_baked", end, formatEnc) + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + pack.postpack() + #We need to also make sure out postpacked atlases gets split w. premultiplied + #CHECK FOR ATLAS MAPS! + + if bpy.context.scene.TLM_SceneProperties.tlm_split_premultiplied: + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for atlas in bpy.context.scene.TLM_PostAtlasList: + + for file in dirfiles: + if file.startswith(atlas.name): + + print("TODO: SPLIT LOGLUV FOR: " + str(file)) + encoding.splitLogLuvAlpha(os.path.join(dirpath, file), dirpath, 0) + + #Need to update file list for some reason? + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for atlas in bpy.context.scene.TLM_PostAtlasList: + + #FIND SOME WAY TO FIND THE RIGTH FILE! TOO TIRED NOW! + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: + for slot in obj.material_slots: + + mat = slot.material + + node_tree = mat.node_tree + + foundBakedNode = False + + #for file in dirfiles: + # if file.startswith(atlas.name): + # if file.endswith("XYZ"): + + #Find nodes + for node in node_tree.nodes: + + if node.name == "TLM_Lightmap": + + print("Found the main lightmap node: LOGLUV") + + for file in dirfiles: + if file.startswith(atlas.name) and file.endswith("XYZ.png"): + print("Found an atlas file: " + str(file)) + node.image.filepath_raw = os.path.join(dirpath, file) + print("CHANGED LIGHTMAP MAIN INTO XYZ: " + str(file)) + + if node.name == "TLM_Lightmap_Extra": + + print("Found the main lightmap node: LOGLUV") + + for file in dirfiles: + if file.startswith(atlas.name) and file.endswith("W.png"): + print("Found an atlas file: " + str(file)) + node.image.filepath_raw = os.path.join(dirpath, file) + print("CHANGED LIGHTMAP MAIN INTO W: " + str(file)) + + #print("Found the extra lightmap node: LOGLUV") + # if node.image.filepath_raw.startswith(atlas.name): + # if node.image.filepath_raw.endswith("W.png"): + # print("ALREADY W: " + str(node.image.filepath_raw)) + + # else: + + # for file in dirfiles: + # if file.startswith(atlas.name): + # if file.endswith("W.png"): + + # node.image.filepath_raw = os.path.join(dirpath, file) + # print("CHANGED LIGHTMAP MAIN INTO W: " + str(file)) + + #for file in dirfiles: + # if file.endswith(end + ".hdr"): + + #for atlas in bpy.context.scene.TLM_PostAtlasList: + + + + #print("TODO: SPLIT LOGLUV FOR: " + str(atlas.name) + "..file?") + + #CHECK FOR ATLAS MAPS! + #dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + # for file in dirfiles: + # if file.endswith(end + ".hdr"): + + # img = bpy.data.images.load(os.path.join(dirpath, file), check_existing=False) + + # encoding.encodeLogLuvGPU(img, dirpath, 0) + + # if sceneProperties.tlm_split_premultiplied: + + # image_name = img.name + + # if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + # image_name = image_name[:-4] + + # image_name = image_name + '_encoded.png' + + # print("SPLIT PREMULTIPLIED: " + image_name) + # encoding.splitLogLuvAlpha(os.path.join(dirpath, image_name), dirpath, 0) + + for image in bpy.data.images: + if image.users < 1: + bpy.data.images.remove(image) + + if scene.TLM_SceneProperties.tlm_headless: + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + atlasName = obj.TLM_ObjectProperties.tlm_atlas_pointer + img_name = atlasName + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + elif obj.TLM_ObjectProperties.tlm_postpack_object: + atlasName = obj.TLM_ObjectProperties.tlm_postatlas_pointer + img_name = atlasName + '_baked' + ".hdr" + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + else: + img_name = obj.name + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + if "tlm_plus_mode" in bpy.app.driver_namespace: #First DIR pass + + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: #First DIR pass + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = extension = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_dir" + extension)) + + print("First DIR pass complete") + + bpy.app.driver_namespace["tlm_plus_mode"] = 2 + + prepare_build(self=0, background_mode=False, shutdown_after_build=False) + + if not background_pass and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + + filepath = bpy.data.filepath + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + if not filename.endswith("_dir"): + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_ao" + extension)) + + print("Second AO pass complete") + + total_time = sec_to_hours((time() - start_time)) + tlm_log.append(total_time) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + + print(total_time) + + bpy.context.scene["TLM_Buildstat"] = total_time + + reset_settings(previous_settings["settings"]) + + bpy.app.driver_namespace["tlm_plus_mode"] = 0 + + if not background_pass: + + #TODO CHANGE! + + nodes.exchangeLightmapsToPostfix(end, end + "_dir", formatEnc) + + nodes.applyAOPass() + + else: + + total_time = sec_to_hours((time() - start_time)) + tlm_log.append(total_time) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(total_time) + + bpy.context.scene["TLM_Buildstat"] = total_time + + reset_settings(previous_settings["settings"]) + + tlm_log.append("Lightmap building finished") + tlm_log.append("--------------------------") + print("Lightmap building finished") + + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + pass + + if sceneProperties.tlm_lightmap_engine == "OctaneRender": + + pass + + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": + pass + + if not background_pass and scene.TLM_EngineProperties.tlm_bake_mode != "Background" and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + if scene.TLM_SceneProperties.tlm_alert_on_finish: + + alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + + if alertSelect == "dash": + soundfile = "dash.ogg" + elif alertSelect == "pingping": + soundfile = "pingping.ogg" + elif alertSelect == "gentle": + soundfile = "gentle.ogg" + else: + soundfile = "noot.ogg" + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) + + device = aud.Device() + sound = aud.Sound.file(sound_path) + device.play(sound) + + if logging: + print("Log file output:") + tlm_log.dumpLog() + + if bpy.app.background: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Writing background process report") + + write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + if os.path.exists(os.path.join(write_directory, "process.tlm")): + + process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + + process_status[1]["completed"] = True + + with open(os.path.join(write_directory, "process.tlm"), 'w') as file: + json.dump(process_status, file, indent=2) + + if postprocess_shutdown: + sys.exit() + +#TODO - SET BELOW TO UTILITY + +def reset_settings(prev_settings): + scene = bpy.context.scene + cycles = scene.cycles + + cycles.samples = int(prev_settings[0]) + cycles.max_bounces = int(prev_settings[1]) + cycles.diffuse_bounces = int(prev_settings[2]) + cycles.glossy_bounces = int(prev_settings[3]) + cycles.transparent_max_bounces = int(prev_settings[4]) + cycles.transmission_bounces = int(prev_settings[5]) + cycles.volume_bounces = int(prev_settings[6]) + cycles.caustics_reflective = prev_settings[7] + cycles.caustics_refractive = prev_settings[8] + cycles.device = prev_settings[9] + scene.render.engine = prev_settings[10] + bpy.context.view_layer.objects.active = prev_settings[11] + scene.render.resolution_x = prev_settings[13][0] + scene.render.resolution_y = prev_settings[13][1] + + #for obj in prev_settings[12]: + # obj.select_set(True) + +def naming_check(): + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if obj.name != "": + + if "_" in obj.name: + obj.name = obj.name.replace("_",".") + if " " in obj.name: + obj.name = obj.name.replace(" ",".") + if "[" in obj.name: + obj.name = obj.name.replace("[",".") + if "]" in obj.name: + obj.name = obj.name.replace("]",".") + if "ø" in obj.name: + obj.name = obj.name.replace("ø","oe") + if "æ" in obj.name: + obj.name = obj.name.replace("æ","ae") + if "å" in obj.name: + obj.name = obj.name.replace("å","aa") + if "/" in obj.name: + obj.name = obj.name.replace("/",".") + + for slot in obj.material_slots: + if "_" in slot.material.name: + slot.material.name = slot.material.name.replace("_",".") + if " " in slot.material.name: + slot.material.name = slot.material.name.replace(" ",".") + if "[" in slot.material.name: + slot.material.name = slot.material.name.replace("[",".") + if "[" in slot.material.name: + slot.material.name = slot.material.name.replace("]",".") + if "ø" in slot.material.name: + slot.material.name = slot.material.name.replace("ø","oe") + if "æ" in slot.material.name: + slot.material.name = slot.material.name.replace("æ","ae") + if "å" in slot.material.name: + slot.material.name = slot.material.name.replace("å","aa") + if "/" in slot.material.name: + slot.material.name = slot.material.name.replace("/",".") + +def opencv_check(): + + cv2 = util.find_spec("cv2") + + if cv2 is not None: + return 0 + else: + return 1 + +def check_save(): + if not bpy.data.is_saved: + + return 1 + + else: + + return 0 + +def check_denoiser(): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Checking denoiser path") + + scene = bpy.context.scene + + if scene.TLM_SceneProperties.tlm_denoise_use: + + if scene.TLM_SceneProperties.tlm_denoise_engine == "OIDN": + + oidnPath = scene.TLM_OIDNEngineProperties.tlm_oidn_path + + if scene.TLM_OIDNEngineProperties.tlm_oidn_path == "": + return 1 + + if platform.system() == "Windows": + if not scene.TLM_OIDNEngineProperties.tlm_oidn_path.endswith(".exe"): + return 1 + else: + if os.path.isfile(bpy.path.abspath(scene.TLM_OIDNEngineProperties.tlm_oidn_path)): + return 0 + else: + return 1 + else: + return 0 + +def check_materials(): + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + + if mat is None: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("MatNone") + mat = bpy.data.materials.new(name="Material") + mat.use_nodes = True + slot.material = mat + + nodes = mat.node_tree.nodes + + #TODO FINISH MATERIAL CHECK -> Nodes check + #Afterwards, redo build/utility + +def sec_to_hours(seconds): + a=str(seconds//3600) + b=str((seconds%3600)//60) + c=str(round((seconds%3600)%60,1)) + d=["{} hours {} mins {} seconds".format(a, b, c)] + return d + +def setMode(): + + obj = bpy.context.scene.objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + hidden = False + + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + if not hidden: + bpy.ops.object.mode_set(mode='OBJECT') + + #TODO Make some checks that returns to previous selection + +def setGui(mode): + + if mode == 0: + + context = bpy.context + driver = bpy.app.driver_namespace + + if "TLM_UI" in driver: + driver["TLM_UI"].remove_handle() + + if mode == 1: + + #bpy.context.area.tag_redraw() + context = bpy.context + driver = bpy.app.driver_namespace + driver["TLM_UI"] = Viewport.ViewportDraw(context, "Building Lightmaps") + + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + +def checkAtlasSize(): + + overflow = False + + scene = bpy.context.scene + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + for atlas in bpy.context.scene.TLM_PostAtlasList: + + atlas_resolution = int(int(atlas.tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)) + + utilized = 0 + atlasUsedArea = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: + + atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 + + utilized = atlasUsedArea / (int(atlas_resolution) ** 2) + if (utilized * 100) > 100: + overflow = True + print("Overflow for: " + str(atlas.name)) + + if overflow == True: + return True + else: + return False + diff --git a/blender/arm/lightmapper/utility/cycles/ao.py b/blender/arm/lightmapper/utility/cycles/ao.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/utility/cycles/cache.py b/blender/arm/lightmapper/utility/cycles/cache.py new file mode 100644 index 0000000000..07f068b715 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/cache.py @@ -0,0 +1,124 @@ +import bpy + +#Todo - Check if already exists, in case multiple objects has the same material + +def backup_material_copy(slot): + material = slot.material + dup = material.copy() + dup.name = "." + material.name + "_Original" + dup.use_fake_user = True + +def backup_material_cache(slot, path): + bpy.ops.wm.save_as_mainfile(filepath=path, copy=True) + +def backup_material_cache_restore(slot, path): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Restore cache") + +# def backup_material_restore(obj): #?? +# if bpy.context.scene.TLM_SceneProperties.tlm_verbose: +# print("Restoring material for: " + obj.name) + +#Check if object has TLM_PrevMatArray +# if yes +# - check if array.len is bigger than 0: +# if yes: +# for slot in object: +# originalMaterial = TLM_PrevMatArray[index] +# +# +# if no: +# - In which cases are these? + +# if no: +# - In which cases are there not? +# - If a lightmapped material was applied to a non-lightmap object? + + + # if bpy.data.materials[originalMaterial].users > 0: #TODO - Check if all lightmapped + + # print("Material has multiple users") + + # if originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + # elif "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + + # else: + + # print("Material has one user") + + # if "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + # elif originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + +def backup_material_restore(obj): #?? + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Restoring material for: " + obj.name) + + if "TLM_PrevMatArray" in obj: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Material restore array found: " + str(obj["TLM_PrevMatArray"])) + #Running through the slots + prevMatArray = obj["TLM_PrevMatArray"] + slotsLength = len(prevMatArray) + + if len(prevMatArray) > 0: + for idx, slot in enumerate(obj.material_slots): #For each slot, we get the index + #We only need the index, corresponds to the array index + try: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Attempting to set material") + originalMaterial = prevMatArray[idx] + except IndexError: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Material restore failed - Resetting") + originalMaterial = "" + + if slot.material is not None: + #if slot.material.users < 2: + #slot.material.user_clear() #Seems to be bad; See: https://developer.blender.org/T49837 + #bpy.data.materials.remove(slot.material) + + if "." + originalMaterial + "_Original" in bpy.data.materials: + slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + slot.material.use_fake_user = False + + else: + + print("No previous material for " + obj.name) + + else: + + print("No previous material for " + obj.name) + +def backup_material_rename(obj): #?? + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Renaming material for: " + obj.name) + + + if "TLM_PrevMatArray" in obj: + + for slot in obj.material_slots: + + if slot.material is not None: + if slot.material.name.endswith("_Original"): + newname = slot.material.name[1:-9] + if newname in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Removing material: " + bpy.data.materials[newname].name) + #if bpy.data.materials[newname].users < 2: + #bpy.data.materials.remove(bpy.data.materials[newname]) #TODO - Maybe remove this + slot.material.name = newname + + del obj["TLM_PrevMatArray"] + + else: + + print("No Previous material array for: " + obj.name) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/indirect.py b/blender/arm/lightmapper/utility/cycles/indirect.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/lightmapper/utility/cycles/lightmap.py b/blender/arm/lightmapper/utility/cycles/lightmap.py new file mode 100644 index 0000000000..14347d8464 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/lightmap.py @@ -0,0 +1,179 @@ +import bpy, os +from .. import build +from time import time, sleep + +def bake(plus_pass=0): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Initializing lightmap baking.") + + for obj in bpy.context.scene.objects: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(False) + + iterNum = 0 + currentIterNum = 0 + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + + hidden = False + + #We check if the object is hidden + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + #We check if the object's collection is hidden + collections = obj.users_collection + + for collection in collections: + + if collection.hide_viewport: + hidden = True + if collection.hide_render: + hidden = True + + try: + if collection.name in bpy.context.scene.view_layers[0].layer_collection.children: + if bpy.context.scene.view_layers[0].layer_collection.children[collection.name].hide_viewport: + hidden = True + except: + print("Error: Could not find collection: " + collection.name) + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use and not hidden: + iterNum = iterNum + 1 + + if iterNum > 1: + iterNum = iterNum - 1 + + for obj in bpy.context.scene.objects: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Checking visibility status for object and collections: " + obj.name) + + hidden = False + + #We check if the object is hidden + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + #We check if the object's collection is hidden + collections = obj.users_collection + + for collection in collections: + + if collection.hide_viewport: + hidden = True + if collection.hide_render: + hidden = True + + try: + if collection.name in bpy.context.scene.view_layers[0].layer_collection.children: + if bpy.context.scene.view_layers[0].layer_collection.children[collection.name].hide_viewport: + hidden = True + except: + print("Error: Could not find collection: " + collection.name) + + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use and not hidden: + + scene = bpy.context.scene + + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + obj.hide_render = False + scene.render.bake.use_clear = False + + #os.system("cls") + + #if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + #elapsed = build.sec_to_hours((time() - bpy.app.driver_namespace["tlm_start_time"])) + #print("Baked: " + str(currentIterNum) + " | Left: " + str(iterNum-currentIterNum)) + elapsedSeconds = time() - bpy.app.driver_namespace["tlm_start_time"] + bakedObjects = currentIterNum + bakedLeft = iterNum-currentIterNum + if bakedObjects == 0: + bakedObjects = 1 + averagePrBake = elapsedSeconds / bakedObjects + remaining = averagePrBake * bakedLeft + #print(time() - bpy.app.driver_namespace["tlm_start_time"]) + print("Elapsed time: " + str(round(elapsedSeconds, 2)) + "s | ETA remaining: " + str(round(remaining, 2)) + "s") #str(elapsed[0]) + #print("Averaged: " + str(averagePrBake)) + #print("Remaining: " + str(remaining)) + + if scene.TLM_EngineProperties.tlm_target == "vertex": + scene.render.bake.target = "VERTEX_COLORS" + + if scene.TLM_EngineProperties.tlm_lighting_mode == "combined": + print("Baking combined: Direct + Indirect") + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect": + print("Baking combined: Indirect") + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "ao": + print("Baking combined: AO") + bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + + elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirectao": + + print("IndirAO") + + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: + print("IndirAO: 1") + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + print("IndirAO: 2") + bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + + elif scene.TLM_EngineProperties.tlm_lighting_mode == "complete": + bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + else: + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + + + #Save image between + if scene.TLM_SceneProperties.tlm_save_preprocess_lightmaps: + for image in bpy.data.images: + if image.name.endswith("_baked"): + + saveDir = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + bakemap_path = os.path.join(saveDir, image.name) + filepath_ext = ".hdr" + image.filepath_raw = bakemap_path + filepath_ext + image.file_format = "HDR" + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Saving to: " + image.filepath_raw) + image.save() + + bpy.ops.object.select_all(action='DESELECT') + currentIterNum = currentIterNum + 1 + + for image in bpy.data.images: + if image.name.endswith("_baked"): + + saveDir = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + bakemap_path = os.path.join(saveDir, image.name) + filepath_ext = ".hdr" + image.filepath_raw = bakemap_path + filepath_ext + image.file_format = "HDR" + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Saving to: " + image.filepath_raw) + image.save() diff --git a/blender/arm/lightmapper/utility/cycles/nodes.py b/blender/arm/lightmapper/utility/cycles/nodes.py new file mode 100644 index 0000000000..da5e28bc29 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/nodes.py @@ -0,0 +1,527 @@ +import bpy, os + +def apply_lightmaps(): + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + hidden = False + + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + if not hidden: + + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + scene = bpy.context.scene + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + + #Find nodes + for node in nodes: + if node.name == "Baked Image": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Finding node source for material: " + mat.name + " @ " + obj.name) + + extension = ".hdr" + + postfix = "_baked" + + if scene.TLM_SceneProperties.tlm_denoise_use: + postfix = "_denoised" + if scene.TLM_SceneProperties.tlm_filtering_use: + postfix = "_filtered" + + if node.image: + node.image.source = "FILE" + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + print("Atlas object image") + image_name = obj.TLM_ObjectProperties.tlm_atlas_pointer + postfix + extension #TODO FIX EXTENSION + elif obj.TLM_ObjectProperties.tlm_postpack_object: + print("Atlas object image (postpack)") + image_name = obj.TLM_ObjectProperties.tlm_postatlas_pointer + postfix + extension #TODO FIX EXTENSION + else: + print("Baked object image") + image_name = obj.name + postfix + extension #TODO FIX EXTENSION + + node.image.filepath_raw = os.path.join(dirpath, image_name) + +def apply_materials(load_atlas=0): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying materials") + if load_atlas: + print("- In load Atlas mode") + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + hidden = False + + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + if not hidden: + + uv_layers = obj.data.uv_layers + uv_layers.active_index = 0 + scene = bpy.context.scene + + decoding = False + + #Sort name + for slot in obj.material_slots: + mat = slot.material + if mat.name.endswith('_temp'): + old = slot.material + slot.material = bpy.data.materials[old.name.split('_' + obj.name)[0]] + + if(scene.TLM_SceneProperties.tlm_decoder_setup): + + tlm_rgbm = bpy.data.node_groups.get('RGBM Decode') + tlm_rgbd = bpy.data.node_groups.get('RGBD Decode') + tlm_logluv = bpy.data.node_groups.get('LogLuv Decode') + + if tlm_rgbm == None: + load_library('RGBM Decode') + + if tlm_rgbd == None: + load_library('RGBD Decode') + + if tlm_logluv == None: + load_library('LogLuv Decode') + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + tlm_exposure = bpy.data.node_groups.get("Exposure") + + if tlm_exposure == None: + load_library("Exposure") + + #Apply materials + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(obj.name) + for slot in obj.material_slots: + + mat = slot.material + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(slot.material) + + + if not mat.TLM_ignore: + + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + foundBakedNode = False + + #Find nodes + for node in nodes: + if node.name == "Baked Image": + lightmapNode = node + lightmapNode.location = -1200, 300 + lightmapNode.name = "TLM_Lightmap" + foundBakedNode = True + + # if load_atlas: + # print("Load Atlas for: " + obj.name) + # img_name = obj.TLM_ObjectProperties.tlm_atlas_pointer + '_baked' + # print("Src: " + img_name) + # else: + # img_name = obj.name + '_baked' + + img_name = obj.name + '_baked' + + if not foundBakedNode: + + if scene.TLM_EngineProperties.tlm_target == "vertex": + + lightmapNode = node_tree.nodes.new(type="ShaderNodeVertexColor") + lightmapNode.location = -1200, 300 + lightmapNode.name = "TLM_Lightmap" + + else: + + lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage") + lightmapNode.location = -1200, 300 + lightmapNode.name = "TLM_Lightmap" + lightmapNode.interpolation = bpy.context.scene.TLM_SceneProperties.tlm_texture_interpolation + lightmapNode.extension = bpy.context.scene.TLM_SceneProperties.tlm_texture_extrapolation + + if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""): + lightmapNode.image = bpy.data.images[obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"] + else: + lightmapNode.image = bpy.data.images[img_name] + + #Find output node + outputNode = nodes[0] + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in node_tree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + #Find mainnode + mainNode = outputNode.inputs[0].links[0].from_node + + if (mainNode.type == "MIX_SHADER"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Mix shader found") + + #TODO SHIFT BETWEEN from node input 1 or 2 based on which type + mainNode = outputNode.inputs[0].links[0].from_node.inputs[1].links[0].from_node + + if (mainNode.type == "ADD_SHADER"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Mix shader found") + + mainNode = outputNode.inputs[0].links[0].from_node.inputs[0].links[0].from_node + + if (mainNode.type == "ShaderNodeMixRGB"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Mix RGB shader found") + + mainNode = outputNode.inputs[0].links[0].from_node.inputs[0].links[0].from_node + + #Add all nodes first + #Add lightmap multipliction texture + mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB") + mixNode.name = "Lightmap_Multiplication" + mixNode.location = -800, 300 + if scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO": + mixNode.blend_type = 'MULTIPLY' + else: + mixNode.blend_type = 'MULTIPLY' + + if scene.TLM_EngineProperties.tlm_lighting_mode == "complete": + mixNode.inputs[0].default_value = 0.0 + else: + mixNode.inputs[0].default_value = 1.0 + + UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + UVLightmap.uv_map = uv_channel + + UVLightmap.name = "Lightmap_UV" + UVLightmap.location = -1500, 300 + + if(scene.TLM_SceneProperties.tlm_decoder_setup): + if scene.TLM_SceneProperties.tlm_encoding_device == "CPU": + if scene.TLM_SceneProperties.tlm_encoding_mode_a == 'RGBM': + DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup") + DecodeNode.node_tree = bpy.data.node_groups["RGBM Decode"] + DecodeNode.location = -400, 300 + DecodeNode.name = "Lightmap_RGBM_Decode" + decoding = True + if scene.TLM_SceneProperties.tlm_encoding_mode_b == "RGBD": + DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup") + DecodeNode.node_tree = bpy.data.node_groups["RGBD Decode"] + DecodeNode.location = -400, 300 + DecodeNode.name = "Lightmap_RGBD_Decode" + decoding = True + else: + if scene.TLM_SceneProperties.tlm_encoding_mode_b == 'RGBM': + DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup") + DecodeNode.node_tree = bpy.data.node_groups["RGBM Decode"] + DecodeNode.location = -400, 300 + DecodeNode.name = "Lightmap_RGBM_Decode" + decoding = True + if scene.TLM_SceneProperties.tlm_encoding_mode_b == "RGBD": + DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup") + DecodeNode.node_tree = bpy.data.node_groups["RGBD Decode"] + DecodeNode.location = -400, 300 + DecodeNode.name = "Lightmap_RGBD_Decode" + decoding = True + if scene.TLM_SceneProperties.tlm_encoding_mode_b == "LogLuv": + DecodeNode = node_tree.nodes.new(type="ShaderNodeGroup") + DecodeNode.node_tree = bpy.data.node_groups["LogLuv Decode"] + DecodeNode.location = -400, 300 + DecodeNode.name = "Lightmap_LogLuv_Decode" + decoding = True + + if scene.TLM_SceneProperties.tlm_split_premultiplied: + + lightmapNodeExtra = node_tree.nodes.new(type="ShaderNodeTexImage") + lightmapNodeExtra.location = -1200, 800 + lightmapNodeExtra.name = "TLM_Lightmap_Extra" + lightmapNodeExtra.interpolation = bpy.context.scene.TLM_SceneProperties.tlm_texture_interpolation + lightmapNodeExtra.extension = bpy.context.scene.TLM_SceneProperties.tlm_texture_extrapolation + lightmapNodeExtra.image = lightmapNode.image + + # #IF OBJ IS USING ATLAS? + # if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""): + # #lightmapNode.image = bpy.data.images[obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"] + # #print("OBS! OBJ IS USING ATLAS, RESULT WILL BE WRONG!") + # #bpy.app.driver_namespace["logman"].append("OBS! OBJ IS USING ATLAS, RESULT WILL BE WRONG!") + # pass + # if (obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_postatlas_pointer != ""): + # #print("OBS! OBJ IS USING ATLAS, RESULT WILL BE WRONG!") + # #bpy.app.driver_namespace["logman"].append("OBS! OBJ IS USING ATLAS, RESULT WILL BE WRONG!") + # print() + # lightmapNodeExtra.image = lightmapNode.image + + #lightmapPath = lightmapNode.image.filepath_raw + #print("PREM: " + lightmapPath) + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + ExposureNode = node_tree.nodes.new(type="ShaderNodeGroup") + ExposureNode.node_tree = bpy.data.node_groups["Exposure"] + ExposureNode.inputs[1].default_value = scene.TLM_EngineProperties.tlm_exposure_multiplier + ExposureNode.location = -500, 300 + ExposureNode.name = "Lightmap_Exposure" + + #Add Basecolor node + if len(mainNode.inputs[0].links) == 0: + baseColorValue = mainNode.inputs[0].default_value + baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB") + baseColorNode.outputs[0].default_value = baseColorValue + baseColorNode.location = ((mainNode.location[0] - 1100, mainNode.location[1] - 300)) + baseColorNode.name = "Lightmap_BasecolorNode_A" + else: + baseColorNode = mainNode.inputs[0].links[0].from_node + baseColorNode.name = "LM_P" + + #Linking + if decoding and scene.TLM_SceneProperties.tlm_encoding_use: + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + + mat.node_tree.links.new(lightmapNode.outputs[0], DecodeNode.inputs[0]) #Connect lightmap node to decodenode + + if scene.TLM_SceneProperties.tlm_split_premultiplied: + mat.node_tree.links.new(lightmapNodeExtra.outputs[0], DecodeNode.inputs[1]) #Connect lightmap node to decodenode + else: + mat.node_tree.links.new(lightmapNode.outputs[1], DecodeNode.inputs[1]) #Connect lightmap node to decodenode + + mat.node_tree.links.new(DecodeNode.outputs[0], mixNode.inputs[1]) #Connect decode node to mixnode + mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect exposure node to mixnode + + else: + + mat.node_tree.links.new(lightmapNode.outputs[0], DecodeNode.inputs[0]) #Connect lightmap node to decodenode + if scene.TLM_SceneProperties.tlm_split_premultiplied: + mat.node_tree.links.new(lightmapNodeExtra.outputs[0], DecodeNode.inputs[1]) #Connect lightmap node to decodenode + else: + mat.node_tree.links.new(lightmapNode.outputs[1], DecodeNode.inputs[1]) #Connect lightmap node to decodenode + + mat.node_tree.links.new(DecodeNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode + + mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node + mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node + + if not scene.TLM_EngineProperties.tlm_target == "vertex": + mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode + + if scene.TLM_SceneProperties.tlm_split_premultiplied: + mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNodeExtra.inputs[0]) #Connect uvnode to lightmapnode + + else: + + if(scene.TLM_EngineProperties.tlm_exposure_multiplier > 0): + mat.node_tree.links.new(lightmapNode.outputs[0], ExposureNode.inputs[0]) #Connect lightmap node to mixnode + mat.node_tree.links.new(ExposureNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode + else: + mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[1]) #Connect lightmap node to mixnode + mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[2]) #Connect basecolor to pbr node + mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node + if not scene.TLM_EngineProperties.tlm_target == "vertex": + mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode + + #If skip metallic + if scene.TLM_SceneProperties.tlm_metallic_clamp == "skip": + if mainNode.inputs[4].default_value > 0.1: #DELIMITER + moutput = mainNode.inputs[0].links[0].from_node + mat.node_tree.links.remove(moutput.outputs[0].links[0]) + +def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): + + if not bpy.context.scene.TLM_EngineProperties.tlm_target == "vertex": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(ext_postfix, new_postfix, formatHDR) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + #Here + #If the object is part of atlas + print("CHECKING FOR REPART") + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": #TODO, ALSO CONFIGURE FOR POSTATLAS + + if bpy.context.scene.TLM_AtlasList[obj.TLM_ObjectProperties.tlm_atlas_pointer].tlm_atlas_merge_samemat: + + #For each material we check if it ends with a number + for slot in obj.material_slots: + + part = slot.name.rpartition('.') + if part[2].isnumeric() and part[0] in bpy.data.materials: + + print("Material for obj: " + obj.name + " was numeric, and the material: " + part[0] + " was found.") + slot.material = bpy.data.materials.get(part[0]) + + + + # for slot in obj.material_slots: + # mat = slot.material + # node_tree = mat.node_tree + # nodes = mat.node_tree.nodes + + try: + + hidden = False + + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + if not hidden: + + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + for node in nodes: + if node.name == "Baked Image" or node.name == "TLM_Lightmap": + img_name = node.image.filepath_raw + cutLen = len(ext_postfix + formatHDR) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Len:" + str(len(ext_postfix + formatHDR)) + "|" + ext_postfix + ".." + formatHDR) + + #Simple way to sort out objects with multiple materials + if formatHDR == ".hdr" or formatHDR == ".exr": + if not node.image.filepath_raw.endswith(new_postfix + formatHDR): + print("Node1: " + node.image.filepath_raw + " => " + img_name[:-cutLen] + new_postfix + formatHDR) + node.image.filepath_raw = img_name[:-cutLen] + new_postfix + formatHDR + else: + cutLen = len(ext_postfix + ".hdr") + if not node.image.filepath_raw.endswith(new_postfix + formatHDR): + if not node.image.filepath_raw.endswith("_XYZ.png"): + print("Node2: " + node.image.filepath_raw + " => " + img_name[:-cutLen] + new_postfix + formatHDR) + node.image.filepath_raw = img_name[:-cutLen] + new_postfix + formatHDR + + for node in nodes: + if bpy.context.scene.TLM_SceneProperties.tlm_encoding_use and bpy.context.scene.TLM_SceneProperties.tlm_encoding_mode_b == "LogLuv": + if bpy.context.scene.TLM_SceneProperties.tlm_split_premultiplied: + if node.name == "TLM_Lightmap": + img_name = node.image.filepath_raw + print("PREM Main: " + img_name) + if node.image.filepath_raw.endswith("_encoded.png"): + print(node.image.filepath_raw + " => " + node.image.filepath_raw[:-4] + "_XYZ.png") + if not node.image.filepath_raw.endswith("_XYZ.png"): + node.image.filepath_raw = node.image.filepath_raw[:-4] + "_XYZ.png" + if node.name == "TLM_Lightmap_Extra": + img_path = node.image.filepath_raw[:-8] + "_W.png" + img = bpy.data.images.load(img_path) + node.image = img + bpy.data.images.load(img_path) + print("PREM Extra: " + img_path) + node.image.filepath_raw = img_path + node.image.colorspace_settings.name = "Linear" + + except: + + print("Error occured with postfix change for obj: " + obj.name) + + for image in bpy.data.images: + image.reload() + +def applyAOPass(): + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + hidden = False + + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + if not hidden: + + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + for node in nodes: + if node.name == "Baked Image" or node.name == "TLM_Lightmap": + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + LightmapPath = node.image.filepath_raw + + filebase = os.path.basename(LightmapPath) + filename = os.path.splitext(filebase)[0] + extension = os.path.splitext(filebase)[1] + AOImagefile = filename[:-4] + "_ao" + AOImagePath = os.path.join(dirpath, AOImagefile + extension) + + AOMap = nodes.new('ShaderNodeTexImage') + AOMap.name = "TLM_AOMap" + AOImage = bpy.data.images.load(AOImagePath) + AOMap.image = AOImage + AOMap.location = -800, 0 + + AOMult = nodes.new(type="ShaderNodeMixRGB") + AOMult.name = "TLM_AOMult" + AOMult.blend_type = 'MULTIPLY' + AOMult.inputs[0].default_value = 1.0 + AOMult.location = -300, 300 + + multyNode = nodes["Lightmap_Multiplication"] + mainNode = nodes["Principled BSDF"] + UVMapNode = nodes["Lightmap_UV"] + + node_tree.links.remove(multyNode.outputs[0].links[0]) + + node_tree.links.new(multyNode.outputs[0], AOMult.inputs[1]) + node_tree.links.new(AOMap.outputs[0], AOMult.inputs[2]) + node_tree.links.new(AOMult.outputs[0], mainNode.inputs[0]) + node_tree.links.new(UVMapNode.outputs[0], AOMap.inputs[0]) + +def load_library(asset_name): + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + + if bpy.data.filepath.endswith('tlm_data.blend'): # Prevent load in library itself + return + + data_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'assets/tlm_data.blend')) + data_names = [asset_name] + + # Import + data_refs = data_names.copy() + with bpy.data.libraries.load(data_path, link=False) as (data_from, data_to): + data_to.node_groups = data_refs + + for ref in data_refs: + ref.use_fake_user = True \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/prepare.py b/blender/arm/lightmapper/utility/cycles/prepare.py new file mode 100644 index 0000000000..03216ecdc0 --- /dev/null +++ b/blender/arm/lightmapper/utility/cycles/prepare.py @@ -0,0 +1,947 @@ +import bpy, math, time + +from . import cache +from .. utility import * + +def assemble(): + + configure_world() + + configure_lights() + + configure_meshes() + +def init(self, prev_container): + + store_existing(prev_container) + + set_settings() + + configure_world() + + configure_lights() + + configure_meshes(self) + + print("Config mesh catch omitted: REMEMBER TO SET IT BACK NAXELA") + # try: + # configure_meshes(self) + # except Exception as e: + + # print("An error occured during mesh configuration. See error below:") + + # print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}") + + # if not bpy.context.scene.TLM_SceneProperties.tlm_verbose: + # print("Turn on verbose mode to get more detail.") + +def configure_world(): + pass + +def configure_lights(): + pass + +def configure_meshes(self): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Configuring meshes: Start") + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Configuring meshes: Material restore") + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Configuring meshes: Material rename check") + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + #for image in bpy.data.images: + # if image.name.endswith("_baked"): + # bpy.data.images.remove(image, do_unlink=True) + + iterNum = 0 + currentIterNum = 0 + + scene = bpy.context.scene + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Object: Setting UV, converting modifiers and prepare channels") + + #OBJECT: Set UV, CONVERT AND PREPARE + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + + hidden = False + + #We check if the object is hidden + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + #We check if the object's collection is hidden + collections = obj.users_collection + + for collection in collections: + + if collection.hide_viewport: + hidden = True + if collection.hide_render: + hidden = True + + try: + if collection.name in bpy.context.scene.view_layers[0].layer_collection.children: + if bpy.context.scene.view_layers[0].layer_collection.children[collection.name].hide_viewport: + hidden = True + except: + print("Error: Could not find collection: " + collection.name) + + + #Additional check for zero poly meshes + mesh = obj.data + if (len(mesh.polygons)) < 1: + print("Found an object with zero polygons. Skipping object: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use and not hidden: + + print("Preparing: UV initiation for object: " + obj.name) + + if len(obj.data.vertex_colors) < 1: + obj.data.vertex_colors.new(name="TLM") + + if scene.TLM_SceneProperties.tlm_reset_uv: + + uv_layers = obj.data.uv_layers + uv_channel = "UVMap_Lightmap" + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying transform to: " + obj.name) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + if scene.TLM_SceneProperties.tlm_apply_modifiers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying modifiers to: " + obj.name) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.convert(target='MESH') + + for slot in obj.material_slots: + material = slot.material + skipIncompatibleMaterials(material) + + obj.hide_select = False #Remember to toggle this back + for slot in obj.material_slots: + if "." + slot.name + '_Original' in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original') + slot.material = bpy.data.materials["." + slot.name + '_Original'] + + #ATLAS UV PROJECTING + print("PREPARE: ATLAS") + for atlasgroup in scene.TLM_AtlasList: + + print("Adding UV Projection for Atlas group: " + atlasgroup.name) + + atlas = atlasgroup.name + atlas_items = [] + + bpy.ops.object.select_all(action='DESELECT') + + #Atlas: Set UV, CONVERT AND PREPARE + for obj in bpy.context.scene.objects: + + if obj.TLM_ObjectProperties.tlm_atlas_pointer == atlasgroup.name: + + hidden = False + + #We check if the object is hidden + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + #We check if the object's collection is hidden + collections = obj.users_collection + + for collection in collections: + + if collection.hide_viewport: + hidden = True + if collection.hide_render: + hidden = True + + try: + if collection.name in bpy.context.scene.view_layers[0].layer_collection.children: + if bpy.context.scene.view_layers[0].layer_collection.children[collection.name].hide_viewport: + hidden = True + except: + print("Error: Could not find collection: " + collection.name) + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and not hidden: + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("UV map created for object: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + else: + print("Existing UV map found for object: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_Lightmap': + uv_layers.active_index = i + break + + atlas_items.append(obj) + obj.select_set(True) + + if atlasgroup.tlm_atlas_lightmap_unwrap_mode == "SmartProject": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Atlasgroup Smart Project for: " + str(atlas_items)) + for obj in atlas_items: + print("Applying Smart Project to: ") + print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name) + + + if len(atlas_items) > 0: + bpy.context.view_layer.objects.active = atlas_items[0] + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + print("Smart project done.") + elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Lightmap": + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=atlasgroup.tlm_atlas_unwrap_margin) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Xatlas": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Using Xatlas on Atlas Group: " + atlas) + + for obj in atlas_items: + obj.select_set(True) + if len(atlas_items) > 0: + bpy.context.view_layer.objects.active = atlas_items[0] + + bpy.ops.object.mode_set(mode='EDIT') + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + bpy.ops.object.mode_set(mode='OBJECT') + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for Atlas Group: " + atlas) + + if atlasgroup.tlm_use_uv_packer: + bpy.ops.object.select_all(action='DESELECT') + for obj in atlas_items: + obj.select_set(True) + if len(atlas_items) > 0: + bpy.context.view_layer.objects.active = atlas_items[0] + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + bpy.context.scene.UVPackerProps.uvp_padding = atlasgroup.tlm_uv_packer_padding + bpy.context.scene.UVPackerProps.uvp_engine = atlasgroup.tlm_uv_packer_packing_engine + + #print(x) + + print("!!!!!!!!!!!!!!!!!!!!! Using UV Packer on: " + obj.name) + + if uv_layers.active == "UVMap_Lightmap": + print("YES") + else: + print("NO") + uv_layers.active_index = len(uv_layers) - 1 + + if uv_layers.active == "UVMap_Lightmap": + print("YES") + else: + print("NO") + uv_layers.active_index = len(uv_layers) - 1 + + bpy.ops.uvpackeroperator.packbtn() + + # if bpy.context.scene.UVPackerProps.uvp_engine == "OP0": + # time.sleep(1) + # else: + # time.sleep(2) + time.sleep(2) + + #FIX THIS! MAKE A SEPARATE CALL. THIS IS A THREADED ASYNC + + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + #print(x) + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + #OBJECT UV PROJECTING + print("PREPARE: OBJECTS") + for obj in bpy.context.scene.objects: + if obj.name in bpy.context.view_layer.objects: #Possible fix for view layer error + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + + hidden = False + + #We check if the object is hidden + if obj.hide_get(): + hidden = True + if obj.hide_viewport: + hidden = True + if obj.hide_render: + hidden = True + + #We check if the object's collection is hidden + collections = obj.users_collection + + for collection in collections: + + if collection.hide_viewport: + hidden = True + if collection.hide_render: + hidden = True + + try: + if collection.name in bpy.context.scene.view_layers[0].layer_collection.children: + if bpy.context.scene.view_layers[0].layer_collection.children[collection.name].hide_viewport: + hidden = True + except: + print("Error: Could not find collection: " + collection.name) + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use and not hidden: + + objWasHidden = False + + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) + + currentIterNum = currentIterNum + 1 + + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + obs = bpy.context.view_layer.objects + active = obs.active + + #Provide material if none exists + print("Preprocessing material for: " + obj.name) + preprocess_material(obj, scene) + + #UV Layer management here + if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + print("Managing layer for Obj: " + obj.name) + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) + + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) + + if obj.TLM_ObjectProperties.tlm_use_uv_packer: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + bpy.context.scene.UVPackerProps.uvp_padding = obj.TLM_ObjectProperties.tlm_uv_packer_padding + bpy.context.scene.UVPackerProps.uvp_engine = obj.TLM_ObjectProperties.tlm_uv_packer_packing_engine + + #print(x) + + print("!!!!!!!!!!!!!!!!!!!!! Using UV Packer on: " + obj.name) + + if uv_layers.active == "UVMap_Lightmap": + print("YES") + else: + print("NO") + uv_layers.active_index = len(uv_layers) - 1 + + if uv_layers.active == "UVMap_Lightmap": + print("YES") + else: + print("NO") + uv_layers.active_index = len(uv_layers) - 1 + + bpy.ops.uvpackeroperator.packbtn() + + if bpy.context.scene.UVPackerProps.uvp_engine == "OP0": + time.sleep(1) + else: + time.sleep(2) + + #FIX THIS! MAKE A SEPARATE CALL. THIS IS A THREADED ASYNC + + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + #print(x) + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + #print(x) + + #Sort out nodes + for slot in obj.material_slots: + + nodetree = slot.material.node_tree + + outputNode = nodetree.nodes[0] #Presumed to be material output node + + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + mainNode = outputNode.inputs[0].links[0].from_node + + if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: + + #TODO! FIND THE PRINCIPLED PBR + self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + + if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] + else: + self.report({'INFO'}, "No principled found. Seeking diffuse") + if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] + else: + self.report({'INFO'}, "No supported nodes. Continuing anyway.") + + if mainNode.type == 'GROUP': + if mainNode.node_tree != "Armory PBR": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material group is not supported!") + + if (mainNode.type == "ShaderNodeMixRGB"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Mix shader found") + + #Skip for now + slot.material.TLM_ignore = True + + if (mainNode.type == "BSDF_PRINCIPLED"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Principled") + if scene.TLM_EngineProperties.tlm_directional_mode == "None": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Directional mode") + if not len(mainNode.inputs[22].links) == 0: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("NOT LEN 0") + ninput = mainNode.inputs[22].links[0] + noutput = mainNode.inputs[22].links[0].from_node + nodetree.links.remove(noutput.outputs[0].links[0]) + + #Clamp metallic + if bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "limit": + + MainMetNodeSocket = mainNode.inputs.get("Metallic") + if not len(MainMetNodeSocket.links) == 0: + + print("Creating new clamp node") + + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.9 + minput = mainNode.inputs.get("Metallic").links[0] #Metal input socket + moutput = mainNode.inputs.get("Metallic").links[0].from_socket #Output socket + + nodetree.links.remove(minput) + + nodetree.links.new(moutput, MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0], MainMetNodeSocket) #clamp node to metinput + + elif mainNode.type == "PRINCIPLED_BSDF" and MainMetNodeSocket.links[0].from_node.type == "CLAMP": + + pass + + else: + + print("New clamp node NOT made") + + if mainNode.inputs[4].default_value > 0.9: + mainNode.inputs[4].default_value = 0.9 + + elif bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "zero": + + MainMetNodeSocket = mainNode.inputs[4] + if not len(MainMetNodeSocket.links) == 0: + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.0 + minput = mainNode.inputs[4].links[0] #Metal input socket + moutput = mainNode.inputs[4].links[0].from_socket #Output socket + + nodetree.links.remove(minput) + + nodetree.links.new(moutput, MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0], MainMetNodeSocket) #clamp node to metinput + else: + mainNode.inputs[4].default_value = 0.0 + + else: #Skip + pass + + if (mainNode.type == "BSDF_DIFFUSE"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Diffuse") + + # if (mainNode.type == "BSDF_DIFFUSE"): + # if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + # print("BSDF_Diffuse") + + #TODO FIX THIS PART! + #THIS IS USED IN CASES WHERE FOR SOME REASON THE USER FORGETS TO CONNECT SOMETHING INTO THE OUTPUT MATERIAL + for slot in obj.material_slots: + + nodetree = bpy.data.materials[slot.name].node_tree + nodes = nodetree.nodes + + #First search to get the first output material type + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + mainNode = node + break + + #Fallback to get search + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes.get("Material Output") + + #Last resort to first node in list + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes[0].inputs[0].links[0].from_node + + # for node in nodes: + # if "LM" in node.name: + # nodetree.links.new(node.outputs[0], mainNode.inputs[0]) + + # for node in nodes: + # if "Lightmap" in node.name: + # nodes.remove(node) + +def preprocess_material(obj, scene): + if len(obj.material_slots) == 0: + single = False + number = 0 + while single == False: + matname = obj.name + ".00" + str(number) + if matname in bpy.data.materials: + single = False + number = number + 1 + else: + mat = bpy.data.materials.new(name=matname) + mat.use_nodes = True + obj.data.materials.append(mat) + single = True + + #We copy the existing material slots to an ordered array, which corresponds to the slot index + matArray = [] + for slot in obj.material_slots: + matArray.append(slot.name) + + obj["TLM_PrevMatArray"] = matArray + + #We check and safeguard against NoneType + for slot in obj.material_slots: + if slot.material is None: + matName = obj.name + ".00" + str(0) + bpy.data.materials.new(name=matName) + slot.material = bpy.data.materials[matName] + slot.material.use_nodes = True + + for slot in obj.material_slots: + + cache.backup_material_copy(slot) + + mat = slot.material + if mat.users > 1: + copymat = mat.copy() + slot.material = copymat + + #SOME ATLAS EXCLUSION HERE? + ob = obj + for slot in ob.material_slots: + #If temporary material already exists + if slot.material.name.endswith('_temp'): + continue + n = slot.material.name + '_' + ob.name + '_temp' + if not n in bpy.data.materials: + slot.material = slot.material.copy() + slot.material.name = n + + #Add images for baking + img_name = obj.name + '_baked' + #Resolution is object lightmap resolution divided by global scaler + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + + if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""): + + atlas_image_name = obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked" + + res = int(scene.TLM_AtlasList[obj.TLM_ObjectProperties.tlm_atlas_pointer].tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale) + + #If image not in bpy.data.images or if size changed, make a new image + if atlas_image_name not in bpy.data.images or bpy.data.images[atlas_image_name].size[0] != res or bpy.data.images[atlas_image_name].size[1] != res: + img = bpy.data.images.new(img_name, int(res), int(res), alpha=True, float_buffer=True) + + num_pixels = len(img.pixels) + result_pixel = list(img.pixels) + + for i in range(0,num_pixels,4): + + if scene.TLM_SceneProperties.tlm_override_bg_color: + result_pixel[i+0] = scene.TLM_SceneProperties.tlm_override_color[0] + result_pixel[i+1] = scene.TLM_SceneProperties.tlm_override_color[1] + result_pixel[i+2] = scene.TLM_SceneProperties.tlm_override_color[2] + else: + result_pixel[i+0] = 0.0 + result_pixel[i+1] = 0.0 + result_pixel[i+2] = 0.0 + result_pixel[i+3] = 1.0 + + img.pixels = result_pixel + + img.name = atlas_image_name + else: + img = bpy.data.images[atlas_image_name] + + for slot in obj.material_slots: + mat = slot.material + mat.use_nodes = True + nodes = mat.node_tree.nodes + + if "Baked Image" in nodes: + img_node = nodes["Baked Image"] + else: + img_node = nodes.new('ShaderNodeTexImage') + img_node.name = 'Baked Image' + img_node.location = (100, 100) + img_node.image = img + img_node.select = True + nodes.active = img_node + + #We need to save this file first in Blender 3.3 due to new filmic option? + image = img + saveDir = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + bakemap_path = os.path.join(saveDir, image.name) + filepath_ext = ".hdr" + image.filepath_raw = bakemap_path + filepath_ext + image.file_format = "HDR" + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Saving to: " + image.filepath_raw) + image.save() + + else: + + res = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale) + + #If image not in bpy.data.images or if size changed, make a new image + if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != res or bpy.data.images[img_name].size[1] != res: + img = bpy.data.images.new(img_name, int(res), int(res), alpha=True, float_buffer=True) + + num_pixels = len(img.pixels) + result_pixel = list(img.pixels) + + for i in range(0,num_pixels,4): + if scene.TLM_SceneProperties.tlm_override_bg_color: + result_pixel[i+0] = scene.TLM_SceneProperties.tlm_override_color[0] + result_pixel[i+1] = scene.TLM_SceneProperties.tlm_override_color[1] + result_pixel[i+2] = scene.TLM_SceneProperties.tlm_override_color[2] + else: + result_pixel[i+0] = 0.0 + result_pixel[i+1] = 0.0 + result_pixel[i+2] = 0.0 + result_pixel[i+3] = 1.0 + + img.pixels = result_pixel + + img.name = img_name + else: + img = bpy.data.images[img_name] + + for slot in obj.material_slots: + mat = slot.material + mat.use_nodes = True + nodes = mat.node_tree.nodes + + if "Baked Image" in nodes: + img_node = nodes["Baked Image"] + else: + img_node = nodes.new('ShaderNodeTexImage') + img_node.name = 'Baked Image' + img_node.location = (100, 100) + img_node.image = img + img_node.select = True + nodes.active = img_node + + #We need to save this file first in Blender 3.3 due to new filmic option? + image = img + saveDir = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + bakemap_path = os.path.join(saveDir, image.name) + filepath_ext = ".hdr" + image.filepath_raw = bakemap_path + filepath_ext + image.file_format = "HDR" + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Saving to: " + image.filepath_raw) + image.save() + +def set_settings(): + + scene = bpy.context.scene + cycles = scene.cycles + scene.render.engine = "CYCLES" + sceneProperties = scene.TLM_SceneProperties + engineProperties = scene.TLM_EngineProperties + cycles.device = scene.TLM_EngineProperties.tlm_mode + + print(bpy.app.version) + + if bpy.app.version[0] == 3: + if cycles.device == "GPU": + scene.cycles.tile_size = 256 + else: + scene.cycles.tile_size = 32 + else: + if cycles.device == "GPU": + scene.render.tile_x = 256 + scene.render.tile_y = 256 + else: + scene.render.tile_x = 32 + scene.render.tile_y = 32 + + if engineProperties.tlm_quality == "0": + cycles.samples = 32 + cycles.max_bounces = 1 + cycles.diffuse_bounces = 1 + cycles.glossy_bounces = 1 + cycles.transparent_max_bounces = 1 + cycles.transmission_bounces = 1 + cycles.volume_bounces = 1 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "1": + cycles.samples = 64 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "2": + cycles.samples = 512 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "3": + cycles.samples = 1024 + cycles.max_bounces = 256 + cycles.diffuse_bounces = 256 + cycles.glossy_bounces = 256 + cycles.transparent_max_bounces = 256 + cycles.transmission_bounces = 256 + cycles.volume_bounces = 256 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "4": + cycles.samples = 2048 + cycles.max_bounces = 512 + cycles.diffuse_bounces = 512 + cycles.glossy_bounces = 512 + cycles.transparent_max_bounces = 512 + cycles.transmission_bounces = 512 + cycles.volume_bounces = 512 + cycles.caustics_reflective = True + cycles.caustics_refractive = True + else: #Custom + pass + +def store_existing(prev_container): + + scene = bpy.context.scene + cycles = scene.cycles + + selected = [] + + for obj in bpy.context.scene.objects: + if obj.select_get(): + selected.append(obj.name) + + prev_container["settings"] = [ + cycles.samples, + cycles.max_bounces, + cycles.diffuse_bounces, + cycles.glossy_bounces, + cycles.transparent_max_bounces, + cycles.transmission_bounces, + cycles.volume_bounces, + cycles.caustics_reflective, + cycles.caustics_refractive, + cycles.device, + scene.render.engine, + bpy.context.view_layer.objects.active, + selected, + [scene.render.resolution_x, scene.render.resolution_y] + ] + +def skipIncompatibleMaterials(material): + node_tree = material.node_tree + nodes = material.node_tree.nodes + + #ADD OR MIX SHADER? CUSTOM/GROUP? + #IF Principled has emissive or transparency? + + SkipMatList = ["EMISSION", + "BSDF_TRANSPARENT", + "BACKGROUND", + "BSDF_HAIR", + "BSDF_HAIR_PRINCIPLED", + "HOLDOUT", + "PRINCIPLED_VOLUME", + "BSDF_REFRACTION", + "EEVEE_SPECULAR", + "BSDF_TRANSLUCENT", + "VOLUME_ABSORPTION", + "VOLUME_SCATTER"] + + #Find output node + outputNode = nodes[0] + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in node_tree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + #Find mainnode + mainNode = outputNode.inputs[0].links[0].from_node + + if mainNode.type in SkipMatList: + material.TLM_ignore = True + print("Ignored material: " + material.name) + +def packUVPack(): + + + + pass diff --git a/blender/arm/lightmapper/utility/denoiser/integrated.py b/blender/arm/lightmapper/utility/denoiser/integrated.py new file mode 100644 index 0000000000..9a27d13d82 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/integrated.py @@ -0,0 +1,80 @@ +import bpy, os + +class TLM_Integrated_Denoise: + + image_array = [] + image_output_destination = "" + + def load(self, images): + self.image_array = images + + self.cull_undefined() + + def setOutputDir(self, dir): + self.image_output_destination = dir + + def cull_undefined(self): + + #Do a validation check before denoising + + cam = bpy.context.scene.camera + if not cam: + bpy.ops.object.camera_add() + + #Just select the first camera we find, needed for the compositor + for obj in bpy.context.scene.objects: + if obj.type == "CAMERA": + bpy.context.scene.camera = obj + return + + def denoise(self): + + if not bpy.context.scene.use_nodes: + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + for image in self.image_array: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Image...: " + image) + + img = bpy.data.images.load(self.image_output_destination + "/" + image) + + image_node = tree.nodes.new(type='CompositorNodeImage') + image_node.image = img + image_node.location = 0, 0 + + denoise_node = tree.nodes.new(type='CompositorNodeDenoise') + denoise_node.location = 300, 0 + + comp_node = tree.nodes.new('CompositorNodeComposite') + comp_node.location = 600, 0 + + links = tree.links + links.new(image_node.outputs[0], denoise_node.inputs[0]) + links.new(denoise_node.outputs[0], comp_node.inputs[0]) + + # set output resolution to image res + bpy.context.scene.render.resolution_x = img.size[0] + bpy.context.scene.render.resolution_y = img.size[1] + bpy.context.scene.render.resolution_percentage = 100 + + filePath = bpy.data.filepath + path = os.path.dirname(filePath) + + base = os.path.basename(image) + filename, file_extension = os.path.splitext(image) + filename = filename[:-6] + + bpy.context.scene.render.filepath = self.image_output_destination + "/" + filename + "_denoised" + file_extension + + denoised_image_path = self.image_output_destination + bpy.context.scene.render.image_settings.file_format = "HDR" + + bpy.ops.render.render(write_still=True) + + #Cleanup + comp_nodes = [image_node, denoise_node, comp_node] + for node in comp_nodes: + tree.nodes.remove(node) diff --git a/blender/arm/lightmapper/utility/denoiser/oidn.py b/blender/arm/lightmapper/utility/denoiser/oidn.py new file mode 100644 index 0000000000..76154de2d9 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/oidn.py @@ -0,0 +1,207 @@ +import bpy, os, sys, re, platform, subprocess +import numpy as np + +class TLM_OIDN_Denoise: + + image_array = [] + + image_output_destination = "" + + denoised_array = [] + + def __init__(self, oidnProperties, img_array, dirpath): + + self.oidnProperties = oidnProperties + + self.image_array = img_array + + self.image_output_destination = dirpath + + self.check_binary() + + def check_binary(self): + + oidnPath = self.oidnProperties.tlm_oidn_path + + if oidnPath != "": + + file = oidnPath + filename, file_extension = os.path.splitext(file) + + + if platform.system() == 'Windows': + + if(file_extension == ".exe"): + + pass + + else: + + self.oidnProperties.tlm_oidn_path = os.path.join(self.oidnProperties.tlm_oidn_path,"oidnDenoise.exe") + + else: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Please provide OIDN path") + + def denoise(self): + + for image in self.image_array: + + if image not in self.denoised_array: + + image_path = os.path.join(self.image_output_destination, image) + + #Save to pfm + loaded_image = bpy.data.images.load(image_path, check_existing=False) + + width = loaded_image.size[0] + height = loaded_image.size[1] + + image_output_array = np.zeros([width, height, 3], dtype="float32") + image_output_array = np.array(loaded_image.pixels) + image_output_array = image_output_array.reshape(height, width, 4) + image_output_array = np.float32(image_output_array[:,:,:3]) + + image_output_denoise_destination = image_path[:-4] + ".pfm" + + image_output_denoise_result_destination = image_path[:-4] + "_denoised.pfm" + + with open(image_output_denoise_destination, "wb") as fileWritePFM: + self.save_pfm(fileWritePFM, image_output_array) + + #Denoise + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Loaded image: " + str(loaded_image)) + + verbose = self.oidnProperties.tlm_oidn_verbose + affinity = self.oidnProperties.tlm_oidn_affinity + + if verbose: + print("Denoiser search: " + bpy.path.abspath(self.oidnProperties.tlm_oidn_path)) + v = "3" + else: + v = "0" + + if affinity: + a = "1" + else: + a = "0" + + threads = str(self.oidnProperties.tlm_oidn_threads) + maxmem = str(self.oidnProperties.tlm_oidn_maxmem) + + if platform.system() == 'Windows': + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + pipePath = [oidnPath, '-f', 'RTLightmap', '-hdr', image_output_denoise_destination, '-o', image_output_denoise_result_destination, '-verbose', v, '-threads', threads, '-affinity', a, '-maxmem', maxmem] + elif platform.system() == 'Darwin': + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v] + else: + oidnPath = bpy.path.abspath(self.oidnProperties.tlm_oidn_path) + oidnPath = oidnPath.replace(' ', '\\ ') + image_output_denoise_destination = image_output_denoise_destination.replace(' ', '\\ ') + image_output_denoise_result_destination = image_output_denoise_result_destination.replace(' ', '\\ ') + pipePath = [oidnPath + ' -f ' + ' RTLightmap ' + ' -hdr ' + image_output_denoise_destination + ' -o ' + image_output_denoise_result_destination + ' -verbose ' + v] + + if not verbose: + denoisePipe = subprocess.Popen(pipePath, stdout=subprocess.PIPE, stderr=None, shell=True) + else: + denoisePipe = subprocess.Popen(pipePath, shell=True) + + denoisePipe.communicate()[0] + + if platform.system() != 'Windows': + image_output_denoise_result_destination = image_output_denoise_result_destination.replace('\\', '') + + with open(image_output_denoise_result_destination, "rb") as f: + denoise_data, scale = self.load_pfm(f) + + ndata = np.array(denoise_data) + ndata2 = np.dstack((ndata, np.ones((width,height)))) + img_array = ndata2.ravel() + + loaded_image.pixels = img_array + loaded_image.filepath_raw = image_output_denoise_result_destination = image_path[:-10] + "_denoised.hdr" + loaded_image.file_format = "HDR" + loaded_image.save() + + self.denoised_array.append(image) + + print(image_path) + + def clean(self): + + self.denoised_array.clear() + self.image_array.clear() + + for file in self.image_output_destination: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + #self.image_output_destination + + #Clean temporary files here.. + #...pfm + #...denoised.hdr + + + def load_pfm(self, file, as_flat_list=False): + #start = time() + + header = file.readline().decode("utf-8").rstrip() + if header == "PF": + color = True + elif header == "Pf": + color = False + else: + raise Exception("Not a PFM file.") + + dim_match = re.match(r"^(\d+)\s(\d+)\s$", file.readline().decode("utf-8")) + if dim_match: + width, height = map(int, dim_match.groups()) + else: + raise Exception("Malformed PFM header.") + + scale = float(file.readline().decode("utf-8").rstrip()) + if scale < 0: # little-endian + endian = "<" + scale = -scale + else: + endian = ">" # big-endian + + data = np.fromfile(file, endian + "f") + shape = (height, width, 3) if color else (height, width) + if as_flat_list: + result = data + else: + result = np.reshape(data, shape) + #print("PFM import took %.3f s" % (time() - start)) + return result, scale + + def save_pfm(self, file, image, scale=1): + #start = time() + + if image.dtype.name != "float32": + raise Exception("Image dtype must be float32 (got %s)" % image.dtype.name) + + if len(image.shape) == 3 and image.shape[2] == 3: # color image + color = True + elif len(image.shape) == 2 or len(image.shape) == 3 and image.shape[2] == 1: # greyscale + color = False + else: + raise Exception("Image must have H x W x 3, H x W x 1 or H x W dimensions.") + + file.write(b"PF\n" if color else b"Pf\n") + file.write(b"%d %d\n" % (image.shape[1], image.shape[0])) + + endian = image.dtype.byteorder + + if endian == "<" or endian == "=" and sys.byteorder == "little": + scale = -scale + + file.write(b"%f\n" % scale) + + image.tofile(file) + + #print("PFM export took %.3f s" % (time() - start)) diff --git a/blender/arm/lightmapper/utility/denoiser/optix.py b/blender/arm/lightmapper/utility/denoiser/optix.py new file mode 100644 index 0000000000..dab900ff38 --- /dev/null +++ b/blender/arm/lightmapper/utility/denoiser/optix.py @@ -0,0 +1,92 @@ +import bpy, os, platform, subprocess + +class TLM_Optix_Denoise: + + image_array = [] + + image_output_destination = "" + + denoised_array = [] + + def __init__(self, optixProperties, img_array, dirpath): + + self.optixProperties = optixProperties + + self.image_array = img_array + + self.image_output_destination = dirpath + + self.check_binary() + + def check_binary(self): + + optixPath = self.optixProperties.tlm_optix_path + + if optixPath != "": + + file = optixPath + filename, file_extension = os.path.splitext(file) + + if(file_extension == ".exe"): + + #if file exists optixDenoise or denoise + + pass + + else: + + #if file exists optixDenoise or denoise + + self.optixProperties.tlm_optix_path = os.path.join(self.optixProperties.tlm_optix_path,"Denoiser.exe") + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Please provide Optix path") + + def denoise(self): + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Optix: Denoising") + for image in self.image_array: + + if image not in self.denoised_array: + + image_path = os.path.join(self.image_output_destination, image) + + denoise_output_destination = image_path[:-10] + "_denoised.hdr" + + if platform.system() == 'Windows': + optixPath = bpy.path.abspath(self.optixProperties.tlm_optix_path) + pipePath = [optixPath, '-i', image_path, '-o', denoise_output_destination] + elif platform.system() == 'Darwin': + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Mac for Optix is still unsupported") + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Linux for Optix is still unsupported") + + if self.optixProperties.tlm_optix_verbose: + denoisePipe = subprocess.Popen(pipePath, shell=True) + else: + denoisePipe = subprocess.Popen(pipePath, stdout=subprocess.PIPE, stderr=None, shell=True) + + denoisePipe.communicate()[0] + + image = bpy.data.images.load(image_path, check_existing=False) + bpy.data.images[image.name].filepath_raw = bpy.data.images[image.name].filepath_raw[:-4] + "_denoised.hdr" + bpy.data.images[image.name].reload() + + def clean(self): + + self.denoised_array.clear() + self.image_array.clear() + + for file in self.image_output_destination: + if file.endswith("_baked.hdr"): + baked_image_array.append(file) + + #self.image_output_destination + + #Clean temporary files here.. + #...pfm + #...denoised.hdr \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/encoding.py b/blender/arm/lightmapper/utility/encoding.py new file mode 100644 index 0000000000..96e442bb09 --- /dev/null +++ b/blender/arm/lightmapper/utility/encoding.py @@ -0,0 +1,674 @@ +import bpy, math, os, gpu, bgl, importlib +import numpy as np +from . import utility +from fractions import Fraction +from gpu_extras.batch import batch_for_shader + +def splitLogLuvAlphaAtlas(imageIn, outDir, quality): + pass + +def splitLogLuvAlpha(imageIn, outDir, quality): + + bpy.app.driver_namespace["logman"].append("Starting LogLuv split for: " + str(imageIn)) + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + print(imageIn) + image = cv2.imread(imageIn, cv2.IMREAD_UNCHANGED) + #cv2.imshow('image', image) + split = cv2.split(image) + merged = cv2.merge([split[0], split[1], split[2]]) + alpha = split[3] + #b,g,r = cv2.split(image) + #merged = cv2.merge([b, g, r]) + #alpha = cv2.merge([a,a,a]) + image_name = os.path.basename(imageIn)[:-4] + #os.path.join(outDir, image_name+"_XYZ.png") + + cv2.imwrite(os.path.join(outDir, image_name+"_XYZ.png"), merged) + cv2.imwrite(os.path.join(outDir, image_name+"_W.png"), alpha) + +def encodeLogLuvGPU(image, outDir, quality): + + bpy.app.driver_namespace["logman"].append("Starting LogLuv encode for: " + str(image.name)) + + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + const mat3 cLogLuvM = mat3( 0.2209, 0.3390, 0.4184, 0.1138, 0.6780, 0.7319, 0.0102, 0.1130, 0.2969 ); + vec4 LinearToLogLuv( in vec4 value ) { + vec3 Xp_Y_XYZp = cLogLuvM * value.rgb; + Xp_Y_XYZp = max( Xp_Y_XYZp, vec3( 1e-6, 1e-6, 1e-6 ) ); + vec4 vResult; + vResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z; + float Le = 2.0 * log2(Xp_Y_XYZp.y) + 127.0; + vResult.w = fract( Le ); + vResult.z = ( Le - ( floor( vResult.w * 255.0 ) ) / 255.0 ) / 255.0; + return vResult; + //return vec4(Xp_Y_XYZp,1); + } + + const mat3 cLogLuvInverseM = mat3( 6.0014, -2.7008, -1.7996, -1.3320, 3.1029, -5.7721, 0.3008, -1.0882, 5.6268 ); + vec4 LogLuvToLinear( in vec4 value ) { + float Le = value.z * 255.0 + value.w; + vec3 Xp_Y_XYZp; + Xp_Y_XYZp.y = exp2( ( Le - 127.0 ) / 2.0 ); + Xp_Y_XYZp.z = Xp_Y_XYZp.y / value.y; + Xp_Y_XYZp.x = value.x * Xp_Y_XYZp.z; + vec3 vRGB = cLogLuvInverseM * Xp_Y_XYZp.rgb; + //return vec4( max( vRGB, 0.0 ), 1.0 ); + return vec4( max( Xp_Y_XYZp, 0.0 ), 1.0 ); + } + + void main() + { + //fragColor = LinearToLogLuv(pow(texture(image, texCoord_interp), vec4(0.454))); + fragColor = LinearToLogLuv(texture(image, texCoord_interp)); + //fragColor = LogLuvToLinear(LinearToLogLuv(texture(image, texCoord_interp))); + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + +def encodeImageRGBDGPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + //Code from here: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/helperFunctions.fx + + const float PI = 3.1415926535897932384626433832795; + const float HALF_MIN = 5.96046448e-08; // Smallest positive half. + + const float LinearEncodePowerApprox = 2.2; + const float GammaEncodePowerApprox = 1.0 / LinearEncodePowerApprox; + const vec3 LuminanceEncodeApprox = vec3(0.2126, 0.7152, 0.0722); + + const float Epsilon = 0.0000001; + #define saturate(x) clamp(x, 0.0, 1.0) + + float maxEps(float x) { + return max(x, Epsilon); + } + + float toLinearSpace(float color) + { + return pow(color, LinearEncodePowerApprox); + } + + vec3 toLinearSpace(vec3 color) + { + return pow(color, vec3(LinearEncodePowerApprox)); + } + + vec4 toLinearSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(LinearEncodePowerApprox)), color.a); + } + + vec3 toGammaSpace(vec3 color) + { + return pow(color, vec3(GammaEncodePowerApprox)); + } + + vec4 toGammaSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(GammaEncodePowerApprox)), color.a); + } + + float toGammaSpace(float color) + { + return pow(color, GammaEncodePowerApprox); + } + + float square(float value) + { + return value * value; + } + + // Check if configurable value is needed. + const float rgbdMaxRange = 255.0; + + vec4 toRGBD(vec3 color) { + float maxRGB = maxEps(max(color.r, max(color.g, color.b))); + float D = max(rgbdMaxRange / maxRGB, 1.); + D = clamp(floor(D) / 255.0, 0., 1.); + vec3 rgb = color.rgb * D; + + // Helps with png quantization. + rgb = toGammaSpace(rgb); + + return vec4(rgb, D); + } + + vec3 fromRGBD(vec4 rgbd) { + // Helps with png quantization. + rgbd.rgb = toLinearSpace(rgbd.rgb); + + // return rgbd.rgb * ((rgbdMaxRange / 255.0) / rgbd.a); + + return rgbd.rgb / rgbd.a; + } + + void main() + { + + fragColor = toRGBD(texture(image, texCoord_interp).rgb); + + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + + #Todo - Find a way to save + #bpy.ops.image.save_all_modified() + +#TODO - FINISH THIS +def encodeImageRGBMGPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + //Code from here: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/helperFunctions.fx + + const float PI = 3.1415926535897932384626433832795; + const float HALF_MIN = 5.96046448e-08; // Smallest positive half. + + const float LinearEncodePowerApprox = 2.2; + const float GammaEncodePowerApprox = 1.0 / LinearEncodePowerApprox; + const vec3 LuminanceEncodeApprox = vec3(0.2126, 0.7152, 0.0722); + + const float Epsilon = 0.0000001; + #define saturate(x) clamp(x, 0.0, 1.0) + + float maxEps(float x) { + return max(x, Epsilon); + } + + float toLinearSpace(float color) + { + return pow(color, LinearEncodePowerApprox); + } + + vec3 toLinearSpace(vec3 color) + { + return pow(color, vec3(LinearEncodePowerApprox)); + } + + vec4 toLinearSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(LinearEncodePowerApprox)), color.a); + } + + vec3 toGammaSpace(vec3 color) + { + return pow(color, vec3(GammaEncodePowerApprox)); + } + + vec4 toGammaSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(GammaEncodePowerApprox)), color.a); + } + + float toGammaSpace(float color) + { + return pow(color, GammaEncodePowerApprox); + } + + float square(float value) + { + return value * value; + } + + // Check if configurable value is needed. + const float rgbdMaxRange = 255.0; + + vec4 toRGBM(vec3 color) { + + vec4 rgbm; + color *= 1.0/6.0; + rgbm.a = saturate( max( max( color.r, color.g ), max( color.b, 1e-6 ) ) ); + rgbm.a = clamp(floor(D) / 255.0, 0., 1.); + rgbm.rgb = color / rgbm.a; + + return + + float maxRGB = maxEps(max(color.r, max(color.g, color.b))); + float D = max(rgbdMaxRange / maxRGB, 1.); + D = clamp(floor(D) / 255.0, 0., 1.); + vec3 rgb = color.rgb * D; + + // Helps with png quantization. + rgb = toGammaSpace(rgb); + + return vec4(rgb, D); + } + + vec3 fromRGBD(vec4 rgbd) { + // Helps with png quantization. + rgbd.rgb = toLinearSpace(rgbd.rgb); + + // return rgbd.rgb * ((rgbdMaxRange / 255.0) / rgbd.a); + + return rgbd.rgb / rgbd.a; + } + + void main() + { + + fragColor = toRGBM(texture(image, texCoord_interp).rgb); + + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + + #Todo - Find a way to save + #bpy.ops.image.save_all_modified() + +def encodeImageRGBMCPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + num_pixels = len(input_image.pixels) + result_pixel = list(input_image.pixels) + + for i in range(0,num_pixels,4): + for j in range(3): + result_pixel[i+j] *= 1.0 / maxRange; + result_pixel[i+3] = saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6)) + result_pixel[i+3] = math.ceil(result_pixel[i+3] * 255.0) / 255.0 + for j in range(3): + result_pixel[i+j] /= result_pixel[i+3] + + target_image.pixels = result_pixel + input_image = target_image + + #Save RGBM + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + input_image.save() + + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + # input_image.filepath_raw = outDir + "_encoded.png" + # input_image.file_format = "PNG" + # bpy.context.scene.render.image_settings.quality = quality + # input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + #input_image. + #input_image.save() + +def saturate(num, floats=True): + if num <= 0: + num = 0 + elif num > (1 if floats else 255): + num = (1 if floats else 255) + return num + +def maxEps(x): + return max(x, 1e-6) + +def encodeImageRGBDCPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + num_pixels = len(input_image.pixels) + result_pixel = list(input_image.pixels) + + rgbdMaxRange = 255.0 + + for i in range(0,num_pixels,4): + + maxRGB = maxEps(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2])) + D = max(rgbdMaxRange/maxRGB, 1.0) + D = np.clip((math.floor(D) / 255.0), 0.0, 1.0) + + result_pixel[i] = math.pow(result_pixel[i] * D, 1/2.2) + result_pixel[i+1] = math.pow(result_pixel[i+1] * D, 1/2.2) + result_pixel[i+2] = math.pow(result_pixel[i+2] * D, 1/2.2) + result_pixel[i+3] = D + + target_image.pixels = result_pixel + + input_image = target_image + + #Save RGBD + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + input_image.save() \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/numpy.py b/blender/arm/lightmapper/utility/filtering/numpy.py new file mode 100644 index 0000000000..7922dbd9f2 --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/numpy.py @@ -0,0 +1,49 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_NP_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + #opencv_process_image = cv2.imread(file_input, -1) + + print("Filtering: " + file_input) + + print(os.path.join(lightmap_dir, file)) + + if scene.TLM_SceneProperties.tlm_numpy_filtering_mode == "3x3 blur": + pass + + #filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + #cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/opencv.py b/blender/arm/lightmapper/utility/filtering/opencv.py new file mode 100644 index 0000000000..c6b1b5578e --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/opencv.py @@ -0,0 +1,178 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_CV_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + opencv_process_image = cv2.imread(file_input, -1) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Filtering: " + os.path.basename(file_input)) + + obj_name = os.path.basename(file_input).split("_")[0] + + #SEAM TESTING# ##################### + + # obj = bpy.data.objects[obj_name] + + # bpy.context.view_layer.objects.active = obj + # bpy.ops.object.mode_set(mode='EDIT') + # bpy.ops.uv.export_layout(filepath=os.path.join(lightmap_dir,obj_name), export_all=True, mode='PNG', opacity=0.0) + # bpy.ops.object.mode_set(mode='OBJECT') + # print("Exported") + + #SEAM TESTING# ##################### + + if obj_name in bpy.context.scene.objects: + override = bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override + elif obj_name in scene.TLM_AtlasList: + override = False + else: + override = False + + if override: + + print(os.path.join(lightmap_dir, file)) + + objectProperties = bpy.data.objects[obj_name].TLM_ObjectProperties + + #TODO OVERRIDE FILTERING OPTION! REWRITE + if objectProperties.tlm_mesh_filtering_mode == "Box": + if objectProperties.tlm_mesh_filtering_box_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength + 1, objectProperties.tlm_mesh_filtering_box_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength, objectProperties.tlm_mesh_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Gaussian": + if objectProperties.tlm_mesh_filtering_gaussian_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength + 1, objectProperties.tlm_mesh_filtering_gaussian_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength, objectProperties.tlm_mesh_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Bilateral": + diameter_size = objectProperties.tlm_mesh_filtering_bilateral_diameter + sigma_color = objectProperties.tlm_mesh_filtering_bilateral_color_deviation + sigma_space = objectProperties.tlm_mesh_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if objectProperties.tlm_mesh_filtering_median_kernel % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel + 1, objectProperties.tlm_mesh_filtering_median_kernel + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel, objectProperties.tlm_mesh_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Written to: " + filter_file_output) + + else: + + print(os.path.join(lightmap_dir, file)) + + #TODO OVERRIDE FILTERING OPTION! + if scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + if scene.TLM_SceneProperties.tlm_filtering_box_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength + 1,scene.TLM_SceneProperties.tlm_filtering_box_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength,scene.TLM_SceneProperties.tlm_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + if scene.TLM_SceneProperties.tlm_filtering_gaussian_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + diameter_size = scene.TLM_SceneProperties.tlm_filtering_bilateral_diameter + sigma_color = scene.TLM_SceneProperties.tlm_filtering_bilateral_color_deviation + sigma_space = scene.TLM_SceneProperties.tlm_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if scene.TLM_SceneProperties.tlm_filtering_median_kernel % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1 , scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel, scene.TLM_SceneProperties.tlm_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Written to: " + filter_file_output) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/shader.py b/blender/arm/lightmapper/utility/filtering/shader.py new file mode 100644 index 0000000000..55028702c1 --- /dev/null +++ b/blender/arm/lightmapper/utility/filtering/shader.py @@ -0,0 +1,160 @@ +import bpy, os, importlib +from os import listdir +from os.path import isfile, join + +class TLM_Shader_Filtering: + + image_output_destination = "" + + def init(lightmap_dir, denoise): + + scene = bpy.context.scene + + print("Beginning filtering for files: ") + + if denoise: + file_ending = "_denoised.hdr" + else: + file_ending = "_baked.hdr" + + dirfiles = [f for f in listdir(lightmap_dir) if isfile(join(lightmap_dir, f))] + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring filtering") + return 0 + else: + cv2 = importlib.__import__("cv2") + + for file in dirfiles: + + if denoise: + file_ending = "_denoised.hdr" + file_split = 13 + else: + file_ending = "_baked.hdr" + file_split = 10 + + if file.endswith(file_ending): + + file_input = os.path.join(lightmap_dir, file) + os.chdir(lightmap_dir) + + opencv_process_image = cv2.imread(file_input, -1) + + print("Filtering: " + os.path.basename(file_input)) + + obj_name = os.path.basename(file_input).split("_")[0] + + if bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override: + + print("OVERRIDE!") + + print(os.path.join(lightmap_dir, file)) + + objectProperties = bpy.data.objects[obj_name].TLM_ObjectProperties + + #TODO OVERRIDE FILTERING OPTION! REWRITE + if objectProperties.tlm_mesh_filtering_mode == "Box": + if objectProperties.tlm_mesh_filtering_box_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength + 1, objectProperties.tlm_mesh_filtering_box_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_box_strength, objectProperties.tlm_mesh_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Gaussian": + if objectProperties.tlm_mesh_filtering_gaussian_strength % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength + 1, objectProperties.tlm_mesh_filtering_gaussian_strength + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_gaussian_strength, objectProperties.tlm_mesh_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif objectProperties.tlm_mesh_filtering_mode == "Bilateral": + diameter_size = objectProperties.tlm_mesh_filtering_bilateral_diameter + sigma_color = objectProperties.tlm_mesh_filtering_bilateral_color_deviation + sigma_space = objectProperties.tlm_mesh_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if objectProperties.tlm_mesh_filtering_median_kernel % 2 == 0: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel + 1, objectProperties.tlm_mesh_filtering_median_kernel + 1) + else: + kernel_size = (objectProperties.tlm_mesh_filtering_median_kernel, objectProperties.tlm_mesh_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if objectProperties.tlm_mesh_filtering_iterations > 1: + for x in range(objectProperties.tlm_mesh_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + else: + + print(os.path.join(lightmap_dir, file)) + + #TODO OVERRIDE FILTERING OPTION! + if scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + if scene.TLM_SceneProperties.tlm_filtering_box_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength + 1,scene.TLM_SceneProperties.tlm_filtering_box_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_box_strength,scene.TLM_SceneProperties.tlm_filtering_box_strength) + opencv_bl_result = cv2.blur(opencv_process_image, kernel_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.blur(opencv_bl_result, kernel_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + if scene.TLM_SceneProperties.tlm_filtering_gaussian_strength % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_gaussian_strength,scene.TLM_SceneProperties.tlm_filtering_gaussian_strength) + sigma_size = 0 + opencv_bl_result = cv2.GaussianBlur(opencv_process_image, kernel_size, sigma_size) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.GaussianBlur(opencv_bl_result, kernel_size, sigma_size) + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + diameter_size = scene.TLM_SceneProperties.tlm_filtering_bilateral_diameter + sigma_color = scene.TLM_SceneProperties.tlm_filtering_bilateral_color_deviation + sigma_space = scene.TLM_SceneProperties.tlm_filtering_bilateral_coordinate_deviation + opencv_bl_result = cv2.bilateralFilter(opencv_process_image, diameter_size, sigma_color, sigma_space) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.bilateralFilter(opencv_bl_result, diameter_size, sigma_color, sigma_space) + else: + + if scene.TLM_SceneProperties.tlm_filtering_median_kernel % 2 == 0: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1 , scene.TLM_SceneProperties.tlm_filtering_median_kernel + 1) + else: + kernel_size = (scene.TLM_SceneProperties.tlm_filtering_median_kernel, scene.TLM_SceneProperties.tlm_filtering_median_kernel) + + opencv_bl_result = cv2.medianBlur(opencv_process_image, kernel_size[0]) + if scene.TLM_SceneProperties.tlm_filtering_iterations > 1: + for x in range(scene.TLM_SceneProperties.tlm_filtering_iterations): + opencv_bl_result = cv2.medianBlur(opencv_bl_result, kernel_size[0]) + + filter_file_output = os.path.join(lightmap_dir, file[:-file_split] + "_filtered.hdr") + + cv2.imwrite(filter_file_output, opencv_bl_result) + + print("Written to: " + filter_file_output) + + # if file.endswith(file_ending): + # print() + # baked_image_array.append(file) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/gui/Viewport.py b/blender/arm/lightmapper/utility/gui/Viewport.py new file mode 100644 index 0000000000..01ef600ddc --- /dev/null +++ b/blender/arm/lightmapper/utility/gui/Viewport.py @@ -0,0 +1,77 @@ +import bpy, blf, bgl, os, gpu +from gpu_extras.batch import batch_for_shader + +class ViewportDraw: + + def __init__(self, context, text): + + bakefile = "TLM_Overlay.png" + scriptDir = os.path.dirname(os.path.realpath(__file__)) + bakefile_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'assets', bakefile)) + + image_name = "TLM_Overlay.png" + + bpy.ops.image.open(filepath=bakefile_path) + + print("Self path: " + bakefile_path) + + for img in bpy.data.images: + if img.filepath.endswith(image_name): + image = img + break + + if not image: + image = bpy.data.images[image_name] + + x = 15 + y = 15 + w = 400 + h = 200 + + self.shader = gpu.shader.from_builtin('2D_IMAGE') + self.batch = batch_for_shader( + self.shader, 'TRI_FAN', + { + "pos": ((x, y), (x+w, y), (x+w, y+h), (x, y+h)), + "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + self.text = text + self.image = image + #self.handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_text_callback, (context,), 'WINDOW', 'POST_PIXEL') + self.handle2 = bpy.types.SpaceView3D.draw_handler_add(self.draw_image_callback, (context,), 'WINDOW', 'POST_PIXEL') + + def draw_text_callback(self, context): + + font_id = 0 + blf.position(font_id, 15, 15, 0) + blf.size(font_id, 20, 72) + blf.draw(font_id, "%s" % (self.text)) + + def draw_image_callback(self, context): + + if self.image: + bgl.glEnable(bgl.GL_BLEND) + bgl.glActiveTexture(bgl.GL_TEXTURE0) + + try: + bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode) + except: + bpy.types.SpaceView3D.draw_handler_remove(self.handle2, 'WINDOW') + + self.shader.bind() + self.shader.uniform_int("image", 0) + self.batch.draw(self.shader) + bgl.glDisable(bgl.GL_BLEND) + + def update_text(self, text): + + self.text = text + + def remove_handle(self): + #bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW') + bpy.types.SpaceView3D.draw_handler_remove(self.handle2, 'WINDOW') diff --git a/blender/arm/lightmapper/utility/icon.py b/blender/arm/lightmapper/utility/icon.py new file mode 100644 index 0000000000..54f8acd8c5 --- /dev/null +++ b/blender/arm/lightmapper/utility/icon.py @@ -0,0 +1,31 @@ +import os +import bpy + +from bpy.utils import previews + +icons = None +directory = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'icons')) + +def id(identifier): + return image(identifier).icon_id + +def image(identifier): + def icon(identifier): + if identifier in icons: + return icons[identifier] + return icons.load(identifier, os.path.join(directory, identifier + '.png'), 'IMAGE') + + if icons: + return icon(identifier) + else: + create() + return icon(identifier) + + +def create(): + global icons + icons = previews.new() + + +def remove(): + previews.remove(icons) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/log.py b/blender/arm/lightmapper/utility/log.py new file mode 100644 index 0000000000..456ebcdaf0 --- /dev/null +++ b/blender/arm/lightmapper/utility/log.py @@ -0,0 +1,21 @@ +import bpy +import datetime + +class TLM_Logman: + + _log = [] + + def __init__(self): + print("Logger started Init") + self.append("Logger started.") + + def append(self, appended): + self._log.append(str(datetime.datetime.now()) + ": " + str(appended)) + + #TODO! + def stats(): + pass + + def dumpLog(self): + for line in self._log: + print(line) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/luxcore/setup.py b/blender/arm/lightmapper/utility/luxcore/setup.py new file mode 100644 index 0000000000..13bfc57bf4 --- /dev/null +++ b/blender/arm/lightmapper/utility/luxcore/setup.py @@ -0,0 +1,259 @@ +import bpy + +from .. utility import * + +def init(self, prev_container): + + #TODO - JSON classes + export.scene = """scene.camera.cliphither = 0.1 +scene.camera.clipyon = 100 +scene.camera.shutteropen = 0 +scene.camera.shutterclose = 1 +scene.camera.autovolume.enable = 1 +scene.camera.lookat.orig = 7.358891 -6.925791 4.958309 +scene.camera.lookat.target = 6.707333 -6.31162 4.513038 +scene.camera.up = -0.3240135 0.3054208 0.8953956 +scene.camera.screenwindow = -1 1 -0.5625 0.5625 +scene.camera.lensradius = 0 +scene.camera.focaldistance = 10 +scene.camera.autofocus.enable = 0 +scene.camera.type = "perspective" +scene.camera.oculusrift.barrelpostpro.enable = 0 +scene.camera.fieldofview = 39.59776 +scene.camera.bokeh.blades = 0 +scene.camera.bokeh.power = 3 +scene.camera.bokeh.distribution.type = "NONE" +scene.camera.bokeh.scale.x = 0.7071068 +scene.camera.bokeh.scale.y = 0.7071068 +scene.lights.__WORLD_BACKGROUND_LIGHT__.gain = 2e-05 2e-05 2e-05 +scene.lights.__WORLD_BACKGROUND_LIGHT__.transformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.id = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature = -1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature.normalize = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.diffuse.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.glossy.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.specular.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.type = "sky2" +scene.lights.__WORLD_BACKGROUND_LIGHT__.dir = 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.turbidity = 2.2 +scene.lights.__WORLD_BACKGROUND_LIGHT__.groundalbedo = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.enable = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.color = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.autoscale = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.width = 512 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.height = 256 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibilitymapcache.enable = 0 +scene.lights.2382361116072.gain = 1 1 1 +scene.lights.2382361116072.transformation = -0.2908646 0.9551712 -0.05518906 0 -0.7711008 -0.1998834 0.6045247 0 0.5663932 0.2183912 0.7946723 0 4.076245 1.005454 5.903862 1 +scene.lights.2382361116072.id = 0 +scene.lights.2382361116072.temperature = -1 +scene.lights.2382361116072.temperature.normalize = 0 +scene.lights.2382361116072.type = "sphere" +scene.lights.2382361116072.color = 1 1 1 +scene.lights.2382361116072.power = 0 +scene.lights.2382361116072.normalizebycolor = 0 +scene.lights.2382361116072.efficency = 0 +scene.lights.2382361116072.position = 0 0 0 +scene.lights.2382361116072.radius = 0.1 +scene.materials.Material2382357175256.type = "disney" +scene.materials.Material2382357175256.basecolor = "0.7 0.7 0.7" +scene.materials.Material2382357175256.subsurface = "0" +scene.materials.Material2382357175256.roughness = "0.2" +scene.materials.Material2382357175256.metallic = "0" +scene.materials.Material2382357175256.specular = "0.5" +scene.materials.Material2382357175256.speculartint = "0" +scene.materials.Material2382357175256.clearcoat = "0" +scene.materials.Material2382357175256.clearcoatgloss = "1" +scene.materials.Material2382357175256.anisotropic = "0" +scene.materials.Material2382357175256.sheen = "0" +scene.materials.Material2382357175256.sheentint = "0" +scene.materials.Material2382357175256.transparency.shadow = 0 0 0 +scene.materials.Material2382357175256.id = 3364224 +scene.materials.Material2382357175256.emission.gain = 1 1 1 +scene.materials.Material2382357175256.emission.power = 0 +scene.materials.Material2382357175256.emission.normalizebycolor = 1 +scene.materials.Material2382357175256.emission.efficency = 0 +scene.materials.Material2382357175256.emission.theta = 90 +scene.materials.Material2382357175256.emission.id = 0 +scene.materials.Material2382357175256.emission.importance = 1 +scene.materials.Material2382357175256.emission.temperature = -1 +scene.materials.Material2382357175256.emission.temperature.normalize = 0 +scene.materials.Material2382357175256.emission.directlightsampling.type = "AUTO" +scene.materials.Material2382357175256.visibility.indirect.diffuse.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.glossy.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.specular.enable = 1 +scene.materials.Material2382357175256.shadowcatcher.enable = 0 +scene.materials.Material2382357175256.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material2382357175256.photongi.enable = 1 +scene.materials.Material2382357175256.holdout.enable = 0 +scene.materials.Material__0012382357172440.type = "disney" +scene.materials.Material__0012382357172440.basecolor = "0.7 0.7 0.7" +scene.materials.Material__0012382357172440.subsurface = "0" +scene.materials.Material__0012382357172440.roughness = "0.2" +scene.materials.Material__0012382357172440.metallic = "0" +scene.materials.Material__0012382357172440.specular = "0.5" +scene.materials.Material__0012382357172440.speculartint = "0" +scene.materials.Material__0012382357172440.clearcoat = "0" +scene.materials.Material__0012382357172440.clearcoatgloss = "1" +scene.materials.Material__0012382357172440.anisotropic = "0" +scene.materials.Material__0012382357172440.sheen = "0" +scene.materials.Material__0012382357172440.sheentint = "0" +scene.materials.Material__0012382357172440.transparency.shadow = 0 0 0 +scene.materials.Material__0012382357172440.id = 6728256 +scene.materials.Material__0012382357172440.emission.gain = 1 1 1 +scene.materials.Material__0012382357172440.emission.power = 0 +scene.materials.Material__0012382357172440.emission.normalizebycolor = 1 +scene.materials.Material__0012382357172440.emission.efficency = 0 +scene.materials.Material__0012382357172440.emission.theta = 90 +scene.materials.Material__0012382357172440.emission.id = 0 +scene.materials.Material__0012382357172440.emission.importance = 1 +scene.materials.Material__0012382357172440.emission.temperature = -1 +scene.materials.Material__0012382357172440.emission.temperature.normalize = 0 +scene.materials.Material__0012382357172440.emission.directlightsampling.type = "AUTO" +scene.materials.Material__0012382357172440.visibility.indirect.diffuse.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.glossy.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.specular.enable = 1 +scene.materials.Material__0012382357172440.shadowcatcher.enable = 0 +scene.materials.Material__0012382357172440.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material__0012382357172440.photongi.enable = 1 +scene.materials.Material__0012382357172440.holdout.enable = 0 +scene.objects.23823611086320.material = "Material2382357175256" +scene.objects.23823611086320.ply = "mesh-00000.ply" +scene.objects.23823611086320.camerainvisible = 0 +scene.objects.23823611086320.id = 1326487202 +scene.objects.23823611086320.appliedtransformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 +scene.objects.23823611279760.material = "Material__0012382357172440" +scene.objects.23823611279760.ply = "mesh-00001.ply" +scene.objects.23823611279760.camerainvisible = 0 +scene.objects.23823611279760.id = 3772660237 +scene.objects.23823611279760.appliedtransformation = 5 0 0 0 0 5 0 0 0 0 5 0 0 0 0 1 +""" + + export.config = """context.verbose = 1 +accelerator.type = "AUTO" +accelerator.instances.enable = 1 +accelerator.motionblur.enable = 1 +accelerator.bvh.builder.type = "EMBREE_BINNED_SAH" +accelerator.bvh.treetype = 4 +accelerator.bvh.costsamples = 0 +accelerator.bvh.isectcost = 80 +accelerator.bvh.travcost = 10 +accelerator.bvh.emptybonus = 0.5 +scene.epsilon.min = "1e-05" +scene.epsilon.max = "0.1" +scene.file = "scene.scn" +images.scale = 1 +lightstrategy.type = "LOG_POWER" +native.threads.count = 8 +renderengine.type = "BAKECPU" +path.pathdepth.total = "7" +path.pathdepth.diffuse = "5" +path.pathdepth.glossy = "5" +path.pathdepth.specular = "6" +path.hybridbackforward.enable = "0" +path.hybridbackforward.partition = "0.8" +path.hybridbackforward.glossinessthreshold = "0.049" +path.russianroulette.depth = 3 +path.russianroulette.cap = 0.5 +path.clamping.variance.maxvalue = 0 +path.forceblackbackground.enable = "0" +sampler.type = "SOBOL" +sampler.imagesamples.enable = 1 +sampler.sobol.adaptive.strength = "0.9" +sampler.sobol.adaptive.userimportanceweight = 0.75 +sampler.sobol.bucketsize = "16" +sampler.sobol.tilesize = "16" +sampler.sobol.supersampling = "1" +sampler.sobol.overlapping = "1" +path.photongi.sampler.type = "METROPOLIS" +path.photongi.photon.maxcount = 100000000 +path.photongi.photon.maxdepth = 4 +path.photongi.photon.time.start = 0 +path.photongi.photon.time.end = -1 +path.photongi.visibility.lookup.radius = 0 +path.photongi.visibility.lookup.normalangle = 10 +path.photongi.visibility.targethitrate = 0.99 +path.photongi.visibility.maxsamplecount = 1048576 +path.photongi.glossinessusagethreshold = 0.05 +path.photongi.indirect.enabled = 0 +path.photongi.indirect.maxsize = 0 +path.photongi.indirect.haltthreshold = 0.05 +path.photongi.indirect.lookup.radius = 0 +path.photongi.indirect.lookup.normalangle = 10 +path.photongi.indirect.usagethresholdscale = 8 +path.photongi.indirect.filter.radiusscale = 3 +path.photongi.caustic.enabled = 0 +path.photongi.caustic.maxsize = 100000 +path.photongi.caustic.updatespp = 8 +path.photongi.caustic.updatespp.radiusreduction = 0.96 +path.photongi.caustic.updatespp.minradius = 0.003 +path.photongi.caustic.lookup.radius = 0.15 +path.photongi.caustic.lookup.normalangle = 10 +path.photongi.debug.type = "none" +path.photongi.persistent.file = "" +path.photongi.persistent.safesave = 1 +film.filter.type = "BLACKMANHARRIS" +film.filter.width = 2 +opencl.platform.index = -1 +film.width = 960 +film.height = 600 +film.safesave = 1 +film.noiseestimation.step = "32" +film.noiseestimation.warmup = "8" +film.noiseestimation.filter.scale = 4 +batch.haltnoisethreshold = 0.01 +batch.haltnoisethreshold.step = 64 +batch.haltnoisethreshold.warmup = 64 +batch.haltnoisethreshold.filter.enable = 1 +batch.haltnoisethreshold.stoprendering.enable = 1 +batch.halttime = "0" +batch.haltspp = 32 +film.outputs.safesave = 1 +film.outputs.0.type = "RGB_IMAGEPIPELINE" +film.outputs.0.filename = "RGB_IMAGEPIPELINE_0.png" +film.outputs.0.index = "0" +film.imagepipelines.000.0.type = "NOP" +film.imagepipelines.000.1.type = "TONEMAP_LINEAR" +film.imagepipelines.000.1.scale = "1" +film.imagepipelines.000.2.type = "GAMMA_CORRECTION" +film.imagepipelines.000.2.value = "2.2" +film.imagepipelines.000.radiancescales.0.enabled = "1" +film.imagepipelines.000.radiancescales.0.globalscale = "1" +film.imagepipelines.000.radiancescales.0.rgbscale = "1" "1" "1" +periodicsave.film.outputs.period = 0 +periodicsave.film.period = 0 +periodicsave.film.filename = "film.flm" +periodicsave.resumerendering.period = 0 +periodicsave.resumerendering.filename = "rendering.rsm" +resumerendering.filesafe = 1 +debug.renderconfig.parse.print = 0 +debug.scene.parse.print = 0 +screen.refresh.interval = 100 +screen.tool.type = "CAMERA_EDIT" +screen.tiles.pending.show = 1 +screen.tiles.converged.show = 0 +screen.tiles.notconverged.show = 0 +screen.tiles.passcount.show = 0 +screen.tiles.error.show = 0 +bake.minmapautosize = 64 +bake.maxmapautosize = 1024 +bake.powerof2autosize.enable = 1 +bake.skipexistingmapfiles = 1 +film.imagepipelines.1.0.type = "NOP" +bake.maps.0.type = "COMBINED" +bake.maps.0.filename = "23823611086320.exr" +bake.maps.0.imagepipelineindex = 1 +bake.maps.0.width = 512 +bake.maps.0.height = 512 +bake.maps.0.autosize.enabled = 1 +bake.maps.0.uvindex = 0 +bake.maps.0.objectnames = "23823611086320" +bake.maps.1.type = "COMBINED" +bake.maps.1.filename = "23823611279760.exr" +bake.maps.1.imagepipelineindex = 1 +bake.maps.1.width = 512 +bake.maps.1.height = 512 +bake.maps.1.autosize.enabled = 1 +bake.maps.1.uvindex = 0 +bake.maps.1.objectnames = "23823611279760" +""" \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/configure.py b/blender/arm/lightmapper/utility/octane/configure.py new file mode 100644 index 0000000000..ba6641ab78 --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/configure.py @@ -0,0 +1,243 @@ +import bpy, math + +#from . import cache +from .. utility import * + +def init(self, prev_container): + + #store_existing(prev_container) + + #set_settings() + + configure_world() + + configure_lights() + + configure_meshes(self) + +def configure_world(): + pass + +def configure_lights(): + pass + +def configure_meshes(self): + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + iterNum = 1 + currentIterNum = 0 + + scene = bpy.context.scene + + for obj in scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + obj.hide_select = False #Remember to toggle this back + + currentIterNum = currentIterNum + 1 + + obj.octane.baking_group_id = 1 + currentIterNum #0 doesn't exist, 1 is neutral and 2 is first baked object + + print("Obj: " + obj.name + " set to baking group: " + str(obj.octane.baking_group_id)) + + for slot in obj.material_slots: + if "." + slot.name + '_Original' in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original') + slot.material = bpy.data.materials["." + slot.name + '_Original'] + + + objWasHidden = False + + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) + + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + + uv_layers = obj.data.uv_layers + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + print("Setting active UV to: " + uv_layers.active_index) + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) + + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + set_camera() + +def set_camera(): + + cam_name = "TLM-BakeCam" + + if not cam_name in bpy.context.scene: + camera = bpy.data.cameras.new(cam_name) + camobj_name = "TLM-BakeCam-obj" + cam_obj = bpy.data.objects.new(camobj_name, camera) + bpy.context.collection.objects.link(cam_obj) + cam_obj.location = ((0,0,0)) + + bpy.context.scene.camera = cam_obj + +def set_settings(): + + scene = bpy.context.scene + cycles = scene.cycles + scene.render.engine = "CYCLES" + sceneProperties = scene.TLM_SceneProperties + engineProperties = scene.TLM_EngineProperties + cycles.device = scene.TLM_EngineProperties.tlm_mode + + if cycles.device == "GPU": + scene.render.tile_x = 256 + scene.render.tile_y = 256 + else: + scene.render.tile_x = 32 + scene.render.tile_y = 32 + + if engineProperties.tlm_quality == "0": + cycles.samples = 32 + cycles.max_bounces = 1 + cycles.diffuse_bounces = 1 + cycles.glossy_bounces = 1 + cycles.transparent_max_bounces = 1 + cycles.transmission_bounces = 1 + cycles.volume_bounces = 1 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "1": + cycles.samples = 64 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "2": + cycles.samples = 512 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "3": + cycles.samples = 1024 + cycles.max_bounces = 256 + cycles.diffuse_bounces = 256 + cycles.glossy_bounces = 256 + cycles.transparent_max_bounces = 256 + cycles.transmission_bounces = 256 + cycles.volume_bounces = 256 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "4": + cycles.samples = 2048 + cycles.max_bounces = 512 + cycles.diffuse_bounces = 512 + cycles.glossy_bounces = 512 + cycles.transparent_max_bounces = 512 + cycles.transmission_bounces = 512 + cycles.volume_bounces = 512 + cycles.caustics_reflective = True + cycles.caustics_refractive = True + else: #Custom + pass + +def store_existing(prev_container): + + scene = bpy.context.scene + cycles = scene.cycles + + selected = [] + + for obj in bpy.context.scene.objects: + if obj.select_get(): + selected.append(obj.name) + + prev_container["settings"] = [ + cycles.samples, + cycles.max_bounces, + cycles.diffuse_bounces, + cycles.glossy_bounces, + cycles.transparent_max_bounces, + cycles.transmission_bounces, + cycles.volume_bounces, + cycles.caustics_reflective, + cycles.caustics_refractive, + cycles.device, + scene.render.engine, + bpy.context.view_layer.objects.active, + selected, + [scene.render.resolution_x, scene.render.resolution_y] + ] \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/lightmap2.py b/blender/arm/lightmapper/utility/octane/lightmap2.py new file mode 100644 index 0000000000..ad84327632 --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/lightmap2.py @@ -0,0 +1,71 @@ +import bpy, os + +def bake(): + + cam_name = "TLM-BakeCam-obj" + + if cam_name in bpy.context.scene.objects: + + print("Camera found...") + + camera = bpy.context.scene.objects[cam_name] + + camera.data.octane.baking_camera = True + + for obj in bpy.context.scene.objects: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(False) + + iterNum = 2 + currentIterNum = 1 + + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + if iterNum > 1: + iterNum = iterNum - 1 + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + currentIterNum = currentIterNum + 1 + + scene = bpy.context.scene + + print("Baking obj: " + obj.name) + + print("Baking ID: " + str(currentIterNum) + " out of " + str(iterNum)) + + bpy.ops.object.select_all(action='DESELECT') + + camera.data.octane.baking_group_id = currentIterNum + + savedir = os.path.dirname(bpy.data.filepath) + user_dir = scene.TLM_Engine3Properties.tlm_lightmap_savedir + directory = os.path.join(savedir, user_dir) + + image_settings = bpy.context.scene.render.image_settings + image_settings.file_format = "HDR" + image_settings.color_depth = '32' + + filename = os.path.join(directory, "LM") + "_" + obj.name + ".hdr" + bpy.context.scene.render.filepath = filename + + resolution = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) + + bpy.context.scene.render.resolution_x = resolution + bpy.context.scene.render.resolution_y = resolution + + bpy.ops.render.render(write_still=True) + + else: + + print("No baking camera found") + + + + + print("Baking in Octane!") \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/pack.py b/blender/arm/lightmapper/utility/pack.py new file mode 100644 index 0000000000..5e6683354d --- /dev/null +++ b/blender/arm/lightmapper/utility/pack.py @@ -0,0 +1,354 @@ +import bpy, os, sys, math, mathutils, importlib +import numpy as np +from . rectpack import newPacker, PackingMode, PackingBin + +def postpack(): + + cv_installed = False + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + print("CV2 not found - Ignoring postpacking") + return 0 + else: + cv2 = importlib.__import__("cv2") + cv_installed = True + + if cv_installed: + + lightmap_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + packedAtlas = {} + + #TODO - TEST WITH ONLY 1 ATLAS AT FIRST (1 Atlas for each, but only 1 bin (no overflow)) + #PackedAtlas = Packer + #Each atlas has bins + #Each bins has rects + #Each rect corresponds to a pack_object + + scene = bpy.context.scene + + sceneProperties = scene.TLM_SceneProperties + + end = "_baked" + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + formatEnc = ".hdr" + + image_channel_depth = cv2.IMREAD_ANYDEPTH + linear_straight = False + + if sceneProperties.tlm_encoding_use and scene.TLM_EngineProperties.tlm_bake_mode != "Background": + + if sceneProperties.tlm_encoding_device == "CPU": + + if sceneProperties.tlm_encoding_mode_a == "HDR": + + if sceneProperties.tlm_format == "EXR": + + formatEnc = ".exr" + + if sceneProperties.tlm_encoding_mode_a == "RGBM": + + formatEnc = "_encoded.png" + image_channel_depth = cv2.IMREAD_UNCHANGED + + else: + + if sceneProperties.tlm_encoding_mode_b == "HDR": + + if sceneProperties.tlm_format == "EXR": + + formatEnc = ".exr" + + if sceneProperties.tlm_encoding_mode_b == "LogLuv": + + formatEnc = "_encoded.png" + image_channel_depth = cv2.IMREAD_UNCHANGED + linear_straight = True + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + + formatEnc = "_encoded.png" + image_channel_depth = cv2.IMREAD_UNCHANGED + + if sceneProperties.tlm_encoding_mode_b == "RGBD": + + formatEnc = "_encoded.png" + image_channel_depth = cv2.IMREAD_UNCHANGED + + packer = {} + + for atlas in bpy.context.scene.TLM_PostAtlasList: #For each atlas + + packer[atlas.name] = newPacker(PackingMode.Offline, PackingBin.BFF, rotation=False) + + bpy.app.driver_namespace["logman"].append("Postpacking: " + str(atlas.name)) + + if scene.TLM_EngineProperties.tlm_setting_supersample == "2x": + supersampling_scale = 2 + elif scene.TLM_EngineProperties.tlm_setting_supersample == "4x": + supersampling_scale = 4 + else: + supersampling_scale = 1 + + atlas_resolution = int(int(atlas.tlm_atlas_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)) + + packer[atlas.name].add_bin(atlas_resolution, atlas_resolution, 1) + + #AtlasList same name prevention? + rect = [] + + #For each object that targets the atlas + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: + + res = int(int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) / int(scene.TLM_EngineProperties.tlm_resolution_scale) * int(supersampling_scale)) + + rect.append((res, res, obj.name)) + + #Add rect to bin + for r in rect: + packer[atlas.name].add_rect(*r) + + print("Rects: " + str(rect)) + print("Bins:" + str(packer[atlas.name])) + + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 3), dtype="float32") + + #Continue here...overwrite value if using 8-bit encoding + if sceneProperties.tlm_encoding_use: + if sceneProperties.tlm_encoding_device == "CPU": + if sceneProperties.tlm_encoding_mode_a == "RGBM": + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8) + if sceneProperties.tlm_encoding_mode_a == "RGBD": + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8) + + if sceneProperties.tlm_encoding_device == "GPU": + if sceneProperties.tlm_encoding_mode_b == "RGBM": + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8) + if sceneProperties.tlm_encoding_mode_b == "RGBD": + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8) + if sceneProperties.tlm_encoding_mode_b == "LogLuv": + packedAtlas[atlas.name] = np.zeros((atlas_resolution,atlas_resolution, 4), dtype=np.uint8) + + packer[atlas.name].pack() + + for idy, rect in enumerate(packer[atlas.name].rect_list()): + + print("Packing atlas at: " + str(rect)) + + aob = rect[5] + + src = cv2.imread(os.path.join(lightmap_directory, aob + end + formatEnc), image_channel_depth) #"_baked.hdr" + + print("Obj name is: " + aob) + + x,y,w,h = rect[1],rect[2],rect[3],rect[4] + + print("Obj Shape: " + str(src.shape)) + print("Atlas shape: " + str(packedAtlas[atlas.name].shape)) + + print("Bin Pos: ",x,y,w,h) + + + packedAtlas[atlas.name][y:h+y, x:w+x] = src + + obj = bpy.data.objects[aob] + + for idx, layer in enumerate(obj.data.uv_layers): + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if layer.name == uv_channel: + obj.data.uv_layers.active_index = idx + + print("UVLayer set to: " + str(obj.data.uv_layers.active_index)) + + atlasRes = atlas_resolution + texRes = rect[3] #Any dimension w/h (square) + ratio = texRes/atlasRes + scaleUV(obj.data.uv_layers.active, (ratio, ratio), (0,1)) + print(rect) + + #Postpack error here... + for uv_verts in obj.data.uv_layers.active.data: + #For each vert + + #NOTES! => X FUNKER + #TODO => Y + + #[0] = bin index + #[1] = x + #[2] = y (? + 1) + #[3] = w + #[4] = h + + vertex_x = uv_verts.uv[0] + (rect[1]/atlasRes) #WORKING! + vertex_y = uv_verts.uv[1] - (rect[2]/atlasRes) # + ((rect[2]-rect[4])/atlasRes) # # + (1-((rect[1]-rect[4])/atlasRes)) + #tr = "X: {0} + ({1}/{2})".format(uv_verts.uv[0],rect[1],atlasRes) + #print(tr) + #vertex_y = 1 - (uv_verts.uv[1]) uv_verts.uv[1] + (rect[1]/atlasRes) + + #SET UV LAYER TO + + # atlasRes = atlas_resolution + # texRes = rect[3] #Any dimension w/h (square) + # print(texRes) + # #texRes = 0.0,0.0 + # #x,y,w,z = x,y,texRes,texRes + # x,y,w,z = x,y,0,0 + + # ratio = atlasRes/texRes + + # if x == 0: + # x_offset = 0 + # else: + # x_offset = 1/(atlasRes/x) + + # if y == 0: + # y_offset = 0 + # else: + # y_offset = 1/(atlasRes/y) + + # vertex_x = (uv_verts.uv[0] * 1/(ratio)) + x_offset + # vertex_y = (1 - ((uv_verts.uv[1] * 1/(ratio)) + y_offset)) + + #TO FIX: + #SELECT ALL + #Scale Y => -1 + + uv_verts.uv[0] = vertex_x + uv_verts.uv[1] = vertex_y + + #scaleUV(obj.data.uv_layers.active, (1, -1), getBoundsCenter(obj.data.uv_layers.active)) + #print(getCenter(obj.data.uv_layers.active)) + + cv2.imwrite(os.path.join(lightmap_directory, atlas.name + end + formatEnc), packedAtlas[atlas.name]) + print("Written: " + str(os.path.join(lightmap_directory, atlas.name + end + formatEnc))) + + #Change the material for each material, slot + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: + for slot in obj.material_slots: + nodetree = slot.material.node_tree + + for node in nodetree.nodes: + + if node.name == "TLM_Lightmap": + + existing_image = node.image + + atlasImage = bpy.data.images.load(os.path.join(lightmap_directory, atlas.name + end + formatEnc), check_existing=True) + + if linear_straight: + if atlasImage.colorspace_settings.name != 'Linear': + atlasImage.colorspace_settings.name = 'Linear' + + node.image = atlasImage + + #print("Seeking for: " + atlasImage.filepath_raw) + #print(x) + + if(os.path.exists(os.path.join(lightmap_directory, obj.name + end + formatEnc))): + os.remove(os.path.join(lightmap_directory, obj.name + end + formatEnc)) + existing_image.user_clear() + + #Add dilation map here... + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: + if atlas.tlm_atlas_dilation: + for slot in obj.material_slots: + nodetree = slot.material.node_tree + + for node in nodetree.nodes: + + if node.name == "TLM_Lightmap": + + existing_image = node.image + + atlasImage = bpy.data.images.load(os.path.join(lightmap_directory, atlas.name + end + formatEnc), check_existing=True) + + img = cv2.imread(atlasImage.filepath_raw, image_channel_depth) + + kernel = np.ones((5,5), dtype="float32") + + img_dilation = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) + + cv2.imshow('Dilation', img_dilation) + cv2.waitKey(0) + + print("TODO: Adding dilation for: " + obj.name) + #TODO MASKING OPTION! + + + + else: + + print("OpenCV not installed. Skipping postpacking process.") + +def getCenter(uv_layer): + + total_x, total_y = 0,0 + len = 0 + + for uv_verts in uv_layer.data: + total_x += uv_verts.uv[0] + total_y += uv_verts.uv[1] + + len += 1 + + center_x = total_x / len + center_y = total_y / len + + return (center_x, center_y) + +def getBoundsCenter(uv_layer): + + min_x = getCenter(uv_layer)[0] + max_x = getCenter(uv_layer)[0] + min_y = getCenter(uv_layer)[1] + max_y = getCenter(uv_layer)[1] + + len = 0 + + for uv_verts in uv_layer.data: + + if uv_verts.uv[0] < min_x: + min_x = uv_verts.uv[0] + if uv_verts.uv[0] > max_x: + max_x = uv_verts.uv[0] + if uv_verts.uv[1] < min_y: + min_y = uv_verts.uv[1] + if uv_verts.uv[1] > max_y: + max_y = uv_verts.uv[1] + + center_x = (max_x - min_x) / 2 + min_x + center_y = (max_y - min_y) / 2 + min_y + + return (center_x, center_y) + + +def scale2D(v, s, p): + return (p[0] + s[0]*(v[0] - p[0]), p[1] + s[1]*(v[1] - p[1])) + +def scaleUV( uvMap, scale, pivot ): + for uvIndex in range( len(uvMap.data) ): + uvMap.data[uvIndex].uv = scale2D(uvMap.data[uvIndex].uv, scale, pivot) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/preconfiguration/object.py b/blender/arm/lightmapper/utility/preconfiguration/object.py new file mode 100644 index 0000000000..103749f53c --- /dev/null +++ b/blender/arm/lightmapper/utility/preconfiguration/object.py @@ -0,0 +1,5 @@ +import bpy, os, re, sys + +def prepare(obj): + print("Preparing: " + obj.name) + pass \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/rectpack/__init__.py b/blender/arm/lightmapper/utility/rectpack/__init__.py new file mode 100644 index 0000000000..18436c8a91 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/__init__.py @@ -0,0 +1,23 @@ +from .guillotine import GuillotineBssfSas, GuillotineBssfLas, \ + GuillotineBssfSlas, GuillotineBssfLlas, GuillotineBssfMaxas, \ + GuillotineBssfMinas, GuillotineBlsfSas, GuillotineBlsfLas, \ + GuillotineBlsfSlas, GuillotineBlsfLlas, GuillotineBlsfMaxas, \ + GuillotineBlsfMinas, GuillotineBafSas, GuillotineBafLas, \ + GuillotineBafSlas, GuillotineBafLlas, GuillotineBafMaxas, \ + GuillotineBafMinas + +from .maxrects import MaxRectsBl, MaxRectsBssf, MaxRectsBaf, MaxRectsBlsf + +from .skyline import SkylineMwf, SkylineMwfl, SkylineBl, \ + SkylineBlWm, SkylineMwfWm, SkylineMwflWm + +from .packer import SORT_AREA, SORT_PERI, SORT_DIFF, SORT_SSIDE, \ + SORT_LSIDE, SORT_RATIO, SORT_NONE + +from .packer import PackerBNF, PackerBFF, PackerBBF, PackerOnlineBNF, \ + PackerOnlineBFF, PackerOnlineBBF, PackerGlobal, newPacker, \ + PackingMode, PackingBin, float2dec + + + + diff --git a/blender/arm/lightmapper/utility/rectpack/enclose.py b/blender/arm/lightmapper/utility/rectpack/enclose.py new file mode 100644 index 0000000000..4417866539 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/enclose.py @@ -0,0 +1,148 @@ +import heapq # heapq.heappush, heapq.heappop +from .packer import newPacker, PackingMode, PackingBin, SORT_LSIDE +from .skyline import SkylineBlWm + + + +class Enclose(object): + + def __init__(self, rectangles=[], max_width=None, max_height=None, rotation=True): + """ + Arguments: + rectangles (list): Rectangle to be enveloped + [(width1, height1), (width2, height2), ...] + max_width (number|None): Enveloping rectangle max allowed width. + max_height (number|None): Enveloping rectangle max allowed height. + rotation (boolean): Enable/Disable rectangle rotation. + """ + # Enclosing rectangle max width + self._max_width = max_width + + # Encloseing rectangle max height + self._max_height = max_height + + # Enable or disable rectangle rotation + self._rotation = rotation + + # Default packing algorithm + self._pack_algo = SkylineBlWm + + # rectangles to enclose [(width, height), (width, height, ...)] + self._rectangles = [] + for r in rectangles: + self.add_rect(*r) + + def _container_candidates(self): + """Generate container candidate list + + Returns: + tuple list: [(width1, height1), (width2, height2), ...] + """ + if not self._rectangles: + return [] + + if self._rotation: + sides = sorted(side for rect in self._rectangles for side in rect) + max_height = sum(max(r[0], r[1]) for r in self._rectangles) + min_width = max(min(r[0], r[1]) for r in self._rectangles) + max_width = max_height + else: + sides = sorted(r[0] for r in self._rectangles) + max_height = sum(r[1] for r in self._rectangles) + min_width = max(r[0] for r in self._rectangles) + max_width = sum(sides) + + if self._max_width and self._max_width < max_width: + max_width = self._max_width + + if self._max_height and self._max_height < max_height: + max_height = self._max_height + + assert(max_width>min_width) + + # Generate initial container widths + candidates = [max_width, min_width] + + width = 0 + for s in reversed(sides): + width += s + candidates.append(width) + + width = 0 + for s in sides: + width += s + candidates.append(width) + + candidates.append(max_width) + candidates.append(min_width) + + # Remove duplicates and widths too big or small + seen = set() + seen_add = seen.add + candidates = [x for x in candidates if not(x in seen or seen_add(x))] + candidates = [x for x in candidates if not(x>max_width or x=min_area] + + def _refine_candidate(self, width, height): + """ + Use bottom-left packing algorithm to find a lower height for the + container. + + Arguments: + width + height + + Returns: + tuple (width, height, PackingAlgorithm): + """ + packer = newPacker(PackingMode.Offline, PackingBin.BFF, + pack_algo=self._pack_algo, sort_algo=SORT_LSIDE, + rotation=self._rotation) + packer.add_bin(width, height) + + for r in self._rectangles: + packer.add_rect(*r) + + packer.pack() + + # Check all rectangles where packed + if len(packer[0]) != len(self._rectangles): + return None + + # Find highest rectangle + new_height = max(packer[0], key=lambda x: x.top).top + return(width, new_height, packer) + + def generate(self): + + # Generate initial containers + candidates = self._container_candidates() + if not candidates: + return None + + # Refine candidates and return the one with the smaller area + containers = [self._refine_candidate(*c) for c in candidates] + containers = [c for c in containers if c] + if not containers: + return None + + width, height, packer = min(containers, key=lambda x: x[0]*x[1]) + + packer.width = width + packer.height = height + return packer + + def add_rect(self, width, height): + """ + Add anoter rectangle to be enclosed + + Arguments: + width (number): Rectangle width + height (number): Rectangle height + """ + self._rectangles.append((width, height)) + + diff --git a/blender/arm/lightmapper/utility/rectpack/geometry.py b/blender/arm/lightmapper/utility/rectpack/geometry.py new file mode 100644 index 0000000000..f00fc8b8c7 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/geometry.py @@ -0,0 +1,344 @@ +from math import sqrt + + + +class Point(object): + + __slots__ = ('x', 'y') + + def __init__(self, x, y): + self.x = x + self.y = y + + def __eq__(self, other): + return (self.x == other.x and self.y == other.y) + + def __repr__(self): + return "P({}, {})".format(self.x, self.y) + + def distance(self, point): + """ + Calculate distance to another point + """ + return sqrt((self.x-point.x)**2+(self.y-point.y)**2) + + def distance_squared(self, point): + return (self.x-point.x)**2+(self.y-point.y)**2 + + +class Segment(object): + + __slots__ = ('start', 'end') + + def __init__(self, start, end): + """ + Arguments: + start (Point): Segment start point + end (Point): Segment end point + """ + assert(isinstance(start, Point) and isinstance(end, Point)) + self.start = start + self.end = end + + def __eq__(self, other): + if not isinstance(other, self.__class__): + None + return self.start==other.start and self.end==other.end + + def __repr__(self): + return "S({}, {})".format(self.start, self.end) + + @property + def length_squared(self): + """Faster than length and useful for some comparisons""" + return self.start.distance_squared(self.end) + + @property + def length(self): + return self.start.distance(self.end) + + @property + def top(self): + return max(self.start.y, self.end.y) + + @property + def bottom(self): + return min(self.start.y, self.end.y) + + @property + def right(self): + return max(self.start.x, self.end.x) + + @property + def left(self): + return min(self.start.x, self.end.x) + + +class HSegment(Segment): + """Horizontal Segment""" + + def __init__(self, start, length): + """ + Create an Horizontal segment given its left most end point and its + length. + + Arguments: + - start (Point): Starting Point + - length (number): segment length + """ + assert(isinstance(start, Point) and not isinstance(length, Point)) + super(HSegment, self).__init__(start, Point(start.x+length, start.y)) + + @property + def length(self): + return self.end.x-self.start.x + + +class VSegment(Segment): + """Vertical Segment""" + + def __init__(self, start, length): + """ + Create a Vertical segment given its bottom most end point and its + length. + + Arguments: + - start (Point): Starting Point + - length (number): segment length + """ + assert(isinstance(start, Point) and not isinstance(length, Point)) + super(VSegment, self).__init__(start, Point(start.x, start.y+length)) + + @property + def length(self): + return self.end.y-self.start.y + + + +class Rectangle(object): + """Basic rectangle primitive class. + x, y-> Lower right corner coordinates + width - + height - + """ + __slots__ = ('width', 'height', 'x', 'y', 'rid') + + def __init__(self, x, y, width, height, rid = None): + """ + Args: + x (int, float): + y (int, float): + width (int, float): + height (int, float): + rid (int): + """ + assert(height >=0 and width >=0) + + self.width = width + self.height = height + self.x = x + self.y = y + self.rid = rid + + @property + def bottom(self): + """ + Rectangle bottom edge y coordinate + """ + return self.y + + @property + def top(self): + """ + Rectangle top edge y coordiante + """ + return self.y+self.height + + @property + def left(self): + """ + Rectangle left ednge x coordinate + """ + return self.x + + @property + def right(self): + """ + Rectangle right edge x coordinate + """ + return self.x+self.width + + @property + def corner_top_l(self): + return Point(self.left, self.top) + + @property + def corner_top_r(self): + return Point(self.right, self.top) + + @property + def corner_bot_r(self): + return Point(self.right, self.bottom) + + @property + def corner_bot_l(self): + return Point(self.left, self.bottom) + + def __lt__(self, other): + """ + Compare rectangles by area (used for sorting) + """ + return self.area() < other.area() + + def __eq__(self, other): + """ + Equal rectangles have same area. + """ + if not isinstance(other, self.__class__): + return False + + return (self.width == other.width and \ + self.height == other.height and \ + self.x == other.x and \ + self.y == other.y) + + def __hash__(self): + return hash((self.x, self.y, self.width, self.height)) + + def __iter__(self): + """ + Iterate through rectangle corners + """ + yield self.corner_top_l + yield self.corner_top_r + yield self.corner_bot_r + yield self.corner_bot_l + + def __repr__(self): + return "R({}, {}, {}, {})".format(self.x, self.y, self.width, self.height) + + def area(self): + """ + Rectangle area + """ + return self.width * self.height + + def move(self, x, y): + """ + Move Rectangle to x,y coordinates + + Arguments: + x (int, float): X coordinate + y (int, float): Y coordinate + """ + self.x = x + self.y = y + + def contains(self, rect): + """ + Tests if another rectangle is contained by this one + + Arguments: + rect (Rectangle): The other rectangle + + Returns: + bool: True if it is container, False otherwise + """ + return (rect.y >= self.y and \ + rect.x >= self.x and \ + rect.y+rect.height <= self.y+self.height and \ + rect.x+rect.width <= self.x+self.width) + + def intersects(self, rect, edges=False): + """ + Detect intersections between this and another Rectangle. + + Parameters: + rect (Rectangle): The other rectangle. + edges (bool): True to consider rectangles touching by their + edges or corners to be intersecting. + (Should have been named include_touching) + + Returns: + bool: True if the rectangles intersect, False otherwise + """ + if edges: + if (self.bottom > rect.top or self.top < rect.bottom or\ + self.left > rect.right or self.right < rect.left): + return False + else: + if (self.bottom >= rect.top or self.top <= rect.bottom or + self.left >= rect.right or self.right <= rect.left): + return False + + return True + + def intersection(self, rect, edges=False): + """ + Returns the rectangle resulting of the intersection between this and another + rectangle. If the rectangles are only touching by their edges, and the + argument 'edges' is True the rectangle returned will have an area of 0. + Returns None if there is no intersection. + + Arguments: + rect (Rectangle): The other rectangle. + edges (bool): If True Rectangles touching by their edges are + considered to be intersection. In this case a rectangle of + 0 height or/and width will be returned. + + Returns: + Rectangle: Intersection. + None: There was no intersection. + """ + if not self.intersects(rect, edges=edges): + return None + + bottom = max(self.bottom, rect.bottom) + left = max(self.left, rect.left) + top = min(self.top, rect.top) + right = min(self.right, rect.right) + + return Rectangle(left, bottom, right-left, top-bottom) + + def join(self, other): + """ + Try to join a rectangle to this one, if the result is also a rectangle + and the operation is successful and this rectangle is modified to the union. + + Arguments: + other (Rectangle): Rectangle to join + + Returns: + bool: True when successfully joined, False otherwise + """ + if self.contains(other): + return True + + if other.contains(self): + self.x = other.x + self.y = other.y + self.width = other.width + self.height = other.height + return True + + if not self.intersects(other, edges=True): + return False + + # Other rectangle is Up/Down from this + if self.left == other.left and self.width == other.width: + y_min = min(self.bottom, other.bottom) + y_max = max(self.top, other.top) + self.y = y_min + self.height = y_max-y_min + return True + + # Other rectangle is Right/Left from this + if self.bottom == other.bottom and self.height == other.height: + x_min = min(self.left, other.left) + x_max = max(self.right, other.right) + self.x = x_min + self.width = x_max-x_min + return True + + return False + diff --git a/blender/arm/lightmapper/utility/rectpack/guillotine.py b/blender/arm/lightmapper/utility/rectpack/guillotine.py new file mode 100644 index 0000000000..dbf3fb1ee6 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/guillotine.py @@ -0,0 +1,368 @@ +from .pack_algo import PackingAlgorithm +from .geometry import Rectangle +import itertools +import operator + + +class Guillotine(PackingAlgorithm): + """Implementation of several variants of Guillotine packing algorithm + + For a more detailed explanation of the algorithm used, see: + Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010) + """ + def __init__(self, width, height, rot=True, merge=True, *args, **kwargs): + """ + Arguments: + width (int, float): + height (int, float): + merge (bool): Optional keyword argument + """ + self._merge = merge + super(Guillotine, self).__init__(width, height, rot, *args, **kwargs) + + + def _add_section(self, section): + """Adds a new section to the free section list, but before that and if + section merge is enabled, tries to join the rectangle with all existing + sections, if successful the resulting section is again merged with the + remaining sections until the operation fails. The result is then + appended to the list. + + Arguments: + section (Rectangle): New free section. + """ + section.rid = 0 + plen = 0 + + while self._merge and self._sections and plen != len(self._sections): + plen = len(self._sections) + self._sections = [s for s in self._sections if not section.join(s)] + self._sections.append(section) + + + def _split_horizontal(self, section, width, height): + """For an horizontal split the rectangle is placed in the lower + left corner of the section (section's xy coordinates), the top + most side of the rectangle and its horizontal continuation, + marks the line of division for the split. + +-----------------+ + | | + | | + | | + | | + +-------+---------+ + |#######| | + |#######| | + |#######| | + +-------+---------+ + If the rectangle width is equal to the the section width, only one + section is created over the rectangle. If the rectangle height is + equal to the section height, only one section to the right of the + rectangle is created. If both width and height are equal, no sections + are created. + """ + # First remove the section we are splitting so it doesn't + # interfere when later we try to merge the resulting split + # rectangles, with the rest of free sections. + #self._sections.remove(section) + + # Creates two new empty sections, and returns the new rectangle. + if height < section.height: + self._add_section(Rectangle(section.x, section.y+height, + section.width, section.height-height)) + + if width < section.width: + self._add_section(Rectangle(section.x+width, section.y, + section.width-width, height)) + + + def _split_vertical(self, section, width, height): + """For a vertical split the rectangle is placed in the lower + left corner of the section (section's xy coordinates), the + right most side of the rectangle and its vertical continuation, + marks the line of division for the split. + +-------+---------+ + | | | + | | | + | | | + | | | + +-------+ | + |#######| | + |#######| | + |#######| | + +-------+---------+ + If the rectangle width is equal to the the section width, only one + section is created over the rectangle. If the rectangle height is + equal to the section height, only one section to the right of the + rectangle is created. If both width and height are equal, no sections + are created. + """ + # When a section is split, depending on the rectangle size + # two, one, or no new sections will be created. + if height < section.height: + self._add_section(Rectangle(section.x, section.y+height, + width, section.height-height)) + + if width < section.width: + self._add_section(Rectangle(section.x+width, section.y, + section.width-width, section.height)) + + + def _split(self, section, width, height): + """ + Selects the best split for a section, given a rectangle of dimmensions + width and height, then calls _split_vertical or _split_horizontal, + to do the dirty work. + + Arguments: + section (Rectangle): Section to split + width (int, float): Rectangle width + height (int, float): Rectangle height + """ + raise NotImplementedError + + + def _section_fitness(self, section, width, height): + """The subclass for each one of the Guillotine selection methods, + BAF, BLSF.... will override this method, this is here only + to asure a valid value return if the worst happens. + """ + raise NotImplementedError + + def _select_fittest_section(self, w, h): + """Calls _section_fitness for each of the sections in free section + list. Returns the section with the minimal fitness value, all the rest + is boilerplate to make the fitness comparison, to rotatate the rectangles, + and to take into account when _section_fitness returns None because + the rectangle couldn't be placed. + + Arguments: + w (int, float): Rectangle width + h (int, float): Rectangle height + + Returns: + (section, was_rotated): Returns the tuple + section (Rectangle): Section with best fitness + was_rotated (bool): The rectangle was rotated + """ + fitn = ((self._section_fitness(s, w, h), s, False) for s in self._sections + if self._section_fitness(s, w, h) is not None) + fitr = ((self._section_fitness(s, h, w), s, True) for s in self._sections + if self._section_fitness(s, h, w) is not None) + + if not self.rot: + fitr = [] + + fit = itertools.chain(fitn, fitr) + + try: + _, sec, rot = min(fit, key=operator.itemgetter(0)) + except ValueError: + return None, None + + return sec, rot + + + def add_rect(self, width, height, rid=None): + """ + Add rectangle of widthxheight dimensions. + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + rid: Optional rectangle user id + + Returns: + Rectangle: Rectangle with placemente coordinates + None: If the rectangle couldn be placed. + """ + assert(width > 0 and height >0) + + # Obtain the best section to place the rectangle. + section, rotated = self._select_fittest_section(width, height) + if not section: + return None + + if rotated: + width, height = height, width + + # Remove section, split and store results + self._sections.remove(section) + self._split(section, width, height) + + # Store rectangle in the selected position + rect = Rectangle(section.x, section.y, width, height, rid) + self.rectangles.append(rect) + return rect + + def fitness(self, width, height): + """ + In guillotine algorithm case, returns the min of the fitness of all + free sections, for the given dimension, both normal and rotated + (if rotation enabled.) + """ + assert(width > 0 and height > 0) + + # Get best fitness section. + section, rotated = self._select_fittest_section(width, height) + if not section: + return None + + # Return fitness of returned section, with correct dimmensions if the + # the rectangle was rotated. + if rotated: + return self._section_fitness(section, height, width) + else: + return self._section_fitness(section, width, height) + + def reset(self): + super(Guillotine, self).reset() + self._sections = [] + self._add_section(Rectangle(0, 0, self.width, self.height)) + + + +class GuillotineBaf(Guillotine): + """Implements Best Area Fit (BAF) section selection criteria for + Guillotine algorithm. + """ + def _section_fitness(self, section, width, height): + if width > section.width or height > section.height: + return None + return section.area()-width*height + + +class GuillotineBlsf(Guillotine): + """Implements Best Long Side Fit (BLSF) section selection criteria for + Guillotine algorithm. + """ + def _section_fitness(self, section, width, height): + if width > section.width or height > section.height: + return None + return max(section.width-width, section.height-height) + + +class GuillotineBssf(Guillotine): + """Implements Best Short Side Fit (BSSF) section selection criteria for + Guillotine algorithm. + """ + def _section_fitness(self, section, width, height): + if width > section.width or height > section.height: + return None + return min(section.width-width, section.height-height) + + +class GuillotineSas(Guillotine): + """Implements Short Axis Split (SAS) selection rule for Guillotine + algorithm. + """ + def _split(self, section, width, height): + if section.width < section.height: + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +class GuillotineLas(Guillotine): + """Implements Long Axis Split (LAS) selection rule for Guillotine + algorithm. + """ + def _split(self, section, width, height): + if section.width >= section.height: + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +class GuillotineSlas(Guillotine): + """Implements Short Leftover Axis Split (SLAS) selection rule for + Guillotine algorithm. + """ + def _split(self, section, width, height): + if section.width-width < section.height-height: + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +class GuillotineLlas(Guillotine): + """Implements Long Leftover Axis Split (LLAS) selection rule for + Guillotine algorithm. + """ + def _split(self, section, width, height): + if section.width-width >= section.height-height: + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +class GuillotineMaxas(Guillotine): + """Implements Max Area Axis Split (MAXAS) selection rule for Guillotine + algorithm. Maximize the larger area == minimize the smaller area. + Tries to make the rectangles more even-sized. + """ + def _split(self, section, width, height): + if width*(section.height-height) <= height*(section.width-width): + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +class GuillotineMinas(Guillotine): + """Implements Min Area Axis Split (MINAS) selection rule for Guillotine + algorithm. + """ + def _split(self, section, width, height): + if width*(section.height-height) >= height*(section.width-width): + return self._split_horizontal(section, width, height) + else: + return self._split_vertical(section, width, height) + + + +# Guillotine algorithms GUILLOTINE-RECT-SPLIT, Selecting one +# Axis split, and one selection criteria. +class GuillotineBssfSas(GuillotineBssf, GuillotineSas): + pass +class GuillotineBssfLas(GuillotineBssf, GuillotineLas): + pass +class GuillotineBssfSlas(GuillotineBssf, GuillotineSlas): + pass +class GuillotineBssfLlas(GuillotineBssf, GuillotineLlas): + pass +class GuillotineBssfMaxas(GuillotineBssf, GuillotineMaxas): + pass +class GuillotineBssfMinas(GuillotineBssf, GuillotineMinas): + pass +class GuillotineBlsfSas(GuillotineBlsf, GuillotineSas): + pass +class GuillotineBlsfLas(GuillotineBlsf, GuillotineLas): + pass +class GuillotineBlsfSlas(GuillotineBlsf, GuillotineSlas): + pass +class GuillotineBlsfLlas(GuillotineBlsf, GuillotineLlas): + pass +class GuillotineBlsfMaxas(GuillotineBlsf, GuillotineMaxas): + pass +class GuillotineBlsfMinas(GuillotineBlsf, GuillotineMinas): + pass +class GuillotineBafSas(GuillotineBaf, GuillotineSas): + pass +class GuillotineBafLas(GuillotineBaf, GuillotineLas): + pass +class GuillotineBafSlas(GuillotineBaf, GuillotineSlas): + pass +class GuillotineBafLlas(GuillotineBaf, GuillotineLlas): + pass +class GuillotineBafMaxas(GuillotineBaf, GuillotineMaxas): + pass +class GuillotineBafMinas(GuillotineBaf, GuillotineMinas): + pass + + + diff --git a/blender/arm/lightmapper/utility/rectpack/maxrects.py b/blender/arm/lightmapper/utility/rectpack/maxrects.py new file mode 100644 index 0000000000..5af8d61c16 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/maxrects.py @@ -0,0 +1,244 @@ +from .pack_algo import PackingAlgorithm +from .geometry import Rectangle +import itertools +import collections +import operator + + +first_item = operator.itemgetter(0) + + + +class MaxRects(PackingAlgorithm): + + def __init__(self, width, height, rot=True, *args, **kwargs): + super(MaxRects, self).__init__(width, height, rot, *args, **kwargs) + + def _rect_fitness(self, max_rect, width, height): + """ + Arguments: + max_rect (Rectangle): Destination max_rect + width (int, float): Rectangle width + height (int, float): Rectangle height + + Returns: + None: Rectangle couldn't be placed into max_rect + integer, float: fitness value + """ + if width <= max_rect.width and height <= max_rect.height: + return 0 + else: + return None + + def _select_position(self, w, h): + """ + Find max_rect with best fitness for placing a rectangle + of dimentsions w*h + + Arguments: + w (int, float): Rectangle width + h (int, float): Rectangle height + + Returns: + (rect, max_rect) + rect (Rectangle): Placed rectangle or None if was unable. + max_rect (Rectangle): Maximal rectangle were rect was placed + """ + if not self._max_rects: + return None, None + + # Normal rectangle + fitn = ((self._rect_fitness(m, w, h), w, h, m) for m in self._max_rects + if self._rect_fitness(m, w, h) is not None) + + # Rotated rectangle + fitr = ((self._rect_fitness(m, h, w), h, w, m) for m in self._max_rects + if self._rect_fitness(m, h, w) is not None) + + if not self.rot: + fitr = [] + + fit = itertools.chain(fitn, fitr) + + try: + _, w, h, m = min(fit, key=first_item) + except ValueError: + return None, None + + return Rectangle(m.x, m.y, w, h), m + + def _generate_splits(self, m, r): + """ + When a rectangle is placed inside a maximal rectangle, it stops being one + and up to 4 new maximal rectangles may appear depending on the placement. + _generate_splits calculates them. + + Arguments: + m (Rectangle): max_rect rectangle + r (Rectangle): rectangle placed + + Returns: + list : list containing new maximal rectangles or an empty list + """ + new_rects = [] + + if r.left > m.left: + new_rects.append(Rectangle(m.left, m.bottom, r.left-m.left, m.height)) + if r.right < m.right: + new_rects.append(Rectangle(r.right, m.bottom, m.right-r.right, m.height)) + if r.top < m.top: + new_rects.append(Rectangle(m.left, r.top, m.width, m.top-r.top)) + if r.bottom > m.bottom: + new_rects.append(Rectangle(m.left, m.bottom, m.width, r.bottom-m.bottom)) + + return new_rects + + def _split(self, rect): + """ + Split all max_rects intersecting the rectangle rect into up to + 4 new max_rects. + + Arguments: + rect (Rectangle): Rectangle + + Returns: + split (Rectangle list): List of rectangles resulting from the split + """ + max_rects = collections.deque() + + for r in self._max_rects: + if r.intersects(rect): + max_rects.extend(self._generate_splits(r, rect)) + else: + max_rects.append(r) + + # Add newly generated max_rects + self._max_rects = list(max_rects) + + def _remove_duplicates(self): + """ + Remove every maximal rectangle contained by another one. + """ + contained = set() + for m1, m2 in itertools.combinations(self._max_rects, 2): + if m1.contains(m2): + contained.add(m2) + elif m2.contains(m1): + contained.add(m1) + + # Remove from max_rects + self._max_rects = [m for m in self._max_rects if m not in contained] + + def fitness(self, width, height): + """ + Metric used to rate how much space is wasted if a rectangle is placed. + Returns a value greater or equal to zero, the smaller the value the more + 'fit' is the rectangle. If the rectangle can't be placed, returns None. + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + + Returns: + int, float: Rectangle fitness + None: Rectangle can't be placed + """ + assert(width > 0 and height > 0) + + rect, max_rect = self._select_position(width, height) + if rect is None: + return None + + # Return fitness + return self._rect_fitness(max_rect, rect.width, rect.height) + + def add_rect(self, width, height, rid=None): + """ + Add rectangle of widthxheight dimensions. + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + rid: Optional rectangle user id + + Returns: + Rectangle: Rectangle with placemente coordinates + None: If the rectangle couldn be placed. + """ + assert(width > 0 and height >0) + + # Search best position and orientation + rect, _ = self._select_position(width, height) + if not rect: + return None + + # Subdivide all the max rectangles intersecting with the selected + # rectangle. + self._split(rect) + + # Remove any max_rect contained by another + self._remove_duplicates() + + # Store and return rectangle position. + rect.rid = rid + self.rectangles.append(rect) + return rect + + def reset(self): + super(MaxRects, self).reset() + self._max_rects = [Rectangle(0, 0, self.width, self.height)] + + + + +class MaxRectsBl(MaxRects): + + def _select_position(self, w, h): + """ + Select the position where the y coordinate of the top of the rectangle + is lower, if there are severtal pick the one with the smallest x + coordinate + """ + fitn = ((m.y+h, m.x, w, h, m) for m in self._max_rects + if self._rect_fitness(m, w, h) is not None) + fitr = ((m.y+w, m.x, h, w, m) for m in self._max_rects + if self._rect_fitness(m, h, w) is not None) + + if not self.rot: + fitr = [] + + fit = itertools.chain(fitn, fitr) + + try: + _, _, w, h, m = min(fit, key=first_item) + except ValueError: + return None, None + + return Rectangle(m.x, m.y, w, h), m + + +class MaxRectsBssf(MaxRects): + """Best Sort Side Fit minimize short leftover side""" + def _rect_fitness(self, max_rect, width, height): + if width > max_rect.width or height > max_rect.height: + return None + + return min(max_rect.width-width, max_rect.height-height) + +class MaxRectsBaf(MaxRects): + """Best Area Fit pick maximal rectangle with smallest area + where the rectangle can be placed""" + def _rect_fitness(self, max_rect, width, height): + if width > max_rect.width or height > max_rect.height: + return None + + return (max_rect.width*max_rect.height)-(width*height) + + +class MaxRectsBlsf(MaxRects): + """Best Long Side Fit minimize long leftover side""" + def _rect_fitness(self, max_rect, width, height): + if width > max_rect.width or height > max_rect.height: + return None + + return max(max_rect.width-width, max_rect.height-height) diff --git a/blender/arm/lightmapper/utility/rectpack/pack_algo.py b/blender/arm/lightmapper/utility/rectpack/pack_algo.py new file mode 100644 index 0000000000..54bfe39de9 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/pack_algo.py @@ -0,0 +1,140 @@ +from .geometry import Rectangle + + +class PackingAlgorithm(object): + """PackingAlgorithm base class""" + + def __init__(self, width, height, rot=True, bid=None, *args, **kwargs): + """ + Initialize packing algorithm + + Arguments: + width (int, float): Packing surface width + height (int, float): Packing surface height + rot (bool): Rectangle rotation enabled or disabled + bid (string|int|...): Packing surface identification + """ + self.width = width + self.height = height + self.rot = rot + self.rectangles = [] + self.bid = bid + self._surface = Rectangle(0, 0, width, height) + self.reset() + + def __len__(self): + return len(self.rectangles) + + def __iter__(self): + return iter(self.rectangles) + + def _fits_surface(self, width, height): + """ + Test surface is big enough to place a rectangle + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + + Returns: + boolean: True if it could be placed, False otherwise + """ + assert(width > 0 and height > 0) + if self.rot and (width > self.width or height > self.height): + width, height = height, width + + if width > self.width or height > self.height: + return False + else: + return True + + def __getitem__(self, key): + """ + Return rectangle in selected position. + """ + return self.rectangles[key] + + def used_area(self): + """ + Total area of rectangles placed + + Returns: + int, float: Area + """ + return sum(r.area() for r in self) + + def fitness(self, width, height, rot = False): + """ + Metric used to rate how much space is wasted if a rectangle is placed. + Returns a value greater or equal to zero, the smaller the value the more + 'fit' is the rectangle. If the rectangle can't be placed, returns None. + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + rot (bool): Enable rectangle rotation + + Returns: + int, float: Rectangle fitness + None: Rectangle can't be placed + """ + raise NotImplementedError + + def add_rect(self, width, height, rid=None): + """ + Add rectangle of widthxheight dimensions. + + Arguments: + width (int, float): Rectangle width + height (int, float): Rectangle height + rid: Optional rectangle user id + + Returns: + Rectangle: Rectangle with placemente coordinates + None: If the rectangle couldn be placed. + """ + raise NotImplementedError + + def rect_list(self): + """ + Returns a list with all rectangles placed into the surface. + + Returns: + List: Format [(x, y, width, height, rid), ...] + """ + rectangle_list = [] + for r in self: + rectangle_list.append((r.x, r.y, r.width, r.height, r.rid)) + + return rectangle_list + + def validate_packing(self): + """ + Check for collisions between rectangles, also check all are placed + inside surface. + """ + surface = Rectangle(0, 0, self.width, self.height) + + for r in self: + if not surface.contains(r): + raise Exception("Rectangle placed outside surface") + + + rectangles = [r for r in self] + if len(rectangles) <= 1: + return + + for r1 in range(0, len(rectangles)-2): + for r2 in range(r1+1, len(rectangles)-1): + if rectangles[r1].intersects(rectangles[r2]): + raise Exception("Rectangle collision detected") + + def is_empty(self): + # Returns true if there is no rectangles placed. + return not bool(len(self)) + + def reset(self): + self.rectangles = [] # List of placed Rectangles. + + + diff --git a/blender/arm/lightmapper/utility/rectpack/packer.py b/blender/arm/lightmapper/utility/rectpack/packer.py new file mode 100644 index 0000000000..dba3f071b2 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/packer.py @@ -0,0 +1,580 @@ +from .maxrects import MaxRectsBssf + +import operator +import itertools +import collections + +import decimal + +# Float to Decimal helper +def float2dec(ft, decimal_digits): + """ + Convert float (or int) to Decimal (rounding up) with the + requested number of decimal digits. + + Arguments: + ft (float, int): Number to convert + decimal (int): Number of digits after decimal point + + Return: + Decimal: Number converted to decima + """ + with decimal.localcontext() as ctx: + ctx.rounding = decimal.ROUND_UP + places = decimal.Decimal(10)**(-decimal_digits) + return decimal.Decimal.from_float(float(ft)).quantize(places) + + +# Sorting algos for rectangle lists +SORT_AREA = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: r[0]*r[1]) # Sort by area + +SORT_PERI = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: r[0]+r[1]) # Sort by perimeter + +SORT_DIFF = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: abs(r[0]-r[1])) # Sort by Diff + +SORT_SSIDE = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: (min(r[0], r[1]), max(r[0], r[1]))) # Sort by short side + +SORT_LSIDE = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: (max(r[0], r[1]), min(r[0], r[1]))) # Sort by long side + +SORT_RATIO = lambda rectlist: sorted(rectlist, reverse=True, + key=lambda r: r[0]/r[1]) # Sort by side ratio + +SORT_NONE = lambda rectlist: list(rectlist) # Unsorted + + + +class BinFactory(object): + + def __init__(self, width, height, count, pack_algo, *args, **kwargs): + self._width = width + self._height = height + self._count = count + + self._pack_algo = pack_algo + self._algo_kwargs = kwargs + self._algo_args = args + self._ref_bin = None # Reference bin used to calculate fitness + + self._bid = kwargs.get("bid", None) + + def _create_bin(self): + return self._pack_algo(self._width, self._height, *self._algo_args, **self._algo_kwargs) + + def is_empty(self): + return self._count<1 + + def fitness(self, width, height): + if not self._ref_bin: + self._ref_bin = self._create_bin() + + return self._ref_bin.fitness(width, height) + + def fits_inside(self, width, height): + # Determine if rectangle widthxheight will fit into empty bin + if not self._ref_bin: + self._ref_bin = self._create_bin() + + return self._ref_bin._fits_surface(width, height) + + def new_bin(self): + if self._count > 0: + self._count -= 1 + return self._create_bin() + else: + return None + + def __eq__(self, other): + return self._width*self._height == other._width*other._height + + def __lt__(self, other): + return self._width*self._height < other._width*other._height + + def __str__(self): + return "Bin: {} {} {}".format(self._width, self._height, self._count) + + + +class PackerBNFMixin(object): + """ + BNF (Bin Next Fit): Only one open bin at a time. If the rectangle + doesn't fit, close the current bin and go to the next. + """ + + def add_rect(self, width, height, rid=None): + while True: + # if there are no open bins, try to open a new one + if len(self._open_bins)==0: + # can we find an unopened bin that will hold this rect? + new_bin = self._new_open_bin(width, height, rid=rid) + if new_bin is None: + return None + + # we have at least one open bin, so check if it can hold this rect + rect = self._open_bins[0].add_rect(width, height, rid=rid) + if rect is not None: + return rect + + # since the rect doesn't fit, close this bin and try again + closed_bin = self._open_bins.popleft() + self._closed_bins.append(closed_bin) + + +class PackerBFFMixin(object): + """ + BFF (Bin First Fit): Pack rectangle in first bin it fits + """ + + def add_rect(self, width, height, rid=None): + # see if this rect will fit in any of the open bins + for b in self._open_bins: + rect = b.add_rect(width, height, rid=rid) + if rect is not None: + return rect + + while True: + # can we find an unopened bin that will hold this rect? + new_bin = self._new_open_bin(width, height, rid=rid) + if new_bin is None: + return None + + # _new_open_bin may return a bin that's too small, + # so we have to double-check + rect = new_bin.add_rect(width, height, rid=rid) + if rect is not None: + return rect + + +class PackerBBFMixin(object): + """ + BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness + """ + + # only create this getter once + first_item = operator.itemgetter(0) + + def add_rect(self, width, height, rid=None): + + # Try packing into open bins + fit = ((b.fitness(width, height), b) for b in self._open_bins) + fit = (b for b in fit if b[0] is not None) + try: + _, best_bin = min(fit, key=self.first_item) + best_bin.add_rect(width, height, rid) + return True + except ValueError: + pass + + # Try packing into one of the empty bins + while True: + # can we find an unopened bin that will hold this rect? + new_bin = self._new_open_bin(width, height, rid=rid) + if new_bin is None: + return False + + # _new_open_bin may return a bin that's too small, + # so we have to double-check + if new_bin.add_rect(width, height, rid): + return True + + + +class PackerOnline(object): + """ + Rectangles are packed as soon are they are added + """ + + def __init__(self, pack_algo=MaxRectsBssf, rotation=True): + """ + Arguments: + pack_algo (PackingAlgorithm): What packing algo to use + rotation (bool): Enable/Disable rectangle rotation + """ + self._rotation = rotation + self._pack_algo = pack_algo + self.reset() + + def __iter__(self): + return itertools.chain(self._closed_bins, self._open_bins) + + def __len__(self): + return len(self._closed_bins)+len(self._open_bins) + + def __getitem__(self, key): + """ + Return bin in selected position. (excluding empty bins) + """ + if not isinstance(key, int): + raise TypeError("Indices must be integers") + + size = len(self) # avoid recalulations + + if key < 0: + key += size + + if not 0 <= key < size: + raise IndexError("Index out of range") + + if key < len(self._closed_bins): + return self._closed_bins[key] + else: + return self._open_bins[key-len(self._closed_bins)] + + def _new_open_bin(self, width=None, height=None, rid=None): + """ + Extract the next empty bin and append it to open bins + + Returns: + PackingAlgorithm: Initialized empty packing bin. + None: No bin big enough for the rectangle was found + """ + factories_to_delete = set() # + new_bin = None + + for key, binfac in self._empty_bins.items(): + + # Only return the new bin if the rect fits. + # (If width or height is None, caller doesn't know the size.) + if not binfac.fits_inside(width, height): + continue + + # Create bin and add to open_bins + new_bin = binfac.new_bin() + if new_bin is None: + continue + self._open_bins.append(new_bin) + + # If the factory was depleted mark for deletion + if binfac.is_empty(): + factories_to_delete.add(key) + + break + + # Delete marked factories + for f in factories_to_delete: + del self._empty_bins[f] + + return new_bin + + def add_bin(self, width, height, count=1, **kwargs): + # accept the same parameters as PackingAlgorithm objects + kwargs['rot'] = self._rotation + bin_factory = BinFactory(width, height, count, self._pack_algo, **kwargs) + self._empty_bins[next(self._bin_count)] = bin_factory + + def rect_list(self): + rectangles = [] + bin_count = 0 + + for abin in self: + for rect in abin: + rectangles.append((bin_count, rect.x, rect.y, rect.width, rect.height, rect.rid)) + bin_count += 1 + + return rectangles + + def bin_list(self): + """ + Return a list of the dimmensions of the bins in use, that is closed + or open containing at least one rectangle + """ + return [(b.width, b.height) for b in self] + + def validate_packing(self): + for b in self: + b.validate_packing() + + def reset(self): + # Bins fully packed and closed. + self._closed_bins = collections.deque() + + # Bins ready to pack rectangles + self._open_bins = collections.deque() + + # User provided bins not in current use + self._empty_bins = collections.OrderedDict() # O(1) deletion of arbitrary elem + self._bin_count = itertools.count() + + +class Packer(PackerOnline): + """ + Rectangles aren't packed untils pack() is called + """ + + def __init__(self, pack_algo=MaxRectsBssf, sort_algo=SORT_NONE, + rotation=True): + """ + """ + super(Packer, self).__init__(pack_algo=pack_algo, rotation=rotation) + + self._sort_algo = sort_algo + + # User provided bins and Rectangles + self._avail_bins = collections.deque() + self._avail_rect = collections.deque() + + # Aux vars used during packing + self._sorted_rect = [] + + def add_bin(self, width, height, count=1, **kwargs): + self._avail_bins.append((width, height, count, kwargs)) + + def add_rect(self, width, height, rid=None): + self._avail_rect.append((width, height, rid)) + + def _is_everything_ready(self): + return self._avail_rect and self._avail_bins + + def pack(self): + + self.reset() + + if not self._is_everything_ready(): + # maybe we should throw an error here? + return + + # Add available bins to packer + for b in self._avail_bins: + width, height, count, extra_kwargs = b + super(Packer, self).add_bin(width, height, count, **extra_kwargs) + + # If enabled sort rectangles + self._sorted_rect = self._sort_algo(self._avail_rect) + + # Start packing + for r in self._sorted_rect: + super(Packer, self).add_rect(*r) + + + +class PackerBNF(Packer, PackerBNFMixin): + """ + BNF (Bin Next Fit): Only one open bin, if rectangle doesn't fit + go to next bin and close current one. + """ + pass + +class PackerBFF(Packer, PackerBFFMixin): + """ + BFF (Bin First Fit): Pack rectangle in first bin it fits + """ + pass + +class PackerBBF(Packer, PackerBBFMixin): + """ + BBF (Bin Best Fit): Pack rectangle in bin that gives best fitness + """ + pass + +class PackerOnlineBNF(PackerOnline, PackerBNFMixin): + """ + BNF Bin Next Fit Online variant + """ + pass + +class PackerOnlineBFF(PackerOnline, PackerBFFMixin): + """ + BFF Bin First Fit Online variant + """ + pass + +class PackerOnlineBBF(PackerOnline, PackerBBFMixin): + """ + BBF Bin Best Fit Online variant + """ + pass + + +class PackerGlobal(Packer, PackerBNFMixin): + """ + GLOBAL: For each bin pack the rectangle with the best fitness. + """ + first_item = operator.itemgetter(0) + + def __init__(self, pack_algo=MaxRectsBssf, rotation=True): + """ + """ + super(PackerGlobal, self).__init__(pack_algo=pack_algo, + sort_algo=SORT_NONE, rotation=rotation) + + def _find_best_fit(self, pbin): + """ + Return best fitness rectangle from rectangles packing _sorted_rect list + + Arguments: + pbin (PackingAlgorithm): Packing bin + + Returns: + key of the rectangle with best fitness + """ + fit = ((pbin.fitness(r[0], r[1]), k) for k, r in self._sorted_rect.items()) + fit = (f for f in fit if f[0] is not None) + try: + _, rect = min(fit, key=self.first_item) + return rect + except ValueError: + return None + + + def _new_open_bin(self, remaining_rect): + """ + Extract the next bin where at least one of the rectangles in + rem + + Arguments: + remaining_rect (dict): rectangles not placed yet + + Returns: + PackingAlgorithm: Initialized empty packing bin. + None: No bin big enough for the rectangle was found + """ + factories_to_delete = set() # + new_bin = None + + for key, binfac in self._empty_bins.items(): + + # Only return the new bin if at least one of the remaining + # rectangles fit inside. + a_rectangle_fits = False + for _, rect in remaining_rect.items(): + if binfac.fits_inside(rect[0], rect[1]): + a_rectangle_fits = True + break + + if not a_rectangle_fits: + factories_to_delete.add(key) + continue + + # Create bin and add to open_bins + new_bin = binfac.new_bin() + if new_bin is None: + continue + self._open_bins.append(new_bin) + + # If the factory was depleted mark for deletion + if binfac.is_empty(): + factories_to_delete.add(key) + + break + + # Delete marked factories + for f in factories_to_delete: + del self._empty_bins[f] + + return new_bin + + def pack(self): + + self.reset() + + if not self._is_everything_ready(): + return + + # Add available bins to packer + for b in self._avail_bins: + width, height, count, extra_kwargs = b + super(Packer, self).add_bin(width, height, count, **extra_kwargs) + + # Store rectangles into dict for fast deletion + self._sorted_rect = collections.OrderedDict( + enumerate(self._sort_algo(self._avail_rect))) + + # For each bin pack the rectangles with lowest fitness until it is filled or + # the rectangles exhausted, then open the next bin where at least one rectangle + # will fit and repeat the process until there aren't more rectangles or bins + # available. + while len(self._sorted_rect) > 0: + + # Find one bin where at least one of the remaining rectangles fit + pbin = self._new_open_bin(self._sorted_rect) + if pbin is None: + break + + # Pack as many rectangles as possible into the open bin + while True: + + # Find 'fittest' rectangle + best_rect_key = self._find_best_fit(pbin) + if best_rect_key is None: + closed_bin = self._open_bins.popleft() + self._closed_bins.append(closed_bin) + break # None of the remaining rectangles can be packed in this bin + + best_rect = self._sorted_rect[best_rect_key] + del self._sorted_rect[best_rect_key] + + PackerBNFMixin.add_rect(self, *best_rect) + + + + + +# Packer factory +class Enum(tuple): + __getattr__ = tuple.index + +PackingMode = Enum(["Online", "Offline"]) +PackingBin = Enum(["BNF", "BFF", "BBF", "Global"]) + + +def newPacker(mode=PackingMode.Offline, + bin_algo=PackingBin.BBF, + pack_algo=MaxRectsBssf, + sort_algo=SORT_AREA, + rotation=True): + """ + Packer factory helper function + + Arguments: + mode (PackingMode): Packing mode + Online: Rectangles are packed as soon are they are added + Offline: Rectangles aren't packed untils pack() is called + bin_algo (PackingBin): Bin selection heuristic + pack_algo (PackingAlgorithm): Algorithm used + rotation (boolean): Enable or disable rectangle rotation. + + Returns: + Packer: Initialized packer instance. + """ + packer_class = None + + # Online Mode + if mode == PackingMode.Online: + sort_algo=None + if bin_algo == PackingBin.BNF: + packer_class = PackerOnlineBNF + elif bin_algo == PackingBin.BFF: + packer_class = PackerOnlineBFF + elif bin_algo == PackingBin.BBF: + packer_class = PackerOnlineBBF + else: + raise AttributeError("Unsupported bin selection heuristic") + + # Offline Mode + elif mode == PackingMode.Offline: + if bin_algo == PackingBin.BNF: + packer_class = PackerBNF + elif bin_algo == PackingBin.BFF: + packer_class = PackerBFF + elif bin_algo == PackingBin.BBF: + packer_class = PackerBBF + elif bin_algo == PackingBin.Global: + packer_class = PackerGlobal + sort_algo=None + else: + raise AttributeError("Unsupported bin selection heuristic") + + else: + raise AttributeError("Unknown packing mode.") + + if sort_algo: + return packer_class(pack_algo=pack_algo, sort_algo=sort_algo, + rotation=rotation) + else: + return packer_class(pack_algo=pack_algo, rotation=rotation) + + diff --git a/blender/arm/lightmapper/utility/rectpack/skyline.py b/blender/arm/lightmapper/utility/rectpack/skyline.py new file mode 100644 index 0000000000..62ab26dfd2 --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/skyline.py @@ -0,0 +1,303 @@ +import collections +import itertools +import operator +import heapq +import copy +from .pack_algo import PackingAlgorithm +from .geometry import Point as P +from .geometry import HSegment, Rectangle +from .waste import WasteManager + + +class Skyline(PackingAlgorithm): + """ Class implementing Skyline algorithm as described by + Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010) + + _skyline: stores all the segments at the top of the skyline. + _waste: Handles all wasted sections. + """ + + def __init__(self, width, height, rot=True, *args, **kwargs): + """ + _skyline is the list used to store all the skyline segments, each + one is a list with the format [x, y, width] where x is the x + coordinate of the left most point of the segment, y the y coordinate + of the segment, and width the length of the segment. The initial + segment is allways [0, 0, surface_width] + + Arguments: + width (int, float): + height (int, float): + rot (bool): Enable or disable rectangle rotation + """ + self._waste_management = False + self._waste = WasteManager(rot=rot) + super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs) + + def _placement_points_generator(self, skyline, width): + """Returns a generator for the x coordinates of all the placement + points on the skyline for a given rectangle. + + WARNING: In some cases could be duplicated points, but it is faster + to compute them twice than to remove them. + + Arguments: + skyline (list): Skyline HSegment list + width (int, float): Rectangle width + + Returns: + generator + """ + skyline_r = skyline[-1].right + skyline_l = skyline[0].left + + # Placements using skyline segment left point + ppointsl = (s.left for s in skyline if s.left+width <= skyline_r) + + # Placements using skyline segment right point + ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l) + + # Merge positions + return heapq.merge(ppointsl, ppointsr) + + def _generate_placements(self, width, height): + """ + Generate a list with + + Arguments: + skyline (list): SkylineHSegment list + width (number): + + Returns: + tuple (Rectangle, fitness): + Rectangle: Rectangle in valid position + left_skyline: Index for the skyline under the rectangle left edge. + right_skyline: Index for the skyline under the rectangle right edte. + """ + skyline = self._skyline + + points = collections.deque() + + left_index = right_index = 0 # Left and right side skyline index + support_height = skyline[0].top + support_index = 0 + + placements = self._placement_points_generator(skyline, width) + for p in placements: + + # If Rectangle's right side changed segment, find new support + if p+width > skyline[right_index].right: + for right_index in range(right_index+1, len(skyline)): + if skyline[right_index].top >= support_height: + support_index = right_index + support_height = skyline[right_index].top + if p+width <= skyline[right_index].right: + break + + # If left side changed segment. + if p >= skyline[left_index].right: + left_index +=1 + + # Find new support if the previous one was shifted out. + if support_index < left_index: + support_index = left_index + support_height = skyline[left_index].top + for i in range(left_index, right_index+1): + if skyline[i].top >= support_height: + support_index = i + support_height = skyline[i].top + + # Add point if there is enought room at the top + if support_height+height <= self.height: + points.append((Rectangle(p, support_height, width, height),\ + left_index, right_index)) + + return points + + def _merge_skyline(self, skylineq, segment): + """ + Arguments: + skylineq (collections.deque): + segment (HSegment): + """ + if len(skylineq) == 0: + skylineq.append(segment) + return + + if skylineq[-1].top == segment.top: + s = skylineq[-1] + skylineq[-1] = HSegment(s.start, s.length+segment.length) + else: + skylineq.append(segment) + + def _add_skyline(self, rect): + """ + Arguments: + seg (Rectangle): + """ + skylineq = collections.deque([]) # Skyline after adding new one + + for sky in self._skyline: + if sky.right <= rect.left or sky.left >= rect.right: + self._merge_skyline(skylineq, sky) + continue + + if sky.left < rect.left and sky.right > rect.left: + # Skyline section partially under segment left + self._merge_skyline(skylineq, + HSegment(sky.start, rect.left-sky.left)) + sky = HSegment(P(rect.left, sky.top), sky.right-rect.left) + + if sky.left < rect.right: + if sky.left == rect.left: + self._merge_skyline(skylineq, + HSegment(P(rect.left, rect.top), rect.width)) + # Skyline section partially under segment right + if sky.right > rect.right: + self._merge_skyline(skylineq, + HSegment(P(rect.right, sky.top), sky.right-rect.right)) + sky = HSegment(sky.start, rect.right-sky.left) + + if sky.left >= rect.left and sky.right <= rect.right: + # Skyline section fully under segment, account for wasted space + if self._waste_management and sky.top < rect.bottom: + self._waste.add_waste(sky.left, sky.top, + sky.length, rect.bottom - sky.top) + else: + # Segment + self._merge_skyline(skylineq, sky) + + # Aaaaand ..... Done + self._skyline = list(skylineq) + + def _rect_fitness(self, rect, left_index, right_index): + return rect.top + + def _select_position(self, width, height): + """ + Search for the placement with the bes fitness for the rectangle. + + Returns: + tuple (Rectangle, fitness) - Rectangle placed in the fittest position + None - Rectangle couldn't be placed + """ + positions = self._generate_placements(width, height) + if self.rot and width != height: + positions += self._generate_placements(height, width) + if not positions: + return None, None + return min(((p[0], self._rect_fitness(*p))for p in positions), + key=operator.itemgetter(1)) + + def fitness(self, width, height): + """Search for the best fitness + """ + assert(width > 0 and height >0) + if width > max(self.width, self.height) or\ + height > max(self.height, self.width): + return None + + # If there is room in wasted space, FREE PACKING!! + if self._waste_management: + if self._waste.fitness(width, height) is not None: + return 0 + + # Get best fitness segment, for normal rectangle, and for + # rotated rectangle if rotation is enabled. + rect, fitness = self._select_position(width, height) + return fitness + + def add_rect(self, width, height, rid=None): + """ + Add new rectangle + """ + assert(width > 0 and height > 0) + if width > max(self.width, self.height) or\ + height > max(self.height, self.width): + return None + + rect = None + # If Waste managment is enabled, first try to place the rectangle there + if self._waste_management: + rect = self._waste.add_rect(width, height, rid) + + # Get best possible rectangle position + if not rect: + rect, _ = self._select_position(width, height) + if rect: + self._add_skyline(rect) + + if rect is None: + return None + + # Store rectangle, and recalculate skyline + rect.rid = rid + self.rectangles.append(rect) + return rect + + def reset(self): + super(Skyline, self).reset() + self._skyline = [HSegment(P(0, 0), self.width)] + self._waste.reset() + + + + +class SkylineWMixin(Skyline): + """Waste managment mixin""" + def __init__(self, width, height, *args, **kwargs): + super(SkylineWMixin, self).__init__(width, height, *args, **kwargs) + self._waste_management = True + + +class SkylineMwf(Skyline): + """Implements Min Waste fit heuristic, minimizing the area wasted under the + rectangle. + """ + def _rect_fitness(self, rect, left_index, right_index): + waste = 0 + for seg in self._skyline[left_index:right_index+1]: + waste +=\ + (min(rect.right, seg.right)-max(rect.left, seg.left)) *\ + (rect.bottom-seg.top) + + return waste + + def _rect_fitnes2s(self, rect, left_index, right_index): + waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1]) + return sum(waste) + +class SkylineMwfl(Skyline): + """Implements Min Waste fit with low profile heuritic, minimizing the area + wasted below the rectangle, at the same time it tries to keep the height + minimal. + """ + def _rect_fitness(self, rect, left_index, right_index): + waste = 0 + for seg in self._skyline[left_index:right_index+1]: + waste +=\ + (min(rect.right, seg.right)-max(rect.left, seg.left)) *\ + (rect.bottom-seg.top) + + return waste*self.width*self.height+rect.top + + +class SkylineBl(Skyline): + """Implements Bottom Left heuristic, the best fit option is that which + results in which the top side of the rectangle lies at the bottom-most + position. + """ + def _rect_fitness(self, rect, left_index, right_index): + return rect.top + + + + +class SkylineBlWm(SkylineBl, SkylineWMixin): + pass + +class SkylineMwfWm(SkylineMwf, SkylineWMixin): + pass + +class SkylineMwflWm(SkylineMwfl, SkylineWMixin): + pass diff --git a/blender/arm/lightmapper/utility/rectpack/waste.py b/blender/arm/lightmapper/utility/rectpack/waste.py new file mode 100644 index 0000000000..dadc74b3ad --- /dev/null +++ b/blender/arm/lightmapper/utility/rectpack/waste.py @@ -0,0 +1,23 @@ +from .guillotine import GuillotineBafMinas +from .geometry import Rectangle + + + +class WasteManager(GuillotineBafMinas): + + def __init__(self, rot=True, merge=True, *args, **kwargs): + super(WasteManager, self).__init__(1, 1, rot=rot, merge=merge, *args, **kwargs) + + def add_waste(self, x, y, width, height): + """Add new waste section""" + self._add_section(Rectangle(x, y, width, height)) + + def _fits_surface(self, width, height): + raise NotImplementedError + + def validate_packing(self): + raise NotImplementedError + + def reset(self): + super(WasteManager, self).reset() + self._sections = [] diff --git a/blender/arm/lightmapper/utility/utility.py b/blender/arm/lightmapper/utility/utility.py new file mode 100644 index 0000000000..6b6efa312c --- /dev/null +++ b/blender/arm/lightmapper/utility/utility.py @@ -0,0 +1,677 @@ +import bpy.ops as O +import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh, shutil, glob, uuid +from io import StringIO +from threading import Thread +from queue import Queue, Empty +from dataclasses import dataclass +from dataclasses import field +from typing import List + +########################################################### +########################################################### +# This set of utility functions are courtesy of LorenzWieseke +# +# Modified by Naxela +# +# https://github.com/Naxela/The_Lightmapper/tree/Lightmap-to-GLB +########################################################### + +class Node_Types: + output_node = 'OUTPUT_MATERIAL' + ao_node = 'AMBIENT_OCCLUSION' + image_texture = 'TEX_IMAGE' + pbr_node = 'BSDF_PRINCIPLED' + diffuse = 'BSDF_DIFFUSE' + mapping = 'MAPPING' + normal_map = 'NORMAL_MAP' + bump_map = 'BUMP' + attr_node = 'ATTRIBUTE' + +class Shader_Node_Types: + emission = "ShaderNodeEmission" + image_texture = "ShaderNodeTexImage" + mapping = "ShaderNodeMapping" + normal = "ShaderNodeNormalMap" + ao = "ShaderNodeAmbientOcclusion" + uv = "ShaderNodeUVMap" + mix = "ShaderNodeMixRGB" + +def select_object(self,obj): + C = bpy.context + try: + O.object.select_all(action='DESELECT') + C.view_layer.objects.active = obj + obj.select_set(True) + except: + self.report({'INFO'},"Object not in View Layer") + + +def select_obj_by_mat(self,mat): + D = bpy.data + for obj in D.objects: + if obj.type == "MESH": + object_materials = [ + slot.material for slot in obj.material_slots] + if mat in object_materials: + select_object(self,obj) + + +def save_image(image): + + filePath = bpy.data.filepath + path = os.path.dirname(filePath) + + try: + os.mkdir(path + "/tex") + except FileExistsError: + pass + + try: + os.mkdir(path + "/tex/" + str(image.size[0])) + except FileExistsError: + pass + + if image.file_format == "JPEG" : + file_ending = ".jpg" + elif image.file_format == "PNG" : + file_ending = ".png" + + savepath = path + "/tex/" + \ + str(image.size[0]) + "/" + image.name + file_ending + + image.filepath_raw = savepath + + image.save() + +def get_file_size(filepath): + size = "Unpack Files" + try: + path = bpy.path.abspath(filepath) + size = os.path.getsize(path) + size /= 1024 + except: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("error getting file path for " + filepath) + + return (size) + + +def scale_image(image, newSize): + if (image.org_filepath != ''): + image.filepath = image.org_filepath + + image.org_filepath = image.filepath + image.scale(newSize[0], newSize[1]) + save_image(image) + + +def check_only_one_pbr(self,material): + check_ok = True + # get pbr shader + nodes = material.node_tree.nodes + pbr_node_type = Node_Types.pbr_node + pbr_nodes = find_node_by_type(nodes,pbr_node_type) + + # check only one pbr node + if len(pbr_nodes) == 0: + self.report({'INFO'}, 'No PBR Shader Found') + check_ok = False + + if len(pbr_nodes) > 1: + self.report({'INFO'}, 'More than one PBR Node found ! Clean before Baking.') + check_ok = False + + return check_ok + +# is material already the baked one +def check_is_org_material(self,material): + check_ok = True + if "_Bake" in material.name: + self.report({'INFO'}, 'Change back to org. Material') + check_ok = False + + return check_ok + + +def clean_empty_materials(self): + for obj in bpy.context.scene.objects: + for slot in obj.material_slots: + mat = slot.material + if mat is None: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Removed Empty Materials from " + obj.name) + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.material_slot_remove() + +def get_pbr_inputs(pbr_node): + + base_color_input = pbr_node.inputs["Base Color"] + metallic_input = pbr_node.inputs["Metallic"] + specular_input = pbr_node.inputs["Specular"] + roughness_input = pbr_node.inputs["Roughness"] + normal_input = pbr_node.inputs["Normal"] + + pbr_inputs = {"base_color_input":base_color_input, "metallic_input":metallic_input,"specular_input":specular_input,"roughness_input":roughness_input,"normal_input":normal_input} + return pbr_inputs + +def find_node_by_type(nodes, node_type): + nodes_found = [n for n in nodes if n.type == node_type] + return nodes_found + +def find_node_by_type_recusivly(material, note_to_start, node_type, del_nodes_inbetween=False): + nodes = material.node_tree.nodes + if note_to_start.type == node_type: + return note_to_start + + for input in note_to_start.inputs: + for link in input.links: + current_node = link.from_node + if (del_nodes_inbetween and note_to_start.type != Node_Types.normal_map and note_to_start.type != Node_Types.bump_map): + nodes.remove(note_to_start) + return find_node_by_type_recusivly(material, current_node, node_type, del_nodes_inbetween) + + +def find_node_by_name_recusivly(node, idname): + if node.bl_idname == idname: + return node + + for input in node.inputs: + for link in input.links: + current_node = link.from_node + return find_node_by_name_recusivly(current_node, idname) + +def make_link(material, socket1, socket2): + links = material.node_tree.links + links.new(socket1, socket2) + + +def add_gamma_node(material, pbrInput): + nodeToPrincipledOutput = pbrInput.links[0].from_socket + + gammaNode = material.node_tree.nodes.new("ShaderNodeGamma") + gammaNode.inputs[1].default_value = 2.2 + gammaNode.name = "Gamma Bake" + + # link in gamma + make_link(material, nodeToPrincipledOutput, gammaNode.inputs["Color"]) + make_link(material, gammaNode.outputs["Color"], pbrInput) + + +def remove_gamma_node(material, pbrInput): + nodes = material.node_tree.nodes + gammaNode = nodes.get("Gamma Bake") + nodeToPrincipledOutput = gammaNode.inputs[0].links[0].from_socket + + make_link(material, nodeToPrincipledOutput, pbrInput) + material.node_tree.nodes.remove(gammaNode) + +def apply_ao_toggle(self,context): + all_materials = bpy.data.materials + ao_toggle = context.scene.toggle_ao + for mat in all_materials: + nodes = mat.node_tree.nodes + ao_node = nodes.get("AO Bake") + if ao_node is not None: + if ao_toggle: + emission_setup(mat,ao_node.outputs["Color"]) + else: + pbr_node = find_node_by_type(nodes,Node_Types.pbr_node)[0] + remove_node(mat,"Emission Bake") + reconnect_PBR(mat, pbr_node) + + +def emission_setup(material, node_output): + nodes = material.node_tree.nodes + emission_node = add_node(material,Shader_Node_Types.emission,"Emission Bake") + + # link emission to whatever goes into current pbrInput + emission_input = emission_node.inputs[0] + make_link(material, node_output, emission_input) + + # link emission to materialOutput + surface_input = nodes.get("Material Output").inputs[0] + emission_output = emission_node.outputs[0] + make_link(material, emission_output, surface_input) + +def link_pbr_to_output(material,pbr_node): + nodes = material.node_tree.nodes + surface_input = nodes.get("Material Output").inputs[0] + make_link(material,pbr_node.outputs[0],surface_input) + + +def reconnect_PBR(material, pbrNode): + nodes = material.node_tree.nodes + pbr_output = pbrNode.outputs[0] + surface_input = nodes.get("Material Output").inputs[0] + make_link(material, pbr_output, surface_input) + +def mute_all_texture_mappings(material, do_mute): + nodes = material.node_tree.nodes + for node in nodes: + if node.bl_idname == "ShaderNodeMapping": + node.mute = do_mute + +def add_node(material,shader_node_type,node_name): + nodes = material.node_tree.nodes + new_node = nodes.get(node_name) + if new_node is None: + new_node = nodes.new(shader_node_type) + new_node.name = node_name + new_node.label = node_name + return new_node + +def remove_node(material,node_name): + nodes = material.node_tree.nodes + node = nodes.get(node_name) + if node is not None: + nodes.remove(node) + +def lightmap_to_ao(material,lightmap_node): + nodes = material.node_tree.nodes + # -----------------------AO SETUP--------------------# + # create group data + gltf_settings = bpy.data.node_groups.get('glTF Settings') + if gltf_settings is None: + bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree') + + # add group to node tree + ao_group = nodes.get('glTF Settings') + if ao_group is None: + ao_group = nodes.new('ShaderNodeGroup') + ao_group.name = 'glTF Settings' + ao_group.node_tree = bpy.data.node_groups['glTF Settings'] + + # create group inputs + if ao_group.inputs.get('Occlusion') is None: + ao_group.inputs.new('NodeSocketFloat','Occlusion') + + # mulitply to control strength + mix_node = add_node(material,Shader_Node_Types.mix,"Adjust Lightmap") + mix_node.blend_type = "MULTIPLY" + mix_node.inputs["Fac"].default_value = 1 + mix_node.inputs["Color2"].default_value = [3,3,3,1] + + # position node + ao_group.location = (lightmap_node.location[0]+600,lightmap_node.location[1]) + mix_node.location = (lightmap_node.location[0]+300,lightmap_node.location[1]) + + make_link(material,lightmap_node.outputs['Color'],mix_node.inputs['Color1']) + make_link(material,mix_node.outputs['Color'],ao_group.inputs['Occlusion']) + + +########################################################### +########################################################### +# This utility function is modified from blender_xatlas +# and calls the object without any explicit object context +# thus allowing blender_xatlas to pack from background. +########################################################### +# Code is courtesy of mattedicksoncom +# Modified by Naxela +# +# https://github.com/mattedicksoncom/blender-xatlas/ +########################################################### + +def gen_safe_name(): + genId = uuid.uuid4().hex + # genId = "u_" + genId.replace("-","_") + return "u_" + genId + +def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): + + blender_xatlas = importlib.util.find_spec("blender_xatlas") + + if blender_xatlas is not None: + import blender_xatlas + else: + return 0 + + packOptions = bpy.context.scene.pack_tool + chartOptions = bpy.context.scene.chart_tool + + sharedProperties = bpy.context.scene.shared_properties + #sharedProperties.unwrapSelection + + context = bpy.context + + #save whatever mode the user was in + startingMode = bpy.context.object.mode + selected_objects = bpy.context.selected_objects + + #check something is actually selected + #external function/operator will select them + if len(selected_objects) == 0: + print("Nothing Selected") + self.report({"WARNING"}, "Nothing Selected, please select Something") + return {'FINISHED'} + + #store the names of objects to be lightmapped + rename_dict = dict() + safe_dict = dict() + + #make sure all the objects have ligthmap uvs + for obj in selected_objects: + if obj.type == 'MESH': + safe_name = gen_safe_name(); + rename_dict[obj.name] = (obj.name,safe_name) + safe_dict[safe_name] = obj.name + context.view_layer.objects.active = obj + if obj.data.users > 1: + obj.data = obj.data.copy() #make single user copy + uv_layers = obj.data.uv_layers + + #setup the lightmap uvs + uvName = "UVMap_Lightmap" + if sharedProperties.lightmapUVChoiceType == "NAME": + uvName = sharedProperties.lightmapUVName + elif sharedProperties.lightmapUVChoiceType == "INDEX": + if sharedProperties.lightmapUVIndex < len(uv_layers): + uvName = uv_layers[sharedProperties.lightmapUVIndex].name + + if not uvName in uv_layers: + uvmap = uv_layers.new(name=uvName) + uv_layers.active_index = len(uv_layers) - 1 + else: + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uvName: + uv_layers.active_index = i + obj.select_set(True) + + #save all the current edges + if sharedProperties.packOnly: + edgeDict = dict() + for obj in selected_objects: + if obj.type == 'MESH': + tempEdgeDict = dict() + tempEdgeDict['object'] = obj.name + tempEdgeDict['edges'] = [] + print(len(obj.data.edges)) + for i in range(0,len(obj.data.edges)): + setEdge = obj.data.edges[i] + tempEdgeDict['edges'].append(i) + edgeDict[obj.name] = tempEdgeDict + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') + else: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='FIXED', ngon_method='BEAUTY') + + bpy.ops.object.mode_set(mode='OBJECT') + + #Create a fake obj export to a string + #Will strip this down further later + fakeFile = StringIO() + blender_xatlas.export_obj_simple.save( + rename_dict=rename_dict, + context=bpy.context, + filepath=fakeFile, + mainUVChoiceType=sharedProperties.mainUVChoiceType, + uvIndex=sharedProperties.mainUVIndex, + uvName=sharedProperties.mainUVName, + use_selection=True, + use_animation=False, + use_mesh_modifiers=True, + use_edges=True, + use_smooth_groups=False, + use_smooth_groups_bitflags=False, + use_normals=True, + use_uvs=True, + use_materials=False, + use_triangles=False, + use_nurbs=False, + use_vertex_groups=False, + use_blen_objects=True, + group_by_object=False, + group_by_material=False, + keep_vertex_order=False, + ) + + #print just for reference + # print(fakeFile.getvalue()) + + #get the path to xatlas + #file_path = os.path.dirname(os.path.abspath(__file__)) + scriptsDir = os.path.join(bpy.utils.user_resource('SCRIPTS'), "addons") + file_path = os.path.join(scriptsDir, "blender_xatlas") + if platform.system() == "Windows": + xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe") + elif platform.system() == "Linux": + xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender") + #need to set permissions for the process on linux + subprocess.Popen( + 'chmod u+x "' + xatlas_path + '"', + shell=True + ) + + #setup the arguments to be passed to xatlas------------------- + arguments_string = "" + for argumentKey in packOptions.__annotations__.keys(): + key_string = str(argumentKey) + if argumentKey is not None: + print(getattr(packOptions,key_string)) + attrib = getattr(packOptions,key_string) + if type(attrib) == bool: + if attrib == True: + arguments_string = arguments_string + " -" + str(argumentKey) + else: + arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) + + for argumentKey in chartOptions.__annotations__.keys(): + if argumentKey is not None: + key_string = str(argumentKey) + print(getattr(chartOptions,key_string)) + attrib = getattr(chartOptions,key_string) + if type(attrib) == bool: + if attrib == True: + arguments_string = arguments_string + " -" + str(argumentKey) + else: + arguments_string = arguments_string + " -" + str(argumentKey) + " " + str(attrib) + + #add pack only option + if sharedProperties.packOnly: + arguments_string = arguments_string + " -packOnly" + + arguments_string = arguments_string + " -atlasLayout" + " " + sharedProperties.atlasLayout + + print(arguments_string) + #END setup the arguments to be passed to xatlas------------------- + + #RUN xatlas process + xatlas_process = subprocess.Popen( + r'"{}"'.format(xatlas_path) + ' ' + arguments_string, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=True + ) + + print(xatlas_path) + + #shove the fake file in stdin + stdin = xatlas_process.stdin + value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly + stdin.write(value) + stdin.flush() + + #Get the output from xatlas + outObj = "" + while True: + output = xatlas_process.stdout.readline() + if not output: + break + outObj = outObj + (output.decode().strip() + "\n") + + #the objects after xatlas processing + # print(outObj) + + + #Setup for reading the output + @dataclass + class uvObject: + obName: string = "" + uvArray: List[float] = field(default_factory=list) + faceArray: List[int] = field(default_factory=list) + + convertedObjects = [] + uvArrayComplete = [] + + + #search through the out put for STARTOBJ + #then start reading the objects + obTest = None + startRead = False + for line in outObj.splitlines(): + + line_split = line.split() + + if not line_split: + continue + + line_start = line_split[0] # we compare with this a _lot_ + # print(line_start) + if line_start == "STARTOBJ": + print("Start reading the objects----------------------------------------") + startRead = True + # obTest = uvObject() + + if startRead: + #if it's a new obj + if line_start == 'o': + #if there is already an object append it + if obTest is not None: + convertedObjects.append(obTest) + + obTest = uvObject() #create new uv object + obTest.obName = line_split[1] + + if obTest is not None: + #the uv coords + if line_start == 'vt': + newUv = [float(line_split[1]),float(line_split[2])] + obTest.uvArray.append(newUv) + uvArrayComplete.append(newUv) + + #the face coords index + #faces are 1 indexed + if line_start == 'f': + #vert/uv/normal + #only need the uvs + newFace = [ + int(line_split[1].split("/")[1]), + int(line_split[2].split("/")[1]), + int(line_split[3].split("/")[1]) + ] + obTest.faceArray.append(newFace) + + #append the final object + convertedObjects.append(obTest) + print(convertedObjects) + + + #apply the output------------------------------------------------------------- + #copy the uvs to the original objects + # objIndex = 0 + print("Applying the UVs----------------------------------------") + # print(convertedObjects) + for importObject in convertedObjects: + bpy.ops.object.select_all(action='DESELECT') + + obTest = importObject + obTest.obName = safe_dict[obTest.obName] #probably shouldn't just replace it + bpy.context.scene.objects[obTest.obName].select_set(True) + context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName] + bpy.ops.object.mode_set(mode = 'OBJECT') + + obj = bpy.context.active_object + me = obj.data + #convert to bmesh to create the new uvs + bm = bmesh.new() + bm.from_mesh(me) + + uv_layer = bm.loops.layers.uv.verify() + + nFaces = len(bm.faces) + #need to ensure lookup table for some reason? + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + + #loop through the faces + for faceIndex in range(nFaces): + faceGroup = obTest.faceArray[faceIndex] + + bm.faces[faceIndex].loops[0][uv_layer].uv = ( + uvArrayComplete[faceGroup[0] - 1][0], + uvArrayComplete[faceGroup[0] - 1][1]) + + bm.faces[faceIndex].loops[1][uv_layer].uv = ( + uvArrayComplete[faceGroup[1] - 1][0], + uvArrayComplete[faceGroup[1] - 1][1]) + + bm.faces[faceIndex].loops[2][uv_layer].uv = ( + uvArrayComplete[faceGroup[2] - 1][0], + uvArrayComplete[faceGroup[2] - 1][1]) + + # objIndex = objIndex + 3 + + # print(objIndex) + #assign the mesh back to the original mesh + bm.to_mesh(me) + #END apply the output------------------------------------------------------------- + + + #Start setting the quads back again------------------------------------------------------------- + if sharedProperties.packOnly: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + for edges in edgeDict: + edgeList = edgeDict[edges] + currentObject = bpy.context.scene.objects[edgeList['object']] + bm = bmesh.new() + bm.from_mesh(currentObject.data) + if hasattr(bm.edges, "ensure_lookup_table"): + bm.edges.ensure_lookup_table() + + #assume that all the triangulated edges come after the original edges + newEdges = [] + for edge in range(len(edgeList['edges']), len(bm.edges)): + newEdge = bm.edges[edge] + newEdge.select = True + newEdges.append(newEdge) + + bmesh.ops.dissolve_edges(bm, edges=newEdges, use_verts=False, use_face_split=False) + bpy.ops.object.mode_set(mode='OBJECT') + bm.to_mesh(currentObject.data) + bm.free() + bpy.ops.object.mode_set(mode='EDIT') + + #End setting the quads back again------------------------------------------------------------- + + #select the original objects that were selected + for objectName in rename_dict: + if objectName[0] in bpy.context.scene.objects: + current_object = bpy.context.scene.objects[objectName[0]] + current_object.select_set(True) + context.view_layer.objects.active = current_object + + bpy.ops.object.mode_set(mode=startingMode) + + print("Finished Xatlas----------------------------------------") + return {'FINISHED'} + +def transfer_assets(copy, source, destination): + for filename in glob.glob(os.path.join(source, '*.*')): + try: + shutil.copy(filename, destination) + except shutil.SameFileError: + pass + +def transfer_load(): + load_folder = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_load_folder)) + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + transfer_assets(True, load_folder, lightmap_folder) diff --git a/blender/arm/live_patch.py b/blender/arm/live_patch.py new file mode 100644 index 0000000000..e653c67f05 --- /dev/null +++ b/blender/arm/live_patch.py @@ -0,0 +1,392 @@ +import os +import shutil +from typing import Any, Type + +import bpy + +import arm.assets +from arm import log, make +from arm.exporter import ArmoryExporter +from arm.logicnode.arm_nodes import ArmLogicTreeNode +import arm.make_state as state +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + arm.assets = arm.reload_module(arm.assets) + arm.exporter = arm.reload_module(arm.exporter) + from arm.exporter import ArmoryExporter + log = arm.reload_module(log) + arm.logicnode.arm_nodes = arm.reload_module(arm.logicnode.arm_nodes) + from arm.logicnode.arm_nodes import ArmLogicTreeNode + make = arm.reload_module(make) + state = arm.reload_module(state) + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + patch_id = 0 + """Current patch id""" + + __running = False + """Whether live patch is currently active""" + +# Any object can act as a message bus owner +msgbus_owner = object() + + +def start(): + """Start the live patch session.""" + log.debug("Live patch session started") + + listen(bpy.types.Object, "location", "obj_location") + listen(bpy.types.Object, "rotation_euler", "obj_rotation") + listen(bpy.types.Object, "scale", "obj_scale") + + # 'energy' is defined in sub classes only, also workaround for + # https://developer.blender.org/T88408 + for light_type in (bpy.types.AreaLight, bpy.types.PointLight, bpy.types.SpotLight, bpy.types.SunLight): + listen(light_type, "color", "light_color") + listen(light_type, "energy", "light_energy") + + global __running + __running = True + + +def stop(): + """Stop the live patch session.""" + global __running, patch_id + if __running: + __running = False + patch_id = 0 + + log.debug("Live patch session stopped") + bpy.msgbus.clear_by_owner(msgbus_owner) + + +def patch_export(): + """Re-export the current scene and update the game accordingly.""" + if not __running or state.proc_build is not None: + return + + arm.assets.invalidate_enabled = False + + with arm.utils.WorkingDir(arm.utils.get_fp()): + asset_path = arm.utils.get_fp_build() + '/compiled/Assets/' + arm.utils.safestr(bpy.context.scene.name) + '.arm' + ArmoryExporter.export_scene(bpy.context, asset_path, scene=bpy.context.scene) + + dir_std_shaders_dst = os.path.join(arm.utils.build_dir(), 'compiled', 'Shaders', 'std') + if not os.path.isdir(dir_std_shaders_dst): + dir_std_shaders_src = os.path.join(arm.utils.get_sdk_path(), 'armory', 'Shaders', 'std') + shutil.copytree(dir_std_shaders_src, dir_std_shaders_dst) + + node_path = arm.utils.get_node_path() + khamake_path = arm.utils.get_khamake_path() + cmd = [ + node_path, khamake_path, 'krom', + '--shaderversion', '330', + '--parallelAssetConversion', '4', + '--to', arm.utils.build_dir() + '/debug', + '--nohaxe', + '--noproject' + ] + + arm.assets.invalidate_enabled = True + state.proc_build = make.run_proc(cmd, patch_done) + + +def patch_done(): + """Signal Iron to reload the running scene after a re-export.""" + js = 'iron.Scene.patch();' + write_patch(js) + state.proc_build = None + + +def write_patch(js: str): + """Write the given javascript code to 'krom.patch'.""" + global patch_id + with open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w', encoding='utf-8') as f: + patch_id += 1 + f.write(str(patch_id) + '\n') + f.write(js) + + +def listen(rna_type: Type[bpy.types.bpy_struct], prop: str, event_id: str): + """Subscribe to '.'. The event_id can be choosen + freely but must match with the id used in send_event(). + """ + bpy.msgbus.subscribe_rna( + key=(rna_type, prop), + owner=msgbus_owner, + args=(event_id, ), + notify=send_event + # options={"PERSISTENT"} + ) + + +def send_event(event_id: str, opt_data: Any = None): + """Send the result of the given event to Krom.""" + if not __running: + return + + if hasattr(bpy.context, 'object') and bpy.context.object is not None: + obj = bpy.context.object.name + + if bpy.context.object.mode == "OBJECT": + if event_id == "obj_location": + vec = bpy.context.object.location + js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.loc.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;' + write_patch(js) + + elif event_id == 'obj_scale': + vec = bpy.context.object.scale + js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.scale.set({vec[0]}, {vec[1]}, {vec[2]}); o.transform.dirty = true;' + write_patch(js) + + elif event_id == 'obj_rotation': + vec = bpy.context.object.rotation_euler.to_quaternion() + js = f'var o = iron.Scene.active.getChild("{obj}"); o.transform.rot.set({vec[1]}, {vec[2]}, {vec[3]}, {vec[0]}); o.transform.dirty = true;' + write_patch(js) + + elif event_id == 'light_color': + light: bpy.types.Light = bpy.context.object.data + vec = light.color + js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.color[0]={vec[0]}; lRaw.color[1]={vec[1]}; lRaw.color[2]={vec[2]};' + write_patch(js) + + elif event_id == 'light_energy': + light: bpy.types.Light = bpy.context.object.data + + # Align strength to Armory, see exporter.export_light() + # TODO: Use exporter.export_light() and simply reload all raw light data in Iron? + strength_fac = 1.0 + if light.type == 'SUN': + strength_fac = 0.325 + elif light.type in ('POINT', 'SPOT', 'AREA'): + strength_fac = 0.01 + + js = f'var lRaw = iron.Scene.active.getLight("{light.name}").data.raw; lRaw.strength={light.energy * strength_fac};' + write_patch(js) + + else: + patch_export() + + if event_id == 'ln_insert_link': + node: ArmLogicTreeNode + link: bpy.types.NodeLink + node, link = opt_data + + # This event is called twice for a connection but we only need + # send it once + if node == link.from_node: + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + + # [1:] is used here because make_logic already uses that for + # node names if arm_debug is used + from_node_name = arm.node_utils.get_export_node_name(node)[1:] + to_node_name = arm.node_utils.get_export_node_name(link.to_node)[1:] + + from_index = arm.node_utils.get_socket_index(node.outputs, link.from_socket) + to_index = arm.node_utils.get_socket_index(link.to_node.inputs, link.to_socket) + + js = f'LivePatch.patchCreateNodeLink("{tree_name}", "{from_node_name}", "{to_node_name}", "{from_index}", "{to_index}");' + write_patch(js) + + elif event_id == 'ln_update_prop': + node: ArmLogicTreeNode + prop_name: str + node, prop_name = opt_data + + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + node_name = arm.node_utils.get_export_node_name(node)[1:] + + value = arm.node_utils.haxe_format_prop_value(node, prop_name) + + if prop_name.endswith('_get'): + # Hack because some nodes use a different Python property + # name than they use in Haxe + prop_name = prop_name[:-4] + + js = f'LivePatch.patchUpdateNodeProp("{tree_name}", "{node_name}", "{prop_name}", {value});' + write_patch(js) + + elif event_id == 'ln_socket_val': + node: ArmLogicTreeNode + socket: bpy.types.NodeSocket + node, socket = opt_data + + socket_index = arm.node_utils.get_socket_index(node.inputs, socket) + + if socket_index != -1: + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + node_name = arm.node_utils.get_export_node_name(node)[1:] + + value = socket.get_default_value() + inp_type = socket.arm_socket_type + + if inp_type in ('VECTOR', 'RGB'): + value = f'new iron.Vec4({arm.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)}, 1.0)' + elif inp_type == 'RGBA': + value = f'new iron.Vec4({arm.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)})' + elif inp_type == 'ROTATION': + value = f'new iron.Quat({arm.node_utils.haxe_format_socket_val(value, array_outer_brackets=False)})' + elif inp_type == 'OBJECT': + value = f'iron.Scene.active.getChild("{value}")' if value != '' else 'null' + else: + value = arm.node_utils.haxe_format_socket_val(value) + + js = f'LivePatch.patchUpdateNodeInputVal("{tree_name}", "{node_name}", {socket_index}, {value});' + write_patch(js) + + elif event_id == 'ln_create': + node: ArmLogicTreeNode = opt_data + + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + node_name = arm.node_utils.get_export_node_name(node)[1:] + node_type = 'armory.logicnode.' + node.bl_idname[2:] + + prop_names = list(arm.node_utils.get_haxe_property_names(node)) + prop_py_names, prop_hx_names = zip(*prop_names) if len(prop_names) > 0 else ([], []) + prop_values = (getattr(node, prop_name) for prop_name in prop_py_names) + prop_datas = arm.node_utils.haxe_format_socket_val(list(zip(prop_hx_names, prop_values))) + + inp_data = [(inp.arm_socket_type, inp.get_default_value()) for inp in node.inputs] + inp_data = arm.node_utils.haxe_format_socket_val(inp_data) + out_data = [(out.arm_socket_type, out.get_default_value()) for out in node.outputs] + out_data = arm.node_utils.haxe_format_socket_val(out_data) + + js = f'LivePatch.patchNodeCreate("{tree_name}", "{node_name}", "{node_type}", {prop_datas}, {inp_data}, {out_data});' + write_patch(js) + + elif event_id == 'ln_delete': + node: ArmLogicTreeNode = opt_data + + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + node_name = arm.node_utils.get_export_node_name(node)[1:] + + js = f'LivePatch.patchNodeDelete("{tree_name}", "{node_name}");' + write_patch(js) + + elif event_id == 'ln_copy': + newnode: ArmLogicTreeNode + node: ArmLogicTreeNode + newnode, node = opt_data + + # Use newnode to get the tree, node has no id_data at this moment + tree_name = arm.node_utils.get_export_tree_name(newnode.get_tree()) + + newnode_name = arm.node_utils.get_export_node_name(newnode)[1:] + node_name = arm.node_utils.get_export_node_name(node)[1:] + + props_list = '[' + ','.join(f'"{p}"' for _, p in arm.node_utils.get_haxe_property_names(node)) + ']' + + inp_data = [(inp.arm_socket_type, inp.get_default_value()) for inp in newnode.inputs] + inp_data = arm.node_utils.haxe_format_socket_val(inp_data) + out_data = [(out.arm_socket_type, out.get_default_value()) for out in newnode.outputs] + out_data = arm.node_utils.haxe_format_socket_val(out_data) + + js = f'LivePatch.patchNodeCopy("{tree_name}", "{node_name}", "{newnode_name}", {props_list}, {inp_data}, {out_data});' + write_patch(js) + + elif event_id == 'ln_update_sockets': + node: ArmLogicTreeNode = opt_data + + tree_name = arm.node_utils.get_export_tree_name(node.get_tree()) + node_name = arm.node_utils.get_export_node_name(node)[1:] + + inp_data = '[' + for idx, inp in enumerate(node.inputs): + inp_data += '{' + # is_linked can be true even if there are no links if the + # user starts dragging a connection away before releasing + # the mouse + if inp.is_linked and len(inp.links) > 0: + inp_data += 'isLinked: true,' + inp_data += f'fromNode: "{arm.node_utils.get_export_node_name(inp.links[0].from_node)[1:]}",' + inp_data += f'fromIndex: {arm.node_utils.get_socket_index(inp.links[0].from_node.outputs, inp.links[0].from_socket)},' + else: + inp_data += 'isLinked: false,' + inp_data += f'socketType: "{inp.arm_socket_type}",' + inp_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(inp.get_default_value())},' + + inp_data += f'toIndex: {idx}' + inp_data += '},' + inp_data += ']' + + out_data = '[' + for idx, out in enumerate(node.outputs): + out_data += '[' + for link in out.links: + out_data += '{' + if out.is_linked: + out_data += 'isLinked: true,' + out_data += f'toNode: "{arm.node_utils.get_export_node_name(link.to_node)[1:]}",' + out_data += f'toIndex: {arm.node_utils.get_socket_index(link.to_node.inputs, link.to_socket)},' + else: + out_data += 'isLinked: false,' + out_data += f'socketType: "{out.arm_socket_type}",' + out_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(out.get_default_value())},' + + out_data += f'fromIndex: {idx}' + out_data += '},' + out_data += '],' + out_data += ']' + + js = f'LivePatch.patchSetNodeLinks("{tree_name}", "{node_name}", {inp_data}, {out_data});' + write_patch(js) + + +def on_operator(operator_id: str): + """As long as bpy.msgbus doesn't listen to changes made by + operators (*), additionally notify the callback manually. + + (*) https://developer.blender.org/T72109 + """ + if not __running: + return + + if operator_id in IGNORE_OPERATORS: + return + + if operator_id == 'TRANSFORM_OT_translate': + send_event('obj_location') + elif operator_id in ('TRANSFORM_OT_rotate', 'TRANSFORM_OT_trackball'): + send_event('obj_rotation') + elif operator_id == 'TRANSFORM_OT_resize': + send_event('obj_scale') + + # Rebuild + else: + patch_export() + + +# Don't re-export the scene for the following operators +IGNORE_OPERATORS = ( + 'ARM_OT_node_add_input', + 'ARM_OT_node_add_input_output', + 'ARM_OT_node_add_input_value', + 'ARM_OT_node_add_output', + 'ARM_OT_node_call_func', + 'ARM_OT_node_remove_input', + 'ARM_OT_node_remove_input_output', + 'ARM_OT_node_remove_input_value', + 'ARM_OT_node_remove_output', + 'ARM_OT_node_search', + + 'NODE_OT_delete', + 'NODE_OT_duplicate_move', + 'NODE_OT_hide_toggle', + 'NODE_OT_link', + 'NODE_OT_move_detach_links', + 'NODE_OT_select', + 'NODE_OT_translate_attach', + 'NODE_OT_translate_attach_remove_on_cancel', + + 'OBJECT_OT_editmode_toggle', + 'OUTLINER_OT_item_activate', + 'UI_OT_button_string_clear', + 'UI_OT_eyedropper_id', + 'VIEW3D_OT_select', + 'VIEW3D_OT_select_box', +) diff --git a/blender/arm/log.py b/blender/arm/log.py new file mode 100644 index 0000000000..96956a285a --- /dev/null +++ b/blender/arm/log.py @@ -0,0 +1,92 @@ +""" +CLI output. +""" + +import platform +import subprocess +import sys +import bpy + +DEBUG = 36 +INFO = 37 +WARN = 35 +ERROR = 31 + +if platform.system() == "Windows": + HAS_COLOR_SUPPORT = platform.release() == "10" + + if HAS_COLOR_SUPPORT: + # Enable ANSI codes. Otherwise, the ANSI sequences might not be + # evaluated correctly for the first colored print statement. + import ctypes + kernel32 = ctypes.windll.kernel32 + + # -11: stdout + handle_out = kernel32.GetStdHandle(-11) + + console_mode = ctypes.c_long() + kernel32.GetConsoleMode(handle_out, ctypes.byref(console_mode)) + + # 0b100: ENABLE_VIRTUAL_TERMINAL_PROCESSING, enables ANSI codes + # see https://docs.microsoft.com/en-us/windows/console/setconsolemode + console_mode.value |= 0b100 + kernel32.SetConsoleMode(handle_out, console_mode) +else: + HAS_COLOR_SUPPORT = True + +info_text = '' +num_warnings = 0 +num_errors = 0 + +def clear(clear_warnings=False, clear_errors=False): + global info_text, num_warnings, num_errors + info_text = '' + if clear_warnings: + num_warnings = 0 + if clear_errors: + num_errors = 0 + +def format_text(text): + return (text[:80] + '..') if len(text) > 80 else text # Limit str size + +def log(text, color=None): + print(colorize(text, color)) + +def debug(text): + print(colorize(text, DEBUG)) + +def info(text): + global info_text + print(colorize(text, INFO)) + info_text = format_text(text) + +def print_warn(text): + print(colorize('WARNING: ' + text, WARN)) + +def warn(text): + global num_warnings + num_warnings += 1 + print_warn(text) + +def error(text): + global num_errors + num_errors += 1 + print(colorize('ERROR: ' + text, ERROR), file=sys.stderr) + +def colorize(text:str, color=None): + if bpy.context.area is not None and bpy.context.area.type == 'CONSOLE': + return text + if HAS_COLOR_SUPPORT and color is not None: + csi = '\033[' + text = csi + str(color) + 'm' + text + csi + '0m' + return text + +def warn_called_process_error(proc: subprocess.CalledProcessError): + out = f'Command {proc.cmd} exited with code {proc.returncode}.' + if proc.output is not None: + out += ( + f'Command output:\n' + f'---------------\n' + f'{proc.output.decode(encoding="utf-8")}' # Output is encoded as bytes by default + ) + warn(out) diff --git a/blender/arm/logicnode/__init__.py b/blender/arm/logicnode/__init__.py new file mode 100644 index 0000000000..9ba4250f9b --- /dev/null +++ b/blender/arm/logicnode/__init__.py @@ -0,0 +1,99 @@ +import importlib +import inspect +import pkgutil +import sys + +import arm +import arm.logicnode.arm_nodes as arm_nodes +from arm.logicnode.arm_props import * +import arm.logicnode.arm_sockets as arm_sockets +from arm.logicnode.replacement import NodeReplacement + +if arm.is_reload(__name__): + arm_nodes = arm.reload_module(arm_nodes) + arm.logicnode.arm_props = arm.reload_module(arm.logicnode.arm_props) + from arm.logicnode.arm_props import * + arm_sockets = arm.reload_module(arm_sockets) + arm.logicnode.replacement = arm.reload_module(arm.logicnode.replacement) + from arm.logicnode.replacement import NodeReplacement + + HAS_RELOADED = True +else: + arm.enable_reload(__name__) + + +def init_categories(): + """Register default node menu categories.""" + arm_nodes.add_category('Logic', icon='OUTLINER', section="basic", + description="Logic nodes are used to control execution flow using branching, loops, gates etc.") + arm_nodes.add_category('Event', icon='INFO', section="basic") + arm_nodes.add_category('Input', icon='GREASEPENCIL', section="basic") + arm_nodes.add_category('Native', icon='MEMORY', section="basic", + description="The Native category contains nodes which interact with the system (Input/Output functionality, etc.) or Haxe.") + + arm_nodes.add_category('Camera', icon='OUTLINER_OB_CAMERA', section="data") + arm_nodes.add_category('Material', icon='MATERIAL', section="data") + arm_nodes.add_category('Light', icon='LIGHT', section="data") + arm_nodes.add_category('Object', icon='OBJECT_DATA', section="data") + arm_nodes.add_category('Scene', icon='SCENE_DATA', section="data") + arm_nodes.add_category('Trait', icon='NODETREE', section="data") + arm_nodes.add_category('Network', icon='WORLD', section="data") + + arm_nodes.add_category('Animation', icon='SEQUENCE', section="motion") + arm_nodes.add_category('Navmesh', icon='UV_VERTEXSEL', section="motion") + arm_nodes.add_category('Transform', icon='TRANSFORM_ORIGINS', section="motion") + arm_nodes.add_category('Physics', icon='PHYSICS', section="motion") + + arm_nodes.add_category('Array', icon='LIGHTPROBE_GRID', section="values") + arm_nodes.add_category('Map', icon='SHORTDISPLAY', section="values") + arm_nodes.add_category('Math', icon='FORCE_HARMONIC', section="values") + arm_nodes.add_category('Random', icon='SEQ_HISTOGRAM', section="values") + arm_nodes.add_category('String', icon='SORTALPHA', section="values") + arm_nodes.add_category('Variable', icon='OPTIONS', section="values") + + arm_nodes.add_category('Draw', icon='GREASEPENCIL', section="graphics") + arm_nodes.add_category('Canvas', icon='RENDERLAYERS', section="graphics", + description="Note: To get the canvas, be sure that the node(s) and the canvas (UI) is attached to the same object.") + arm_nodes.add_category('Postprocess', icon='FREEZE', section="graphics") + arm_nodes.add_category('Renderpath', icon='STICKY_UVS_LOC', section="graphics") + + arm_nodes.add_category('Sound', icon='OUTLINER_OB_SPEAKER', section="sound") + + arm_nodes.add_category('Miscellaneous', icon='RESTRICT_COLOR_ON', section="misc") + arm_nodes.add_category('Layout', icon='SEQ_STRIP_DUPLICATE', section="misc") + + # Make sure that logic node extension packs are displayed at the end + # of the menu by default unless they declare it otherwise + arm_nodes.add_category_section('default') + + +def init_nodes(base_path=__path__, base_package=__package__, subpackages_only=False): + """Calls the `on_register()` method on all logic nodes in a given + `base_package` and all its sub-packages relative to the given + `base_path`, in order to initialize them and to register them to Armory. + + Be aware that calling this function will import all modules in the + given package, so module-level code will be executed. + + If `subpackages_only` is true, modules directly inside the root of + the base package are not searched and imported. + """ + for loader, module_name, is_pkg in pkgutil.walk_packages(base_path, base_package + '.'): + if is_pkg: + # The package must be loaded as well so that the modules from that package can be accessed (see the + # pkgutil.walk_packages documentation for more information on this) + loader.find_module(module_name).load_module(module_name) + + # Only look at modules in sub packages if specified + elif not subpackages_only or module_name.rsplit('.', 1)[0] != base_package: + if 'HAS_RELOADED' not in globals() or module_name not in sys.modules: + _module = importlib.import_module(module_name) + else: + # Reload the module if the SDK was reloaded at least once + _module = importlib.reload(sys.modules[module_name]) + + for name, obj in inspect.getmembers(_module, inspect.isclass): + if name in ("ArmLogicTreeNode", "ArmLogicVariableNodeMixin"): + continue + if issubclass(obj, arm_nodes.ArmLogicTreeNode): + obj.on_register() diff --git a/blender/arm/logicnode/animation/LN_action.py b/blender/arm/logicnode/animation/LN_action.py new file mode 100644 index 0000000000..e791391071 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_action.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class AnimActionNode(ArmLogicTreeNode): + """Stores the given action as a variable.""" + bl_idname = 'LNAnimActionNode' + bl_label = 'Action' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAnimAction', 'Action') + + self.add_output('ArmNodeSocketAnimAction', 'Action', is_var=True) diff --git a/blender/arm/logicnode/animation/LN_blend_action.py b/blender/arm/logicnode/animation/LN_blend_action.py new file mode 100644 index 0000000000..0214e28d5e --- /dev/null +++ b/blender/arm/logicnode/animation/LN_blend_action.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class BlendActionNode(ArmLogicTreeNode): + """Interpolates between the two given actions.""" + bl_idname = 'LNBlendActionNode' + bl_label = 'Blend Action' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketAnimAction', 'Action 1') + self.add_input('ArmNodeSocketAnimAction', 'Action 2') + self.add_input('ArmFloatSocket', 'Factor', default_value = 0.5) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_bone_fk.py b/blender/arm/logicnode/animation/LN_bone_fk.py new file mode 100644 index 0000000000..d994584daf --- /dev/null +++ b/blender/arm/logicnode/animation/LN_bone_fk.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class BoneFKNode(ArmLogicTreeNode): + """Applies forward kinematics in the given object bone.""" + bl_idname = 'LNBoneFKNode' + bl_label = 'Bone FK' + arm_version = 1 + 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('ArmDynamicSocket', 'Transform') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_bone_ik.py b/blender/arm/logicnode/animation/LN_bone_ik.py new file mode 100644 index 0000000000..66e4d32d6a --- /dev/null +++ b/blender/arm/logicnode/animation/LN_bone_ik.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + +class BoneIKNode(ArmLogicTreeNode): + """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 = 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 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_action_state.py b/blender/arm/logicnode/animation/LN_get_action_state.py new file mode 100644 index 0000000000..1b50661336 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_action_state.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class AnimationStateNode(ArmLogicTreeNode): + """Returns the information about the current action of the given object.""" + bl_idname = 'LNAnimationStateNode' + bl_label = 'Get Action State' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmStringSocket', 'Action') + self.add_output('ArmIntSocket', 'Frame') + self.add_output('ArmBoolSocket', 'Is Paused') 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 0000000000..21afb1eef1 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_bone_fk_ik_only.py @@ -0,0 +1,13 @@ +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 arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Bone') + self.add_output('ArmBoolSocket', 'FK or IK only') 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 0000000000..1ce69e7a7a --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_bone_transform.py @@ -0,0 +1,13 @@ +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 arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Bone') + self.add_output('ArmDynamicSocket', 'Transform') diff --git a/blender/arm/logicnode/animation/LN_get_tilesheet_state.py b/blender/arm/logicnode/animation/LN_get_tilesheet_state.py new file mode 100644 index 0000000000..09adee812e --- /dev/null +++ b/blender/arm/logicnode/animation/LN_get_tilesheet_state.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetTilesheetStateNode(ArmLogicTreeNode): + """Returns the information about the current tilesheet of the given object.""" + bl_idname = 'LNGetTilesheetStateNode' + bl_label = 'Get Tilesheet State' + arm_version = 1 + arm_section = 'tilesheet' + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmStringSocket', 'Name') + self.add_output('ArmIntSocket', 'Frame') + self.add_output('ArmBoolSocket', 'Is Paused') diff --git a/blender/arm/logicnode/animation/LN_on_action_marker.py b/blender/arm/logicnode/animation/LN_on_action_marker.py new file mode 100644 index 0000000000..07c4e69031 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_on_action_marker.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class OnActionMarkerNode(ArmLogicTreeNode): + """Activates the output when the object action reaches the action marker.""" + bl_idname = 'LNOnActionMarkerNode' + bl_label = 'On Action Marker' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Marker') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_play_action_from.py b/blender/arm/logicnode/animation/LN_play_action_from.py new file mode 100644 index 0000000000..b9405f72ac --- /dev/null +++ b/blender/arm/logicnode/animation/LN_play_action_from.py @@ -0,0 +1,51 @@ +from arm.logicnode.arm_nodes import * + + +class PlayActionFromNode(ArmLogicTreeNode): + """ + Plays animation action, that starts from given frame, and ends at given frame. + + @input In: Activates the node logic. + @input Object: States object/armature to run the animation action on. + @input Action: States animation action to be played. + @input Start Frame: Sets frame the animation should start at. + @input End Frame: Sets frame the animation should end at. HINT: Set to "-1" if you want the total frames length of the animation. + @input Blend: Sets rate to blend multiple animations together. + @input Speed: Sets rate the animation plays at. + @input Loop: Sets whether the animation should rewind itself after finishing. + + @output Out: Executes whenever the node is run. + @output Done: Executes whenever the played animation is finished. (Only triggers if looping is false.) + """ + bl_idname = 'LNPlayActionFromNode' + bl_label = 'Play Action From' + arm_version = 3 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketAnimAction', 'Action') + self.add_input('ArmIntSocket', 'Start Frame') + self.add_input('ArmIntSocket', 'End Frame') + self.add_input('ArmFloatSocket', 'Blend', default_value = 0.25) + self.add_input('ArmFloatSocket', 'Speed', default_value = 1.0) + self.add_input('ArmBoolSocket', 'Loop', default_value = False) + self.add_input('ArmBoolSocket', 'Reverse', default_value = False) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Done') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version in (0, 1): + return NodeReplacement( + 'LNPlayActionFromNode', self.arm_version, 'LNPlayActionFromNode', 2, + in_socket_mapping={0:0, 1:1, 2:2, 3:3, 4:4}, out_socket_mapping={0:0, 1:1} + ) + + if self.arm_version == 2: + return NodeReplacement( + 'LNPlayActionFromNode', self.arm_version, 'LNPlayActionFromNode', 3, + in_socket_mapping={0:0, 1:1, 2:2, 3:3, 4:5, 5:6, 6:7}, out_socket_mapping={0:0, 1:1} + ) + + raise LookupError() diff --git a/blender/arm/logicnode/animation/LN_play_tilesheet.py b/blender/arm/logicnode/animation/LN_play_tilesheet.py new file mode 100644 index 0000000000..6e008f9eb9 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_play_tilesheet.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class PlayTilesheetNode(ArmLogicTreeNode): + """Plays the given tilesheet action.""" + bl_idname = 'LNPlayTilesheetNode' + bl_label = 'Play Tilesheet' + arm_version = 1 + arm_section = 'tilesheet' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Name') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Done') diff --git a/blender/arm/logicnode/animation/LN_remove_parent_bone.py b/blender/arm/logicnode/animation/LN_remove_parent_bone.py new file mode 100644 index 0000000000..f869ed64b4 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_remove_parent_bone.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class RemoveParentBoneNode(ArmLogicTreeNode): + """Removes the given object parent to the given bone.""" + bl_idname = 'LNRemoveParentBoneNode' + bl_label = 'Remove Parent Bone' + arm_version = 1 + arm_section = 'armature' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketObject', 'Parent') + self.add_input('ArmStringSocket', 'Bone', default_value='Bone') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_set_action_paused.py b/blender/arm/logicnode/animation/LN_set_action_paused.py new file mode 100644 index 0000000000..6c99495985 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_action_paused.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetActionPausedNode(ArmLogicTreeNode): + """Sets the action paused state of the given object.""" + bl_idname = 'LNSetActionPausedNode' + bl_label = 'Set Action Paused' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Paused') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_set_action_speed.py b/blender/arm/logicnode/animation/LN_set_action_speed.py new file mode 100644 index 0000000000..e90cec6746 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_action_speed.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetActionSpeedNode(ArmLogicTreeNode): + """Sets the current action playback speed of the given object.""" + bl_idname = 'LNSetActionSpeedNode' + bl_label = 'Set Action Speed' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmFloatSocket', 'Speed', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') 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 0000000000..8ed6ce45cc --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_bone_fk_ik_only.py @@ -0,0 +1,16 @@ +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 arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Bone') + self.add_input('ArmBoolSocket', 'FK or IK only') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_set_parent_bone.py b/blender/arm/logicnode/animation/LN_set_parent_bone.py new file mode 100644 index 0000000000..e7279db49c --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_parent_bone.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class SetParentBoneNode(ArmLogicTreeNode): + """Sets the given object parent to the given bone.""" + bl_idname = 'LNSetParentBoneNode' + bl_label = 'Set Parent Bone' + arm_version = 1 + arm_section = 'armature' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketObject', 'Parent') + self.add_input('ArmStringSocket', 'Bone', default_value='Bone') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_set_particle_speed.py b/blender/arm/logicnode/animation/LN_set_particle_speed.py new file mode 100644 index 0000000000..2fe92ee403 --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_particle_speed.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetParticleSpeedNode(ArmLogicTreeNode): + """Sets the speed of the given particle source.""" + bl_idname = 'LNSetParticleSpeedNode' + bl_label = 'Set Particle Speed' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmFloatSocket', 'Speed', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/LN_set_tilesheet_paused.py b/blender/arm/logicnode/animation/LN_set_tilesheet_paused.py new file mode 100644 index 0000000000..393a259f7d --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_tilesheet_paused.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetTilesheetPausedNode(ArmLogicTreeNode): + """Sets the tilesheet paused state of the given object.""" + bl_idname = 'LNSetTilesheetPausedNode' + bl_label = 'Set Tilesheet Paused' + arm_section = 'tilesheet' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Paused') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/animation/__init__.py b/blender/arm/logicnode/animation/__init__.py new file mode 100644 index 0000000000..194503ba66 --- /dev/null +++ b/blender/arm/logicnode/animation/__init__.py @@ -0,0 +1,5 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Animation') +add_node_section(name='tilesheet', category='Animation') +add_node_section(name='armature', category='Animation') diff --git a/blender/arm/logicnode/arm_node_group.py b/blender/arm/logicnode/arm_node_group.py new file mode 100644 index 0000000000..0e8e27ddc2 --- /dev/null +++ b/blender/arm/logicnode/arm_node_group.py @@ -0,0 +1,565 @@ +# Some parts of this code is reused from project Sverchok. +# https://github.com/nortikin/sverchok/blob/master/core/node_group.py +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE +from functools import reduce +from typing import List, Set, Dict + +import bpy +from bpy.props import * +import bpy.types +from mathutils import Vector + +import arm +import arm.logicnode.arm_nodes as arm_nodes +from arm.logicnode.arm_nodes import ArmLogicTreeNode +import arm.utils +import arm.props_ui + +if arm.is_reload(__name__): + arm_nodes = arm.reload_module(arm_nodes) + from arm.logicnode.arm_nodes import ArmLogicTreeNode + arm.utils = arm.reload_module(arm.utils) + arm.props_ui = arm.reload_module(arm.props_ui) +else: + arm.enable_reload(__name__) + +array_nodes = arm.logicnode.arm_nodes.array_nodes + + +class ArmGroupTree(bpy.types.NodeTree): + """Separate tree class for subtrees""" + bl_idname = 'ArmGroupTree' + bl_icon = 'NODETREE' + bl_label = 'Group Tree' + + # should be updated by "Go to edit group tree" operator + group_node_name: bpy.props.StringProperty(options={'SKIP_SAVE'}) + + @classmethod + def poll(cls, context): + return False # only for internal usage + + def upstream_trees(self) -> List['ArmGroupTree']: + """ + Returns all the tree subtrees (in case if there are group nodes) + and subtrees of subtrees and so on + The method can help predict if linking new subtree can lead to cyclic linking + """ + next_group_nodes = [node for node in self.nodes if node.bl_idname == 'LNCallGroupNode'] + trees = [self] + safe_counter = 0 + while next_group_nodes: + next_node = next_group_nodes.pop() + if next_node.group_tree: + trees.append(next_node.group_tree) + next_group_nodes.extend([ + node for node in next_node.group_tree.nodes if node.bl_idname == 'LNCallGroupNode']) + safe_counter += 1 + + if safe_counter > 1000: + raise RecursionError(f'Looks like group tree "{self}" has links to itself from other groups') + return trees + + def can_be_linked(self): + """Try to avoid creating loops of group trees with each other""" + # upstream trees of tested treed should nad share trees with downstream trees of current tree + tested_tree_upstream_trees = {t.name for t in self.upstream_trees()} + current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path} + shared_trees = tested_tree_upstream_trees & current_tree_downstream_trees + return not shared_trees + + def update(self): + pass + + +class ArmEditGroupTree(bpy.types.Operator): + """Go into subtree to edit""" + bl_idname = 'arm.edit_group_tree' + bl_label = 'Edit Group Tree' + node_index: StringProperty(name='Node Index', default='') + + def custom_poll(self, context): + if not self.node_index == '': + return True + if context.space_data.type == 'NODE_EDITOR': + if context.active_node and hasattr(context.active_node, 'group_tree'): + if context.active_node.group_tree is not None: + return True + return False + + def execute(self, context): + if self.custom_poll(context): + global array_nodes + if not self.node_index == '': + group_node = array_nodes[self.node_index] + else: + group_node = context.active_node + sub_tree: ArmLogicTree = group_node.group_tree + context.space_data.path.append(sub_tree, node=group_node) + sub_tree.group_node_name = group_node.name + self.node_index = '' + return {'FINISHED'} + return {'CANCELLED'} + + +class ArmCopyGroupTree(bpy.types.Operator): + """Create a copy of this group tree and use it""" + bl_idname = 'arm.copy_group_tree' + bl_label = 'Copy Group Tree' + node_index: StringProperty(name='Node Index', default='') + + def execute(self, context): + global array_nodes + group_node = array_nodes[self.node_index] + group_tree = group_node.group_tree + [setattr(n, 'copy_override', True) for n in group_tree.nodes if n.bl_idname in {'LNGroupInputsNode', 'LNGroupOutputsNode'}] + new_group_tree = group_node.group_tree.copy() + [setattr(n, 'copy_override', False) for n in group_tree.nodes if n.bl_idname in {'LNGroupInputsNode', 'LNGroupOutputsNode'}] + group_node.group_tree = new_group_tree + return {'FINISHED'} + + +class ArmUnlinkGroupTree(bpy.types.Operator): + """Unlink node-group (Shift + Click to set users to zero, data will then not be saved)""" + bl_idname = 'arm.unlink_group_tree' + bl_label = 'Unlink Group Tree' + node_index: StringProperty(name='Node Index', default='') + + def invoke(self, context, event): + self.clear = False + if event.shift: + self.clear = True + self.execute(context) + return {'FINISHED'} + + def execute(self, context): + global array_nodes + group_node = array_nodes[self.node_index] + group_tree = group_node.group_tree + group_node.group_tree = None + if self.clear: + group_tree.user_clear() + return {'FINISHED'} + + +class ArmSearchGroupTree(bpy.types.Operator): + """Browse group trees to be linked""" + bl_idname = 'arm.search_group_tree' + bl_label = 'Search Group Tree' + bl_property = 'tree_name' + node_index: StringProperty(name='Node Index', default='') + + def available_trees(self, context): + linkable_trees = filter(lambda t: hasattr(t, 'can_be_linked') and t.can_be_linked(), bpy.data.node_groups) + return [(t.name, ('0 ' if t.users == 0 else 'F ' if t.use_fake_user else '') + t.name, '') for t in linkable_trees] + + tree_name: bpy.props.EnumProperty(items=available_trees) + + def execute(self, context): + global array_nodes + tree_to_link = bpy.data.node_groups[self.tree_name] + group_node = array_nodes[self.node_index] + group_node.group_tree = tree_to_link + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {'FINISHED'} + + +class ArmAddGroupTree(bpy.types.Operator): + """Create empty subtree for group node""" + bl_idname = "arm.add_group_tree" + bl_label = "Add Group Tree" + node_index: StringProperty(name='Node Index', default='') + + @classmethod + def poll(cls, context): + path = getattr(context.space_data, 'path', []) + if len(path): + if path[-1].node_tree.bl_idname in {'ArmLogicTreeType', 'ArmGroupTree'}: + return True + return False + + def execute(self, context): + """Link new subtree to group node, create input and output nodes in subtree and go to edit one""" + global array_nodes + sub_tree = bpy.data.node_groups.new('Armory group', 'ArmGroupTree') # creating subtree + sub_tree.use_fake_user = True + group_node = array_nodes[self.node_index] + group_node.group_tree = sub_tree # link subtree to group node + sub_tree.nodes.new('LNGroupInputsNode').location = (-250, 0) # create node for putting data into subtree + sub_tree.nodes.new('LNGroupOutputsNode').location = (250, 0) # create node for getting data from subtree + context.space_data.path.append(sub_tree, node=group_node) + sub_tree.group_node_name = group_node.name + return {'FINISHED'} + + +class ArmAddGroupTreeFromSelected(bpy.types.Operator): + """Select nodes group node and placing them into subtree""" + bl_idname = "arm.add_group_tree_from_selected" + bl_label = "Create Group Tree from Selected" + + @classmethod + def poll(cls, context): + path = getattr(context.space_data, 'path', []) + if len(path): + if path[-1].node_tree.bl_idname in {'ArmLogicTreeType', 'ArmGroupTree'}: + return bool(cls.filter_selected_nodes(path[-1].node_tree)) + return False + + def execute(self, context): + """ + Add group tree from selected: + 01. Deselect group Input and Output nodes + 02. Copy nodes into clipboard + 03. Create group tree and move into one + 04. Past nodes from clipboard + 05. Move nodes into tree center + 06. Add group "input" and "output" outside of bounding box of the nodes + 07. TODO: Connect "input" and "output" sockets with group nodes + 08. Add Group tree node in center of selected node in initial tree + 09. TODO: Link the node with appropriate sockets + 10. Cleaning + """ + base_tree = context.space_data.path[-1].node_tree + sub_tree: ArmGroupTree = bpy.data.node_groups.new('Armory group', 'ArmGroupTree') + + # deselect group nodes if selected + [setattr(n, 'select', False) for n in base_tree.nodes if n.select and n.bl_idname in {'LNGroupInputsNode', 'LNGroupOutputsNode'}] + + # Frames can't be just copied because they do not have absolute location, but they can be recreated + frame_names = {n.name for n in base_tree.nodes if n.select and n.bl_idname == 'NodeFrame'} + [setattr(n, 'select', False) for n in base_tree.nodes if n.bl_idname == 'NodeFrame'] + + # copy and past nodes into group tree + bpy.ops.node.clipboard_copy() + context.space_data.path.append(sub_tree) + bpy.ops.node.clipboard_paste() + context.space_data.path.pop() # will enter later via operator + + # move nodes in tree center + sub_tree_nodes = self.filter_selected_nodes(sub_tree) + center = reduce(lambda v1, v2: v1 + v2, [n.location for n in sub_tree_nodes]) / len(sub_tree_nodes) + [setattr(n, 'location', n.location - center) for n in sub_tree_nodes] + + # recreate frames + node_name_mapping = {n.name: n.name for n in sub_tree.nodes} # all nodes have the same name as in base tree + self.recreate_frames(base_tree, sub_tree, frame_names, node_name_mapping) + + # add group input and output nodes + min_x = min(n.location[0] for n in sub_tree_nodes) + max_x = max(n.location[0] for n in sub_tree_nodes) + input_node = sub_tree.nodes.new('LNGroupInputsNode') + input_node.location = (min_x - 250, 0) + output_node = sub_tree.nodes.new('LNGroupOutputsNode') + output_node.location = (max_x + 250, 0) + # add group tree node + initial_nodes = self.filter_selected_nodes(base_tree) + center = reduce(lambda v1, v2: v1 + v2, + [Vector(ArmLogicTreeNode.absolute_location(n)) for n in initial_nodes]) / len(initial_nodes) + group_node = base_tree.nodes.new('LNCallGroupNode') + group_node.select = False + group_node.group_tree = sub_tree + group_node.location = center + sub_tree.group_node_name = group_node.name + + # delete selected nodes and copied frames without children + [base_tree.nodes.remove(n) for n in self.filter_selected_nodes(base_tree)] + with_children_frames = {n.parent.name for n in base_tree.nodes if n.parent} + [base_tree.nodes.remove(n) for n in base_tree.nodes if n.name in frame_names and n.name not in with_children_frames] + + # enter the group tree + bpy.ops.arm.edit_group_tree(node_index=group_node.get_id_str()) + + return {'FINISHED'} + + @staticmethod + def filter_selected_nodes(tree) -> list: + """Avoiding selecting nodes which should not be copied into subtree""" + return [n for n in tree.nodes if n.select and n.bl_idname not in {'LNGroupInputsNode', 'LNGroupOutputsNode'}] + + @staticmethod + def recreate_frames(from_tree: bpy.types.NodeTree, + to_tree: bpy.types.NodeTree, + frame_names: Set[str], + from_to_node_names: Dict[str, str]): + """ + Copy frames from one tree to another + from_to_node_names - mapping of node names between two trees + """ + new_frame_names = {n: to_tree.nodes.new('NodeFrame').name for n in frame_names} + frame_attributes = ['label', 'use_custom_color', 'color', 'label_size', 'text'] + for frame_name in frame_names: + old_frame = from_tree.nodes[frame_name] + new_frame = to_tree.nodes[new_frame_names[frame_name]] + for attr in frame_attributes: + setattr(new_frame, attr, getattr(old_frame, attr)) + for from_node in from_tree.nodes: + if from_node.name not in from_to_node_names: + continue + if from_node.parent and from_node.parent.name in new_frame_names: + if from_node.bl_idname == 'NodeFrame': + to_node = to_tree.nodes[new_frame_names[from_node.name]] + else: + to_node = to_tree.nodes[from_to_node_names[from_node.name]] + to_node.parent = to_tree.nodes[new_frame_names[from_node.parent.name]] + + +class TreeVarNameConflictItem(bpy.types.PropertyGroup): + """Represents two conflicting tree variables with the same name""" + name: StringProperty( + description='The name of the conflicting tree variables' + ) + action: EnumProperty( + name='Conflict Resolution Action', + description='How to resolve the tree variable conflict', + default='rename', + items=[ + ('rename', 'Rename', 'Automatically rename the group\'s tree variable'), + ('merge', 'Merge', 'Merge the conflicting tree variables'), + ] + ) + needs_rename: BoolProperty( + description='If true, the conflict needs to be resolved by renaming' + ) + + +class ArmUngroupGroupTree(bpy.types.Operator): + """Put sub nodes into current layout and delete current group node""" + bl_idname = 'arm.ungroup_group_tree' + bl_label = "Ungroup Group Tree" + bl_options = {'UNDO'} # Required to "un-rename" node's arm_logic_id in case of tree variable conflicts + + conflicts: CollectionProperty(type=TreeVarNameConflictItem) + + @classmethod + def poll(cls, context): + if context.space_data.type == 'NODE_EDITOR': + if context.active_node and hasattr(context.active_node, 'group_tree'): + if context.active_node.group_tree is not None: + return True + return False + + def invoke(self, context, event): + group_node = context.active_node + group_tree = group_node.group_tree + dest_tree = group_node.get_tree() + + # name -> type + group_tree_variables = {} + dest_tree_variables = {} + + for var in group_tree.arm_treevariableslist: + group_tree_variables[var.name] = var.node_type + for var in dest_tree.arm_treevariableslist: + dest_tree_variables[var.name] = var.node_type + + # Check for conflicting tree variables + self.conflicts.clear() # Might still contain values from previous invocation + conflicting_var_names = group_tree_variables.keys() & dest_tree_variables.keys() + user_can_choose = False + for conflicting_var_name in conflicting_var_names: + conflict_item = self.conflicts.add() + conflict_item.name = conflicting_var_name + + # Tree variable types differ, cannot merge + conflict_item.needs_rename = group_tree_variables[conflicting_var_name] != dest_tree_variables[conflicting_var_name] + user_can_choose |= not conflict_item.needs_rename + + # If there are no conflicts or all conflicts _must_ be resolved + # via renaming there's no reason to ask the user + if user_can_choose: + wm = context.window_manager + return wm.invoke_props_dialog(self, width=400) + + return self.execute(context) + + def draw(self, context): + layout = self.layout + + arm.props_ui.draw_multiline_with_icon( + layout, layout_width_px=400, + icon='ERROR', + text=( + 'The group\'s logic tree contains tree variables whose names' + ' are identical to tree variable names in the enclosing tree.' + ) + ) + layout.label(icon='BLANK1', text='Please choose how to resolve the naming conflicts (press ESC to cancel):') + layout.separator() + + conflict_item: TreeVarNameConflictItem + for conflict_item in self.conflicts: + split = layout.split(factor=0.6) + split.alignment = 'RIGHT' + split.label(text=conflict_item.name) + + if conflict_item.needs_rename: + row = split.row() + row.label(text="Needs rename") + else: + row = split.row() + row.prop(conflict_item, "action", expand=True) + + layout.separator() # Add space above Blender's "OK" button + + def execute(self, context): + """Similar to AddGroupTreeFromSelected operator but in backward direction (from subtree to tree)""" + + # go to subtree, select all except input and output groups and mark nodes to be copied + group_node = context.active_node + sub_tree = group_node.group_tree + + if len(self.conflicts) > 0: + self._resolve_conflicts(sub_tree, group_node.get_tree()) + + bpy.ops.arm.edit_group_tree(node_index=group_node.get_id_str()) + [setattr(n, 'select', False) for n in sub_tree.nodes] + group_nodes_filter = filter(lambda n: n.bl_idname not in {'LNGroupInputsNode', 'LNGroupOutputsNode'}, sub_tree.nodes) + for node in group_nodes_filter: + node.select = True + node['sub_node_name'] = node.name # this will be copied within the nodes + + # the attribute should be empty in destination tree + tree = context.space_data.path[-2].node_tree + for node in tree.nodes: + if 'sub_node_name' in node: + del node['sub_node_name'] + + # Frames can't be just copied because they do not have absolute location, but they can be recreated + frame_names = {n.name for n in sub_tree.nodes if n.select and n.bl_idname == 'NodeFrame'} + [setattr(n, 'select', False) for n in sub_tree.nodes if n.bl_idname == 'NodeFrame'] + + if any(n for n in sub_tree.nodes if n.select): # if no selection copy operator will raise error + # copy and past nodes into group tree + bpy.ops.node.clipboard_copy() + context.space_data.path.pop() + bpy.ops.node.clipboard_paste() # this will deselect all and select only pasted nodes + + # move nodes in group node center + tree_select_nodes = [n for n in tree.nodes if n.select] + center = reduce(lambda v1, v2: v1 + v2, + [Vector(ArmLogicTreeNode.absolute_location(n)) for n in tree_select_nodes]) / len(tree_select_nodes) + [setattr(n, 'location', n.location - (center - group_node.location)) for n in tree_select_nodes] + + # recreate frames + node_name_mapping = {n['sub_node_name']: n.name for n in tree.nodes if 'sub_node_name' in n} + ArmAddGroupTreeFromSelected.recreate_frames(sub_tree, tree, frame_names, node_name_mapping) + else: + context.space_data.path.pop() # should exit from subtree anywhere + + # delete group node + tree.nodes.remove(group_node) + for node in tree.nodes: + if 'sub_node_name' in node: + del node['sub_node_name'] + + tree.update() + + return {'FINISHED'} + + def _resolve_conflicts(self, group_tree: bpy.types.NodeTree, dest_tree: bpy.types.NodeTree): + rename_conflict_names = {} # old variable name -> new variable name + for conflict_item in self.conflicts: + if conflict_item.needs_rename or conflict_item.action == 'rename': + # Initialize as empty, will be set further below + rename_conflict_names[conflict_item.name] = '' + + for var_item in group_tree.arm_treevariableslist: + if var_item.name in rename_conflict_names: + # Create a renamed variable in the destination tree and ensure + # its name doesn't conflict with either tree + new_name = group_tree.name + '.' + var_item.name + + dest_var = dest_tree.arm_treevariableslist.add() + dest_varname = arm.utils.unique_name_in_lists( + item_lists=[group_tree.arm_treevariableslist, dest_tree.arm_treevariableslist], + name_attr='name', wanted_name=new_name, ignore_item=dest_var + ) + dest_var['_name'] = dest_varname + rename_conflict_names[var_item.name] = dest_varname + dest_var.node_type = var_item.node_type + dest_var.color = var_item.color + + # Update the logic ids so that copying the nodes to the new tree + # pastes them with references to the newly created dest_var + for node in group_tree.nodes: + node.arm_logic_id = rename_conflict_names.get(node.arm_logic_id, node.arm_logic_id) + + +class ArmAddCallGroupNode(bpy.types.Operator): + """Create A Call Group Node""" + bl_idname = 'arm.add_call_group_node' + bl_label = "Add 'Call Node Group' Node" + + node_ref = None + + @classmethod + def poll(cls, context): + if context.space_data.type == 'NODE_EDITOR': + return context.space_data.edit_tree and context.space_data.tree_type == 'ArmLogicTreeType' + return False + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + self.execute(context) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'MOUSEMOVE': + self.node_ref.location = context.space_data.cursor_location + elif event.type == 'LEFTMOUSE': # Confirm + return {'FINISHED'} + return {'RUNNING_MODAL'} + + def execute(self, context): + tree = context.space_data.path[-1].node_tree + self.node_ref = tree.nodes.new('LNCallGroupNode') + self.node_ref.location = context.space_data.cursor_location + return {'FINISHED'} + + +class ARM_PT_LogicGroupPanel(bpy.types.Panel): + bl_label = 'Armory Logic Group' + bl_idname = 'ARM_PT_LogicGroupPanel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Armory' + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + def has_active_node(self, context): + if context.active_node and hasattr(context.active_node, 'group_tree'): + if context.active_node.group_tree is not None: + return True + return False + + def draw(self, context): + layout = self.layout + layout.operator('arm.add_call_group_node', icon='ADD') + layout.operator('arm.add_group_tree_from_selected', icon='NODETREE') + layout.operator('arm.ungroup_group_tree', icon='NODETREE') + row = layout.row() + row.enabled = self.has_active_node(context) + row.operator('arm.edit_group_tree', icon='FULLSCREEN_ENTER', text='Edit Tree') + + +REG_CLASSES = ( + ArmGroupTree, + ArmEditGroupTree, + ArmCopyGroupTree, + ArmUnlinkGroupTree, + ArmSearchGroupTree, + ArmAddGroupTree, + ArmAddGroupTreeFromSelected, + TreeVarNameConflictItem, + ArmUngroupGroupTree, + ArmAddCallGroupNode, + ARM_PT_LogicGroupPanel +) +register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py new file mode 100644 index 0000000000..827694fc5f --- /dev/null +++ b/blender/arm/logicnode/arm_nodes.py @@ -0,0 +1,1029 @@ +from collections import OrderedDict +import itertools +import textwrap +from typing import Any, final, Generator, List, Optional, Type, Union +from typing import OrderedDict as ODict # Prevent naming conflicts + +import bpy.types +from bpy.props import * +from nodeitems_utils import NodeItem +from arm.logicnode.arm_sockets import ArmCustomSocket + +import arm # we cannot import arm.livepatch here or we have a circular import +# Pass custom property types and NodeReplacement forward to individual +# node modules that import arm_nodes +from arm.logicnode.arm_props import * +from arm.logicnode.replacement import NodeReplacement +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + arm.logicnode.arm_props = arm.reload_module(arm.logicnode.arm_props) + from arm.logicnode.arm_props import * + arm.logicnode.replacement = arm.reload_module(arm.logicnode.replacement) + from arm.logicnode.replacement import NodeReplacement + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) + arm.logicnode.arm_sockets = arm.reload_module(arm.logicnode.arm_sockets) + from arm.logicnode.arm_sockets import ArmCustomSocket +else: + arm.enable_reload(__name__) + +# When passed as a category to add_node(), this will use the capitalized +# name of the package of the node as the category to make renaming +# categories easier. +PKG_AS_CATEGORY = "__pkgcat__" + +nodes = [] +category_items: ODict[str, List['ArmNodeCategory']] = OrderedDict() + +array_nodes: dict[str, 'ArmLogicTreeNode'] = dict() + +# See ArmLogicTreeNode.update() +# format: [tree pointer => (num inputs, num input links, num outputs, num output links)] +last_node_state: dict[int, tuple[int, int, int, int]] = {} + + +class ArmLogicTreeNode(bpy.types.Node): + arm_category = PKG_AS_CATEGORY + arm_section = 'default' + arm_is_obsolete = False + + @final + def init(self, context): + # make sure a given node knows the version of the NodeClass from when it was created + if isinstance(type(self).arm_version, int): + self.arm_version = type(self).arm_version + else: + self.arm_version = 1 + + if not hasattr(self, 'arm_init'): + # Show warning for older node packages + arm.log.warn(f'Node {self.bl_idname} has no arm_init function and might not work correctly!') + else: + self.arm_init(context) + + self.clear_tree_cache() + arm.live_patch.send_event('ln_create', self) + + def register_id(self): + """Registers a node ID so that the ID can be used by operators + to target this node (nodes can't be stored in pointer properties). + """ + array_nodes[self.get_id_str()] = self + + def get_id_str(self) -> str: + return str(self.as_pointer()) + + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'ArmLogicTreeType' or 'ArmGroupTree' + + @classmethod + def on_register(cls): + """Don't call this method register() as it will be triggered before Blender registers the class, resulting in + a double registration.""" + add_node(cls, cls.arm_category, cls.arm_section, cls.arm_is_obsolete) + + @classmethod + def on_unregister(cls): + pass + + @classmethod + def absolute_location(cls, node): + """Gets the absolute location of the node including frames and parent nodes.""" + locx, locy = node.location[:] + if node.parent: + locx += node.parent.location.x + locy += node.parent.location.y + return cls.absolute_location(node.parent) + else: + return locx, locy + + def get_tree(self) -> bpy.types.NodeTree: + return self.id_data + + def clear_tree_cache(self): + self.get_tree().arm_cached = False + + def update(self): + """Called if the node was updated in some way, for example + if socket connections change. This callback is not called if + socket values were changed. + """ + def num_connected(sockets): + return sum([socket.is_linked for socket in sockets]) + + # If a link between sockets is removed, there is currently no + # _reliable_ way in the Blender API to check which connection + # was removed (*). + # + # So instead we just check _if_ the number of links or sockets + # has changed (the update function is called before and after + # each link removal). Because we listen for those updates in + # general, we automatically also listen to link creation events, + # which is more stable than using the dedicated callback for + # that (`insert_link()`), because adding links can remove other + # links and we would need to react to that as well. + # + # (*) https://devtalk.blender.org/t/how-to-detect-which-link-was-deleted-by-user-in-node-editor + + self_id = self.as_pointer() + + current_state = (len(self.inputs), num_connected(self.inputs), len(self.outputs), num_connected(self.outputs)) + if self_id not in last_node_state: + # Lazily initialize the last_node_state dict to also store + # state for nodes that already exist in the tree + last_node_state[self_id] = current_state + + if last_node_state[self_id] != current_state: + self.on_socket_state_change() + last_node_state[self_id] = current_state + + # Notify sockets + for socket in itertools.chain(self.inputs, self.outputs): + if isinstance(socket, ArmCustomSocket): + socket.on_node_update() + + self.clear_tree_cache() + + def free(self): + """Called before the node is deleted.""" + self.clear_tree_cache() + arm.live_patch.send_event('ln_delete', self) + + def copy(self, src_node): + """Called upon node duplication or upon pasting a copied node. + `self` holds the copied node and `src_node` a temporal copy of + the original node at the time of copying). + """ + self.clear_tree_cache() + arm.live_patch.send_event('ln_copy', (self, src_node)) + + def on_prop_update(self, context: bpy.types.Context, prop_name: str): + """Called if a property created with a function from the + arm_props module is changed. If the property has a custom update + function, it is called before `on_prop_update()`. + """ + self.clear_tree_cache() + arm.live_patch.send_event('ln_update_prop', (self, prop_name)) + + def on_socket_val_update(self, context: bpy.types.Context, socket: bpy.types.NodeSocket): + self.clear_tree_cache() + arm.live_patch.send_event('ln_socket_val', (self, socket)) + + def on_socket_state_change(self): + """Called if the state (amount, connection state) of the node's + socket changes (see ArmLogicTreeNode.update()) + """ + arm.live_patch.send_event('ln_update_sockets', self) + + def on_logic_id_change(self): + """Called if the node's arm_logic_id value changes.""" + self.clear_tree_cache() + arm.live_patch.patch_export() + + def insert_link(self, link: bpy.types.NodeLink): + """Called on *both* nodes when a link between two nodes is created.""" + # arm.live_patch.send_event('ln_insert_link', (self, link)) + pass + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + # needs to be overridden by individual node classes with arm_version>1 + """(only called if the node's version is inferior to the node class's version) + Help with the node replacement process, by explaining how a node (`self`) should be replaced. + This method can either return a NodeReplacement object (see `nodes_logic.py`), or a brand new node. + + If a new node is returned, then the following needs to be already set: + - the node's links to the other nodes + - the node's properties + - the node inputs's default values + + If more than one node need to be created (for example, if an input needs a type conversion after update), + please return all the nodes in a list. + + please raise a LookupError specifically when the node's version isn't handled by the function. + + note that the lowest 'defined' version should be 1. if the node's version is 0, it means that it has been saved before versioning was a thing. + NODES OF VERSION 1 AND VERSION 0 SHOULD HAVE THE SAME CONTENTS + """ + if self.arm_version == 0 and type(self).arm_version == 1: + # In case someone doesn't implement this function, but the node has version 0 + return NodeReplacement.Identity(self) + else: + raise LookupError(f"the current node class {repr(type(self)):s} does not implement get_replacement_node() even though it has updated") + + def add_input(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Adds a new input socket to the node. + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + socket = self.inputs.new(socket_type, socket_name) + + if default_value is not None: + if isinstance(socket, ArmCustomSocket): + if socket.arm_socket_type != 'NONE': + socket.default_value_raw = default_value + else: + raise ValueError('specified a default value for an input node that doesn\'t accept one') + else: # should not happen anymore? + socket.default_value = default_value + + if is_var and not socket.display_shape.endswith('_DOT'): + socket.display_shape += '_DOT' + + return socket + + def add_output(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Adds a new output socket to the node. + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + socket = self.outputs.new(socket_type, socket_name) + + # FIXME: …a default_value on an output socket? Why is that a thing? + if default_value is not None: + if socket.arm_socket_type != 'NONE': + socket.default_value_raw = default_value + else: + raise ValueError('specified a default value for an input node that doesn\'t accept one') + + if is_var and not socket.display_shape.endswith('_DOT'): + socket.display_shape += '_DOT' + + return socket + + def get_socket_index(self, socket:bpy.types.NodeSocket) -> int: + """Gets the scket index of a socket in this node.""" + + index = 0 + if socket.is_output: + for output in self.outputs: + if output == socket: + return index + index = index + 1 + else: + for input in self.inputs: + if input == socket: + return index + index = index + 1 + return -1 + + def insert_input(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Insert a new input socket to the node at a particular index. + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + + socket = self.add_input(socket_type, socket_name, default_value, is_var) + self.inputs.move(len(self.inputs) - 1, socket_index) + return socket + + def insert_output(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Insert a new output socket to the node at a particular index. + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + + socket = self.add_output(socket_type, socket_name, default_value, is_var) + self.outputs.move(len(self.outputs) - 1, socket_index) + return socket + + def change_input_socket(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Change an input socket type retaining the previous socket links + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + + old_socket = self.inputs[socket_index] + links = old_socket.links + from_sockets = [] + for link in links: + from_sockets.append(link.from_socket) + current_socket = self.insert_input(socket_type, socket_index, socket_name, default_value, is_var) + if default_value is None: + old_socket.copy_defaults(current_socket) + self.inputs.remove(old_socket) + tree = self.get_tree() + for from_socket in from_sockets: + tree.links.new(from_socket, current_socket) + return current_socket + + def change_output_socket(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: + """Change an output socket type retaining the previous socket links + + If `is_var` is true, a dot is placed inside the socket to denote + that this socket can be used for variable access (see + SetVariable node). + """ + + links = self.outputs[socket_index].links + to_sockets = [] + for link in links: + to_sockets.append(link.to_socket) + self.outputs.remove(self.outputs[socket_index]) + current_socket = self.insert_output(socket_type, socket_index, socket_name, default_value, is_var) + tree = self.get_tree() + for to_socket in to_sockets: + tree.links.new(current_socket, to_socket) + return current_socket + +class ArmLogicVariableNodeMixin(ArmLogicTreeNode): + """A mixin class for variable nodes. This class adds functionality + that allows variable nodes to + 1) be identified as such and + 2) to be promoted to nodes that are linked to a tree variable. + + If a variable node is promoted to a tree variable node and the + tree variable does not exist yet, it is created. Each tree variable + only exists as long as there are variable nodes that are linked to + it. A variable node's links to a tree variables can be removed by + calling `make_local()`. If a tree variable node is copied to a + different tree where the variable doesn't exist, it is created. + + Tree variable nodes come in two states: master and replica nodes. + In order to not having to find memory-intensive and complicated ways + for storing every possible variable node data in the tree variable + UI list entries themselves (Blender doesn't support dynamically + typed properties), we store the data in one of the variable nodes, + called the master node. The other nodes are synchronized with the + master node and must implement a routine to copy the data from the + master node. + + The user doesn't need to know about the master/replica concept, the + master node gets chosen automatically and it is made sure that there + can be only one master node, even after copying. + """ + is_master_node: BoolProperty(default=False) + + _text_wrapper = textwrap.TextWrapper() + + def synchronize_from_master(self, master_node: 'ArmLogicVariableNodeMixin'): + """Called if the node should synchronize its data from the passed + master_node. Override this in variable nodes to react to updates + made to the master node. + """ + pass + + def _synchronize_to_replicas(self, master_node: 'ArmLogicVariableNodeMixin'): + for replica_node in self.get_replica_nodes(self.get_tree(), self.arm_logic_id): + replica_node.synchronize_from_master(master_node) + self.clear_tree_cache() + + def make_local(self): + """Demotes this node to a local variable node that is not linked + to any tree variable. + """ + has_replicas = True + if self.is_master_node: + self._synchronize_to_replicas(self) + has_replicas = self.choose_new_master_node(self.get_tree(), self.arm_logic_id) + self.is_master_node = False + + # Remove the tree variable if there are no more nodes that link + # to it + if not has_replicas: + tree = self.get_tree() + for idx, item in enumerate(tree.arm_treevariableslist): + if item.name == self.arm_logic_id: + tree.arm_treevariableslist.remove(idx) + break + + max_index = len(tree.arm_treevariableslist) - 1 + if tree.arm_treevariableslist_index > max_index: + tree.arm_treevariableslist_index = max_index + + self.arm_logic_id = '' + self.clear_tree_cache() + + def free(self): + self.make_local() + super().free() + + def copy(self, src_node: 'ArmLogicVariableNodeMixin'): + # Because the `copy()` callback is actually called upon pasting + # the node, `src_node` is a temporal copy of the copied node + # that retains the state of the node upon copying. This however + # means that we can't reliably use the master state of the + # pasted node because it might have changed in between, also + # `src_node.get_tree()` will return `None`. So if the pasted + # node is linked to a tree var, we simply check if the tree of + # the pasted node has the tree variable and depending on that we + # set `is_master_node`. + + if self.arm_logic_id != '': + target_tree = self.get_tree() + lst = target_tree.arm_treevariableslist + + self.is_master_node = False # Ignore this node in get_master_node below + if self.__class__.get_master_node(target_tree, self.arm_logic_id) is None: + + # copy() is not only called when manually copying/pasting + # nodes, but also when duplicating logic trees. + # In that case, Blender duplicates the arm_treevariableslist + # property, so all tree variables already exist before + # adding a single node to the new tree. In turn, each + # tree variable exists without a master node before + # the first node referencing that variable is copied over + # to the new tree. + # For this reason, despite having no master node, we need + # to check whether the tree variable already exists. + target_tree_has_variable = False + for item in lst: + if item.name == self.arm_logic_id: + target_tree_has_variable = True + break + + if not target_tree_has_variable: + var_item = lst.add() + var_item['_name'] = arm.utils.unique_name_in_lists( + item_lists=[lst], name_attr='name', wanted_name=self.arm_logic_id, ignore_item=var_item + ) + var_item.node_type = self.bl_idname + var_item.color = arm.utils.get_random_color_rgb() + + target_tree.arm_treevariableslist_index = len(lst) - 1 + arm.make_state.redraw_ui = True + + self.is_master_node = True + else: + # Use existing variable + for item in lst: + if item.name == self.arm_logic_id: + self.color = item.color + break + + super().copy(src_node) + + def on_socket_state_change(self): + if self.is_master_node: + self._synchronize_to_replicas(self) + super().on_socket_state_change() + + def on_logic_id_change(self): + tree = self.get_tree() + is_linked = self.arm_logic_id != '' + for inp in self.inputs: + if is_linked: + for link in inp.links: + tree.links.remove(link) + + inp.hide = is_linked + inp.enabled = not is_linked # Hide in sidebar, see Blender's space_node.py + super().on_logic_id_change() + + def on_prop_update(self, context: bpy.types.Context, prop_name: str): + if self.is_master_node: + self._synchronize_to_replicas(self) + super().on_prop_update(context, prop_name) + + def on_socket_val_update(self, context: bpy.types.Context, socket: bpy.types.NodeSocket): + if self.is_master_node: + self._synchronize_to_replicas(self) + super().on_socket_val_update(context, socket) + + def draw_content(self, context, layout): + """Override in variable nodes as replacement for draw_buttons()""" + pass + + @final + def draw_buttons(self, context, layout): + if self.arm_logic_id == '': + self.draw_content(context, layout) + else: + txt_wrapper = self.__class__._text_wrapper + # Roughly estimate how much text fits in the node's width + txt_wrapper.width = self.width / 6 + + msg = f'Value linked to tree variable "{self.arm_logic_id}"' + lines = txt_wrapper.wrap(msg) + + for line in lines: + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.label(text=line) + row.scale_y = 0.4 + + def draw_label(self) -> str: + if self.arm_logic_id == '': + return self.bl_label + else: + return f'TV: {self.arm_logic_id}' + + @classmethod + def synchronize(cls, tree: bpy.types.NodeTree, logic_id: str): + """Synchronizes the value of the master node of the given + `logic_id` to all replica nodes. + """ + master_node = cls.get_master_node(tree, logic_id) + master_node._synchronize_to_replicas(master_node) + + @staticmethod + def choose_new_master_node(tree: bpy.types.NodeTree, logic_id: str) -> bool: + """Choose a new master node from the remaining replica nodes. + + Return `True` if a new master node was found, otherwise return + `False`. + """ + try: + node = next(ArmLogicVariableNodeMixin.get_replica_nodes(tree, logic_id)) + except StopIteration: + return False # No replica node found + + node.is_master_node = True + return True + + @staticmethod + def get_master_node(tree: bpy.types.NodeTree, logic_id: str) -> Optional['ArmLogicVariableNodeMixin']: + for node in tree.nodes: + if node.arm_logic_id == logic_id and isinstance(node, ArmLogicVariableNodeMixin): + if node.is_master_node: + return node + return None + + @staticmethod + def get_replica_nodes(tree: bpy.types.NodeTree, logic_id: str) -> Generator['ArmLogicVariableNodeMixin', None, None]: + """A generator that iterates over all variable nodes for a given + ID that are not the master node. + """ + for node in tree.nodes: + if node.arm_logic_id == logic_id and isinstance(node, ArmLogicVariableNodeMixin): + if not node.is_master_node: + yield node + + +class ArmNodeAddInputButton(bpy.types.Operator): + """Add a new input socket to the node set by node_index.""" + bl_idname = 'arm.node_add_input' + bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} + + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='ArmDynamicSocket') + name_format: StringProperty(name='Name Format', default='Input {0}') + index_name_offset: IntProperty(name='Index Name Offset', default=0) + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + inps = node.inputs + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types) == len(name_formats) + + format_index = (len(inps) + self.index_name_offset) // len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + inp = inps.new(socket_type, name_format.format(str(format_index))) + # Make sure inputs don't show up if the node links to a tree variable + inp.hide = node.arm_logic_id != '' + inp.enabled = node.arm_logic_id == '' + + # Reset to default again for subsequent calls of this operator + self.node_index = '' + self.socket_type = 'ArmDynamicSocket' + self.name_format = 'Input {0}' + self.index_name_offset = 0 + + return{'FINISHED'} + +class ArmNodeAddInputValueButton(bpy.types.Operator): + """Add new input""" + bl_idname = 'arm.node_add_input_value' + bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='ArmDynamicSocket') + + def execute(self, context): + global array_nodes + inps = array_nodes[self.node_index].inputs + inps.new(self.socket_type, 'Value') + return{'FINISHED'} + +class ArmNodeRemoveInputButton(bpy.types.Operator): + """Remove last input""" + bl_idname = 'arm.node_remove_input' + bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of inputs to remove', default=1, min=1) + min_inputs: IntProperty(name='Number of inputs to keep', default=0, min=0) + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + inps = node.inputs + min_inps = self.min_inputs if not hasattr(node, 'min_inputs') else node.min_inputs + if len(inps) >= min_inps + self.count: + for _ in range(self.count): + inps.remove(inps.values()[-1]) + return{'FINISHED'} + +class ArmNodeRemoveInputValueButton(bpy.types.Operator): + """Remove last input""" + bl_idname = 'arm.node_remove_input_value' + bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + target_name: StringProperty(name='Name of socket to remove', default='Value') + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + inps = node.inputs + min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs + if len(inps) > min_inps and inps[-1].name == self.target_name: + inps.remove(inps.values()[-1]) + return{'FINISHED'} + +class ArmNodeAddOutputButton(bpy.types.Operator): + """Add a new output socket to the node set by node_index""" + bl_idname = 'arm.node_add_output' + bl_label = 'Add Output' + bl_options = {'UNDO', 'INTERNAL'} + + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='ArmDynamicSocket') + name_format: StringProperty(name='Name Format', default='Output {0}') + index_name_offset: IntProperty(name='Index Name Offset', default=0) + + def execute(self, context): + global array_nodes + outs = array_nodes[self.node_index].outputs + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types) == len(name_formats) + + format_index = (len(outs) + self.index_name_offset) // len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + outs.new(socket_type, name_format.format(str(format_index))) + + # Reset to default again for subsequent calls of this operator + self.node_index = '' + self.socket_type = 'ArmDynamicSocket' + self.name_format = 'Output {0}' + self.index_name_offset = 0 + + return{'FINISHED'} + +class ArmNodeRemoveOutputButton(bpy.types.Operator): + """Remove last output""" + bl_idname = 'arm.node_remove_output' + bl_label = 'Remove Output' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of outputs to remove', default=1, min=1) + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + outs = node.outputs + min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs + if len(outs) >= min_outs + self.count: + for _ in range(self.count): + outs.remove(outs.values()[-1]) + return{'FINISHED'} + +class ArmNodeAddInputOutputButton(bpy.types.Operator): + """Add new input and output""" + bl_idname = 'arm.node_add_input_output' + bl_label = 'Add Input Output' + bl_options = {'UNDO', 'INTERNAL'} + + node_index: StringProperty(name='Node Index', default='') + in_socket_type: StringProperty(name='In Socket Type', default='ArmDynamicSocket') + out_socket_type: StringProperty(name='Out Socket Type', default='ArmDynamicSocket') + in_name_format: StringProperty(name='In Name Format', default='Input {0}') + out_name_format: StringProperty(name='Out Name Format', default='Output {0}') + in_index_name_offset: IntProperty(name='In Name Offset', default=0) + out_index_name_offset: IntProperty(name='Out Name Offset', default=0) + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + inps = node.inputs + outs = node.outputs + + in_socket_types = self.in_socket_type.split(';') + in_name_formats = self.in_name_format.split(';') + assert len(in_socket_types) == len(in_name_formats) + + out_socket_types = self.out_socket_type.split(';') + out_name_formats = self.out_name_format.split(';') + assert len(out_socket_types) == len(out_name_formats) + + in_format_index = (len(outs) + self.in_index_name_offset) // len(in_socket_types) + out_format_index = (len(outs) + self.out_index_name_offset) // len(out_socket_types) + for socket_type, name_format in zip(in_socket_types, in_name_formats): + inps.new(socket_type, name_format.format(str(in_format_index))) + for socket_type, name_format in zip(out_socket_types, out_name_formats): + outs.new(socket_type, name_format.format(str(out_format_index))) + + # Reset to default again for subsequent calls of this operator + self.node_index = '' + self.in_socket_type = 'ArmDynamicSocket' + self.out_socket_type = 'ArmDynamicSocket' + self.in_name_format = 'Input {0}' + self.out_name_format = 'Output {0}' + self.in_index_name_offset = 0 + self.out_index_name_offset = 0 + + return{'FINISHED'} + +class ArmNodeRemoveInputOutputButton(bpy.types.Operator): + """Remove last input and output""" + bl_idname = 'arm.node_remove_input_output' + bl_label = 'Remove Input Output' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + in_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + out_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + inps = node.inputs + outs = node.outputs + min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs + min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs + if len(inps) >= min_inps + self.in_count: + for _ in range(self.in_count): + inps.remove(inps.values()[-1]) + if len(outs) >= min_outs + self.out_count: + for _ in range(self.out_count): + outs.remove(outs.values()[-1]) + 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..." + bl_options = {"REGISTER", "INTERNAL"} + bl_property = "item" + + def get_search_items(self, context): + items = [] + for node in get_all_nodes(): + items.append((node.nodetype, node.label, "")) + return items + + item: EnumProperty(items=get_search_items) + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + @classmethod + def description(cls, context, properties): + if cls.poll(context): + return "Search for a logic node" + else: + return "Search for a logic node. This operator is not available" \ + " without an active node tree" + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {"CANCELLED"} + + def execute(self, context): + """Called when a node is added.""" + bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.item, use_transform=True) + return {"FINISHED"} + + +class ArmNodeCategory: + """Represents a category (=directory) of logic nodes.""" + def __init__(self, name: str, icon: str, description: str, category_section: str): + self.name = name + self.icon = icon + self.description = description + self.category_section = category_section + self.node_sections: ODict[str, List[NodeItem]] = OrderedDict() + self.deprecated_nodes: List[NodeItem] = [] + + def register_node(self, node_type: Type[bpy.types.Node], node_section: str) -> None: + """Registers a node to this category so that it will be + displayed int the `Add node` menu.""" + self.add_node_section(node_section) + self.node_sections[node_section].append(arm.node_utils.nodetype_to_nodeitem(node_type)) + + def register_deprecated_node(self, node_type: Type[bpy.types.Node]) -> None: + if hasattr(node_type, 'arm_is_obsolete') and node_type.arm_is_obsolete: + self.deprecated_nodes.append(arm.node_utils.nodetype_to_nodeitem(node_type)) + + def get_all_nodes(self) -> Generator[NodeItem, None, None]: + """Returns all nodes that are registered into this category.""" + yield from itertools.chain(*self.node_sections.values()) + + def add_node_section(self, name: str): + """Adds a node section to this category.""" + if name not in self.node_sections: + self.node_sections[name] = [] + + def sort_nodes(self): + for node_section in self.node_sections: + self.node_sections[node_section] = sorted(self.node_sections[node_section], key=lambda item: item.label) + + +def category_exists(name: str) -> bool: + for category_section in category_items: + for c in category_items[category_section]: + if c.name == name: + return True + + return False + + +def get_category(name: str) -> Optional[ArmNodeCategory]: + for category_section in category_items: + for c in category_items[category_section]: + if c.name == name: + return c + + return None + + +def get_all_categories() -> Generator[ArmNodeCategory, None, None]: + for section_categories in category_items.values(): + yield from itertools.chain(section_categories) + + +def get_all_nodes() -> Generator[NodeItem, None, None]: + for category in get_all_categories(): + yield from itertools.chain(category.get_all_nodes()) + + +def add_category_section(name: str) -> None: + """Adds a section of categories to the node menu to group multiple + categories visually together. The given name only acts as an ID and + is not displayed in the user inferface.""" + global category_items + if name not in category_items: + category_items[name] = [] + + +def add_node_section(name: str, category: str) -> None: + """Adds a section of nodes to the sub menu of the given category to + group multiple nodes visually together. The given name only acts as + an ID and is not displayed in the user inferface.""" + node_category = get_category(category) + + if node_category is not None: + node_category.add_node_section(name) + + +def add_category(category: str, section: str = 'default', icon: str = 'BLANK1', description: str = '') -> Optional[ArmNodeCategory]: + """Adds a category of nodes to the node menu.""" + global category_items + + add_category_section(section) + if not category_exists(category): + node_category = ArmNodeCategory(category, icon, description, section) + category_items[section].append(node_category) + return node_category + + return None + + +def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'default', is_obsolete: bool = False) -> None: + """ + Registers a node to the given category. If no section is given, the + node is put into the default section that does always exist. + + Warning: Make sure that this function is not called multiple times per node! + """ + global nodes + + category = eval_node_category(node_type, category) + + nodes.append(node_type) + node_category = get_category(category) + + if node_category is None: + node_category = add_category(category) + + if is_obsolete: + # We need the obsolete nodes to be registered in order to have them replaced, + # but do not add them to the menu. + if node_category is not None: + # Make the deprecated nodes available for documentation purposes + node_category.register_deprecated_node(node_type) + return + + node_category.register_node(node_type, section) + node_type.bl_icon = node_category.icon + + +def eval_node_category(node: Union[ArmLogicTreeNode, Type[ArmLogicTreeNode]], category='') -> str: + """Return the effective category name, that is the category name of + the given node with resolved `PKG_AS_CATEGORY`. + """ + if category == '': + category = node.arm_category + + if category == PKG_AS_CATEGORY: + return node.__module__.rsplit('.', 2)[-2].capitalize() + return category + + +def deprecated(*alternatives: str, message=""): + """Class decorator to deprecate logic node classes. You can pass multiple string + arguments with the names of the available alternatives as well as a message + (keyword-param only) with further information about the deprecation.""" + + def wrapper(cls: ArmLogicTreeNode) -> ArmLogicTreeNode: + cls.bl_label += ' (Deprecated)' + if hasattr(cls, 'bl_description'): + cls.bl_description = f'Deprecated. {cls.bl_description}' + else: + cls.bl_description = 'Deprecated.' + cls.bl_icon = 'ERROR' + cls.arm_is_obsolete = True + + # Deprecated nodes must use a category other than PKG_AS_CATEGORY + # in order to prevent an empty 'Deprecated' category showing up + # in the add node menu and in the generated wiki pages. The + # "old" category is still used to put the node into the correct + # category in the wiki. + assert cls.arm_category != PKG_AS_CATEGORY, f'Deprecated node {cls.__name__} is missing an explicit category definition!' + + if cls.__doc__ is None: + cls.__doc__ = '' + + if len(alternatives) > 0: + cls.__doc__ += '\n' + f'@deprecated {",".join(alternatives)}: {message}' + else: + cls.__doc__ += '\n' + f'@deprecated : {message}' + + return cls + + return wrapper + + +def is_logic_node_context(context: bpy.context) -> bool: + """Return whether the given bpy context is inside a logic node editor.""" + return context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'ArmLogicTreeType' + +def is_logic_node_edit_context(context: bpy.context) -> bool: + """Return whether the given bpy context is inside a logic node editor and tree is being edited.""" + if context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'ArmLogicTreeType': + return context.space_data.edit_tree + return False + + +def reset_globals(): + global nodes + global category_items + nodes = [] + category_items = OrderedDict() + + +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/arm_props.py b/blender/arm/logicnode/arm_props.py new file mode 100644 index 0000000000..020c4a70ff --- /dev/null +++ b/blender/arm/logicnode/arm_props.py @@ -0,0 +1,282 @@ +"""Custom bpy property creators for logic nodes. Please be aware that +the code in this file is usually run once at registration and not for +each individual node instance when it is created. + +The functions for creating typed properties wrap the private __haxe_prop +function to allow for IDE autocompletion. + +Some default parameters in the signature of functions in this module are +mutable (common Python pitfall, be aware of this!), but because they +don't get accessed later it doesn't matter here and we keep it this way +for parity with the Blender API. +""" +from typing import Any, Callable, Sequence, Union + +import sys +import bpy +from bpy.props import * + +__all__ = [ + 'HaxeBoolProperty', + 'HaxeBoolVectorProperty', + 'HaxeCollectionProperty', + 'HaxeEnumProperty', + 'HaxeFloatProperty', + 'HaxeFloatVectorProperty', + 'HaxeIntProperty', + 'HaxeIntVectorProperty', + 'HaxePointerProperty', + 'HaxeStringProperty', + 'RemoveHaxeProperty' +] + + +def __haxe_prop(prop_type: Callable, prop_name: str, *args, **kwargs) -> Any: + """Declares a logic node property as a property that will be + used ingame for a logic node.""" + update_callback: Callable = kwargs.get('update', None) + if update_callback is None: + def wrapper(self: bpy.types.Node, context: bpy.types.Context): + self.on_prop_update(context, prop_name) + kwargs['update'] = wrapper + else: + def wrapper(self: bpy.types.Node, context: bpy.types.Context): + update_callback(self, context) + self.on_prop_update(context, prop_name) + kwargs['update'] = wrapper + + # Tags are not allowed on classes other than bpy.types.ID or + # bpy.types.Bone, remove them here to prevent registration errors + if 'tags' in kwargs: + del kwargs['tags'] + + return prop_type(*args, **kwargs) + + +def HaxeBoolProperty( + prop_name: str, + *, # force passing further arguments as keywords, see PEP 3102 + name: str = "", + description: str = "", + default=False, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + update=None, + get=None, + set=None +) -> 'bpy.types.BoolProperty': + """Declares a new BoolProperty that has a Haxe counterpart with the + given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(BoolProperty, **locals()) + + +def HaxeBoolVectorProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default: list = (False, False, False), + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + size: int = 3, + update=None, + get=None, + set=None +) -> list['bpy.types.BoolProperty']: + """Declares a new BoolVectorProperty that has a Haxe counterpart + with the given prop_name (Python and Haxe names must be identical + for now). + """ + return __haxe_prop(BoolVectorProperty, **locals()) + + +def HaxeCollectionProperty( + prop_name: str, + *, + type=None, + name: str = "", + description: str = "", + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set() +) -> 'bpy.types.CollectionProperty': + """Declares a new CollectionProperty that has a Haxe counterpart + with the given prop_name (Python and Haxe names must be identical + for now). + """ + return __haxe_prop(CollectionProperty, **locals()) + + +def HaxeEnumProperty( + prop_name: str, + *, + items: Sequence, + name: str = "", + description: str = "", + default: Union[str, set[str]] = None, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + update=None, + get=None, + set=None +) -> 'bpy.types.EnumProperty': + """Declares a new EnumProperty that has a Haxe counterpart with the + given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(EnumProperty, **locals()) + + +def HaxeFloatProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default=0.0, + min: float = -3.402823e+38, + max: float = 3.402823e+38, + soft_min: float = -3.402823e+38, + soft_max: float = 3.402823e+38, + step: int = 3, + precision: int = 2, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + unit: str = 'NONE', + update=None, + get=None, + set=None +) -> 'bpy.types.FloatProperty': + """Declares a new FloatProperty that has a Haxe counterpart with the + given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(FloatProperty, **locals()) + + +def HaxeFloatVectorProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default: list = (0.0, 0.0, 0.0), + min: float = sys.float_info.min, + max: float = sys.float_info.max, + soft_min: float = sys.float_info.min, + soft_max: float = sys.float_info.max, + step: int = 3, + precision: int = 2, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + unit: str = 'NONE', + size: int = 3, + update=None, + get=None, + set=None +) -> list['bpy.types.FloatProperty']: + """Declares a new FloatVectorProperty that has a Haxe counterpart + with the given prop_name (Python and Haxe names must be identical + for now). + """ + return __haxe_prop(FloatVectorProperty, **locals()) + + +def HaxeIntProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default=0, + min: int = -2**31, + max: int = 2**31 - 1, + soft_min: int = -2**31, + soft_max: int = 2**31 - 1, + step: int = 1, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + update=None, + get=None, + set=None +) -> 'bpy.types.IntProperty': + """Declares a new IntProperty that has a Haxe counterpart with the + given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(IntProperty, **locals()) + + +def HaxeIntVectorProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default: list = (0, 0, 0), + min: int = -2**31, + max: int = 2**31 - 1, + soft_min: int = -2**31, + soft_max: int = 2**31 - 1, + step: int = 1, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + size: int = 3, + update=None, + get=None, + set=None +) -> list['bpy.types.IntProperty']: + """Declares a new IntVectorProperty that has a Haxe counterpart with + the given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(IntVectorProperty, **locals()) + + +def HaxePointerProperty( + prop_name: str, + *, + type=None, + name: str = "", + description: str = "", + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + poll=None, + update=None +) -> 'bpy.types.PointerProperty': + """Declares a new PointerProperty that has a Haxe counterpart with + the given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(PointerProperty, **locals()) + + +def RemoveHaxeProperty(cls, attr: str): + RemoveProperty(cls, attr) + + +def HaxeStringProperty( + prop_name: str, + *, + name: str = "", + description: str = "", + default: str = "", + maxlen: int = 0, + options: set = {'ANIMATABLE'}, + override: set = set(), + tags: set = set(), + subtype: str = 'NONE', + update=None, + get=None, + set=None +) -> 'bpy.types.StringProperty': + """Declares a new StringProperty that has a Haxe counterpart with + the given prop_name (Python and Haxe names must be identical for now). + """ + return __haxe_prop(StringProperty, **locals()) diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py new file mode 100644 index 0000000000..a07cad871d --- /dev/null +++ b/blender/arm/logicnode/arm_sockets.py @@ -0,0 +1,665 @@ +from math import pi, cos, sin, sqrt +from typing import Type + +import bpy +from bpy.props import * +from bpy.types import NodeSocket +import mathutils + +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +# See Blender sources: /source/blender/editors/space_node/drawnode.cc +# Permalink for 3.2.2: https://github.com/blender/blender/blob/bcfdb14560e77891d674c2701a5071a7c07baba3/source/blender/editors/space_node/drawnode.cc#L1152-L1167 +socket_colors = { + 'ArmNodeSocketAction': (0.8, 0.3, 0.3, 1), + 'ArmNodeSocketAnimAction': (0.8, 0.8, 0.8, 1), + 'ArmRotationSocket': (0.68, 0.22, 0.62, 1), + 'ArmNodeSocketArray': (0.8, 0.4, 0.0, 1), + 'ArmBoolSocket': (0.80, 0.65, 0.84, 1.0), + 'ArmColorSocket': (0.78, 0.78, 0.16, 1.0), + 'ArmDynamicSocket': (0.39, 0.78, 0.39, 1.0), + 'ArmFloatSocket': (0.63, 0.63, 0.63, 1.0), + 'ArmIntSocket': (0.059, 0.522, 0.149, 1), + 'ArmNodeSocketObject': (0.15, 0.55, 0.75, 1), + 'ArmStringSocket': (0.44, 0.70, 1.00, 1.0), + 'ArmVectorSocket': (0.39, 0.39, 0.78, 1.0), + 'ArmAnySocket': (0.9, 0.9, 0.9, 1) +} + + +def _on_update_socket(self, context): + self.node.on_socket_val_update(context, self) + + +class ArmCustomSocket(NodeSocket): + """ + A custom socket that can be used to define more socket types for + logic node packs. Do not use this type directly (it is not + registered)! + """ + + bl_idname = 'ArmCustomSocket' + bl_label = 'Custom Socket' + # note: trying to use the `type` property will fail. All custom nodes will have "VALUE" as a type, because it is the default. + arm_socket_type = 'NONE' + # please also declare a property named "default_value_raw" of arm_socket_type isn't "NONE" + + def get_default_value(self): + """Override this for values of unconnected input sockets.""" + return None + + def on_node_update(self): + """Called when the update() method of the corresponding node is called.""" + pass + + def copy_defaults(self, socket): + """Called when this socket default values are to be copied to the given socket""" + pass + + +class ArmActionSocket(ArmCustomSocket): + bl_idname = 'ArmNodeSocketAction' + bl_label = 'Action Socket' + arm_socket_type = 'NONE' + + def draw(self, context, layout, node, text): + layout.label(text=self.name) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + +class ArmAnimActionSocket(ArmCustomSocket): + bl_idname = 'ArmNodeSocketAnimAction' + bl_label = 'Action Socket' + arm_socket_type = 'STRING' + + default_value_get: PointerProperty(name='Action', type=bpy.types.Action) # legacy version of the line after this one + default_value_raw: PointerProperty(name='Action', type=bpy.types.Action, update=_on_update_socket) + + def __init__(self): + super().__init__() + if self.default_value_get is not None: + self.default_value_raw = self.default_value_get + self.default_value_get = None + + def get_default_value(self): + if self.default_value_raw is None: + return '' + if self.default_value_raw.name not in bpy.data.actions: + return self.default_value_raw.name + name = arm.utils.asset_name(bpy.data.actions[self.default_value_raw.name]) + return arm.utils.safestr(name) + + def draw(self, context, layout, node, text): + if self.is_output: + layout.label(text=self.name) + elif self.is_linked: + layout.label(text=self.name) + else: + layout.prop_search(self, 'default_value_raw', bpy.data, 'actions', icon='NONE', text='') + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + + +class ArmRotationSocket(ArmCustomSocket): + bl_idname = 'ArmRotationSocket' + bl_label = 'Rotation Socket' + arm_socket_type = 'ROTATION' # the internal representation is a quaternion, AKA a '4D vector' (using mathutils.Vector((x,y,z,w))) + + def get_default_value(self): + if self.default_value_raw is None: + return mathutils.Vector((0.0,0.0,0.0,1.0)) + else: + return self.default_value_raw + + def on_unit_update(self, context): + if self.default_value_unit == 'Rad': + fac = pi/180 # deg->rad conversion + else: + fac = 180/pi # rad->deg conversion + if self.default_value_mode == 'AxisAngle': + self.default_value_s3 *= fac + elif self.default_value_mode == 'EulerAngles': + self.default_value_s0 *= fac + self.default_value_s1 *= fac + self.default_value_s2 *= fac + self.do_update_raw(context) + + def on_mode_update(self, context): + if self.default_value_mode == 'Quaternion': + summ = abs(self.default_value_s0) + summ+= abs(self.default_value_s1) + summ+= abs(self.default_value_s2) + summ+= abs(self.default_value_s3) + if summ<0.01: + self.default_value_s3 = 1.0 + elif self.default_value_mode == 'AxisAngle': + summ = abs(self.default_value_s0) + summ+= abs(self.default_value_s1) + summ+= abs(self.default_value_s2) + if summ<1E-5: + self.default_value_s3 = 0.0 + self.do_update_raw(context) + + @staticmethod + def convert_to_quaternion(part1,part2,param1,param2,param3): + """converts a representation of rotation into a quaternion. + ``part1`` is a vector, ``part2`` is a scalar or None, + ``param1`` is in ('Quaternion', 'EulerAngles', 'AxisAngle'), + ``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle, + ``param3`` is a len-3 string like "XYZ", for EulerAngles """ + if param1=='Quaternion': + qx, qy, qz = part1[0], part1[1], part1[2] + qw = part2 + # need to normalize the quaternion for a rotation (having it be 0 is not an option) + ql = sqrt(qx**2+qy**2+qz**2+qw**2) + if abs(ql)<1E-5: + qx, qy, qz, qw = 0.0,0.0,0.0,1.0 + else: + qx /= ql + qy /= ql + qz /= ql + qw /= ql + return mathutils.Vector((qx,qy,qz,qw)) + + elif param1 == 'AxisAngle': + if param2 == 'Deg': + angle = part2 * pi/180 + else: + angle = part2 + cang, sang = cos(angle/2), sin(angle/2) + x,y,z = part1[0], part1[1], part1[2] + veclen = sqrt(x**2+y**2+z**2) + if veclen<1E-5: + return mathutils.Vector((0.0,0.0,0.0,1.0)) + else: + return mathutils.Vector(( + x/veclen * sang, + y/veclen * sang, + z/veclen * sang, + cang + )) + else: # param1 == 'EulerAngles' + x,y,z = part1[0], part1[1], part1[2] + if param2 == 'Deg': + x *= pi/180 + y *= pi/180 + z *= pi/180 + cx, sx = cos(x/2), sin(x/2) + cy, sy = cos(y/2), sin(y/2) + cz, sz = cos(z/2), sin(z/2) + + qw, qx, qy, qz = 1.0,0.0,0.0,0.0 + for direction in param3[::-1]: + qwi, qxi,qyi,qzi = {'X': (cx,sx,0,0), 'Y': (cy,0,sy,0), 'Z': (cz,0,0,sz)}[direction] + + qw = qw*qwi -qx*qxi -qy*qyi -qz*qzi + qx = qx*qwi +qw*qxi +qy*qzi -qz*qyi + qy = qy*qwi +qw*qyi +qz*qxi -qx*qzi + qz = qz*qwi +qw*qzi +qx*qyi -qy*qxi + return mathutils.Vector((qx,qy,qz,qw)) + + + def do_update_raw(self, context): + part1 = mathutils.Vector(( + self.default_value_s0, + self.default_value_s1, + self.default_value_s2, 1 + )) + part2 = self.default_value_s3 + + self.default_value_raw = self.convert_to_quaternion( + part1, + self.default_value_s3, + self.default_value_mode, + self.default_value_unit, + self.default_value_order + ) + + + def draw(self, context, layout, node, text): + if (self.is_output or self.is_linked): + layout.label(text=self.name) + else: + coll1 = layout.column(align=True) + coll1.label(text=self.name) + bx=coll1.box() + coll = bx.column(align=True) + coll.prop(self, 'default_value_mode') + if self.default_value_mode in ('EulerAngles', 'AxisAngle'): + coll.prop(self, 'default_value_unit') + + if self.default_value_mode == 'EulerAngles': + coll.prop(self, 'default_value_order') + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + elif self.default_value_mode == 'Quaternion': + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + coll.prop(self, 'default_value_s3', text='W') + elif self.default_value_mode == 'AxisAngle': + coll.prop(self, 'default_value_s0', text='X') + coll.prop(self, 'default_value_s1', text='Y') + coll.prop(self, 'default_value_s2', text='Z') + coll.separator() + coll.prop(self, 'default_value_s3', text='Angle') + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + default_value_mode: EnumProperty( + items=[('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_mode_update) + + default_value_unit: EnumProperty( + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad', + update=on_unit_update) + default_value_order: EnumProperty( + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ' + ) + + default_value_s0: FloatProperty(update=do_update_raw) + default_value_s1: FloatProperty(update=do_update_raw) + default_value_s2: FloatProperty(update=do_update_raw) + default_value_s3: FloatProperty(update=do_update_raw) + + default_value_raw: FloatVectorProperty( + name='Value', + description='Raw quaternion obtained for the default value of a ArmRotationSocket socket', + size=4, default=(0,0,0,1), + update = _on_update_socket + ) + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_mode = self.default_value_mode + socket.default_value_unit = self.default_value_unit + socket.default_value_order = self.default_value_order + socket.default_value_s0 = self.default_value_s0 + socket.default_value_s1 = self.default_value_s1 + socket.default_value_s2 = self.default_value_s2 + socket.default_value_s3 = self.default_value_s3 + socket.default_value_raw = self.default_value_raw + + +class ArmArraySocket(ArmCustomSocket): + bl_idname = 'ArmNodeSocketArray' + bl_label = 'Array Socket' + arm_socket_type = 'NONE' + + def draw(self, context, layout, node, text): + layout.label(text=self.name) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + +class ArmBoolSocket(ArmCustomSocket): + bl_idname = 'ArmBoolSocket' + bl_label = 'Boolean Socket' + arm_socket_type = 'BOOLEAN' + + default_value_raw: BoolProperty( + name='Value', + description='Input value used for unconnected socket', + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + draw_socket_layout(self, layout) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + + +class ArmColorSocket(ArmCustomSocket): + bl_idname = 'ArmColorSocket' + bl_label = 'Color Socket' + arm_socket_type = 'RGBA' + + default_value_raw: FloatVectorProperty( + name='Value', + size=4, + subtype='COLOR', + min=0.0, + max=1.0, + default=[0.0, 0.0, 0.0, 1.0], + description='Input value used for unconnected socket', + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + draw_socket_layout_split(self, layout) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + + +class ArmDynamicSocket(ArmCustomSocket): + bl_idname = 'ArmDynamicSocket' + bl_label = 'Dynamic Socket' + arm_socket_type = 'NONE' + + def draw(self, context, layout, node, text): + layout.label(text=self.name) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + +class ArmAnySocket(ArmCustomSocket): + bl_idname = 'ArmAnySocket' + bl_label = 'Any Socket' + arm_socket_type = 'NONE' + + # Callback function when socket label is changed + def on_disp_label_update(self, context): + node = self.node + if node.bl_idname == 'LNGroupInputsNode' or node.bl_idname == 'LNGroupOutputsNode': + if not node.invalid_link: + node.socket_name_update(self) + self.on_node_update() + self.name = self.display_label + + display_label: StringProperty( + name='display_label', + description='Property to store socket display name', + update=on_disp_label_update) + + display_color: FloatVectorProperty( + name='Color', + size=4, + subtype='COLOR', + min=0.0, + max=1.0, + default=socket_colors['ArmAnySocket'] + ) + + def draw(self, context, layout, node, text): + layout.label(text=self.display_label) + + def draw_color(self, context, node): + return self.display_color + + def on_node_update(self): + # Cache name and color of connected socket + if self.is_output: + c_node, c_socket = arm.node_utils.output_get_connected_node(self) + else: + c_node, c_socket = arm.node_utils.input_get_connected_node(self) + + if c_node is None: + self.display_color = socket_colors[self.__class__.bl_idname] + else: + if self.display_label == '': + self.display_label = c_socket.name + self.display_color = c_socket.draw_color(bpy.context, c_node) + + +class ArmFloatSocket(ArmCustomSocket): + bl_idname = 'ArmFloatSocket' + bl_label = 'Float Socket' + arm_socket_type = 'VALUE' + + default_value_raw: FloatProperty( + name='Value', + description='Input value used for unconnected socket', + precision=3, + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + draw_socket_layout(self, layout) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + +class ArmIntSocket(ArmCustomSocket): + bl_idname = 'ArmIntSocket' + bl_label = 'Integer Socket' + arm_socket_type = 'INT' + + default_value_raw: IntProperty( + name='Value', + description='Input value used for unconnected socket', + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + draw_socket_layout(self, layout) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + +class ArmObjectSocket(ArmCustomSocket): + bl_idname = 'ArmNodeSocketObject' + bl_label = 'Object Socket' + arm_socket_type = 'OBJECT' + + default_value_get: PointerProperty(name='Object', type=bpy.types.Object) # legacy version of the line after this one + default_value_raw: PointerProperty(name='Object', type=bpy.types.Object, update=_on_update_socket) + + def __init__(self): + super().__init__() + if self.default_value_get is not None: + self.default_value_raw = self.default_value_get + self.default_value_get = None + + def get_default_value(self): + if self.default_value_raw is None: + return '' + if self.default_value_raw.name not in bpy.data.objects: + return self.default_value_raw.name + return arm.utils.asset_name(bpy.data.objects[self.default_value_raw.name]) + + def draw(self, context, layout, node, text): + if self.is_output: + layout.label(text=self.name) + elif self.is_linked: + layout.label(text=self.name) + else: + row = layout.row(align=True) + row.prop_search(self, 'default_value_raw', context.scene, 'objects', icon='NONE', text=self.name) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + +class ArmStringSocket(ArmCustomSocket): + bl_idname = 'ArmStringSocket' + bl_label = 'String Socket' + arm_socket_type = 'STRING' + + default_value_raw: StringProperty( + name='Value', + description='Input value used for unconnected socket', + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + draw_socket_layout_split(self, layout) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + +class ArmVectorSocket(ArmCustomSocket): + bl_idname = 'ArmVectorSocket' + bl_label = 'Vector Socket' + arm_socket_type = 'VECTOR' + + default_value_raw: FloatVectorProperty( + name='Value', + size=3, + precision=3, + description='Input value used for unconnected socket', + update=_on_update_socket + ) + + def draw(self, context, layout, node, text): + if not self.is_output and not self.is_linked: + col = layout.column(align=True) + col.label(text=self.name + ":") + col.prop(self, 'default_value_raw', text='') + else: + layout.label(text=self.name) + + def draw_color(self, context, node): + return socket_colors[self.bl_idname] + + def get_default_value(self): + return self.default_value_raw + + def copy_defaults(self, socket): + if socket.bl_idname == self.bl_idname: + socket.default_value_raw = self.default_value_raw + +def draw_socket_layout(socket: bpy.types.NodeSocket, layout: bpy.types.UILayout, prop_name='default_value_raw'): + if not socket.is_output and not socket.is_linked: + layout.prop(socket, prop_name, text=socket.name) + else: + layout.label(text=socket.name) + + +def draw_socket_layout_split(socket: bpy.types.NodeSocket, layout: bpy.types.UILayout, prop_name='default_value_raw'): + if not socket.is_output and not socket.is_linked: + # Blender layouts use 0.4 splits + layout = layout.split(factor=0.4, align=True) + + layout.label(text=socket.name) + + if not socket.is_output and not socket.is_linked: + layout.prop(socket, prop_name, text='') + + +def _make_socket_interface(interface_name: str, bl_idname: str) -> Type[bpy.types.NodeSocketInterface]: + """Create a socket interface class that is used by Blender for node + groups. We currently don't use real node groups, but without these + classes Blender will (incorrectly) draw the socket borders in light grey. + """ + def draw(self, context, layout): + pass + + def draw_color(self, context): + # This would be used if we were using "real" node groups + return 0, 0, 0, 1 + + cls = type( + interface_name, + (bpy.types.NodeSocketInterface, ), { + 'bl_socket_idname': bl_idname, + 'draw': draw, + 'draw_color': draw_color, + } + ) + return cls + + +ArmActionSocketInterface = _make_socket_interface('ArmActionSocketInterface', 'ArmNodeSocketAction') +ArmAnimSocketInterface = _make_socket_interface('ArmAnimSocketInterface', 'ArmNodeSocketAnimAction') +ArmRotationSocketInterface = _make_socket_interface('ArmRotationSocketInterface', 'ArmRotationSocket') +ArmArraySocketInterface = _make_socket_interface('ArmArraySocketInterface', 'ArmNodeSocketArray') +ArmBoolSocketInterface = _make_socket_interface('ArmBoolSocketInterface', 'ArmBoolSocket') +ArmColorSocketInterface = _make_socket_interface('ArmColorSocketInterface', 'ArmColorSocket') +ArmDynamicSocketInterface = _make_socket_interface('ArmDynamicSocketInterface', 'ArmDynamicSocket') +ArmFloatSocketInterface = _make_socket_interface('ArmFloatSocketInterface', 'ArmFloatSocket') +ArmIntSocketInterface = _make_socket_interface('ArmIntSocketInterface', 'ArmIntSocket') +ArmObjectSocketInterface = _make_socket_interface('ArmObjectSocketInterface', 'ArmNodeSocketObject') +ArmStringSocketInterface = _make_socket_interface('ArmStringSocketInterface', 'ArmStringSocket') +ArmVectorSocketInterface = _make_socket_interface('ArmVectorSocketInterface', 'ArmVectorSocket') +ArmAnySocketInterface = _make_socket_interface('ArmAnySocketInterface', 'ArmAnySocket') + + +REG_CLASSES = ( + ArmActionSocketInterface, + ArmAnimSocketInterface, + ArmRotationSocketInterface, + ArmArraySocketInterface, + ArmBoolSocketInterface, + ArmColorSocketInterface, + ArmDynamicSocketInterface, + ArmFloatSocketInterface, + ArmIntSocketInterface, + ArmObjectSocketInterface, + ArmStringSocketInterface, + ArmVectorSocketInterface, + ArmAnySocketInterface, + + ArmActionSocket, + ArmAnimActionSocket, + ArmRotationSocket, + ArmArraySocket, + ArmBoolSocket, + ArmColorSocket, + ArmDynamicSocket, + ArmFloatSocket, + ArmIntSocket, + ArmObjectSocket, + ArmStringSocket, + ArmVectorSocket, + ArmAnySocket, +) +register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) diff --git a/blender/arm/logicnode/array/LN_array.py b/blender/arm/logicnode/array/LN_array.py new file mode 100644 index 0000000000..7e4572e562 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class ArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given array as a variable.""" + bl_idname = 'LNArrayNode' + bl_label = 'Array Dynamic' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmDynamicSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmDynamicSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) + \ No newline at end of file diff --git a/blender/arm/logicnode/array/LN_array_add.py b/blender/arm/logicnode/array/LN_array_add.py new file mode 100644 index 0000000000..5ecf60b13b --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_add.py @@ -0,0 +1,45 @@ +from arm.logicnode.arm_nodes import * + +class ArrayAddNode(ArmLogicTreeNode): + """Adds the given value to the given array. + + @input Array: the array to manipulate. + @input Modify Original: if `false`, the input array is copied before adding the value. + @input Unique Values: if `true`, values may occur only once in that array (only primitive data types are supported). + """ + bl_idname = 'LNArrayAddNode' + bl_label = 'Array Add' + arm_version = 4 + min_inputs = 5 + + def __init__(self): + super(ArrayAddNode, self).__init__() + array_nodes[self.get_id_str()] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmBoolSocket', 'Modify Original', default_value=True) + self.add_input('ArmBoolSocket', 'Unique Values') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketArray', 'Array') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input_value', text='Add Input', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmDynamicSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 3): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_boolean.py b/blender/arm/logicnode/array/LN_array_boolean.py new file mode 100644 index 0000000000..f59dada9c5 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_boolean.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class BooleanArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of boolean elements as a variable.""" + bl_idname = 'LNArrayBooleanNode' + bl_label = 'Array Boolean' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(BooleanArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmBoolSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmBoolSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_color.py b/blender/arm/logicnode/array/LN_array_color.py new file mode 100644 index 0000000000..3eb7a2ee40 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_color.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class ColorArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of color elements as a variable.""" + bl_idname = 'LNArrayColorNode' + bl_label = 'Array Color' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(ColorArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmColorSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmColorSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_compare.py b/blender/arm/logicnode/array/LN_array_compare.py new file mode 100644 index 0000000000..5b95d50c7d --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_compare.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ArrayCompareNode(ArmLogicTreeNode): + """Compare arrays.""" + + bl_idname = 'LNArrayCompareNode' + bl_label = 'Array Compare' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmBoolSocket', 'Compare') + diff --git a/blender/arm/logicnode/array/LN_array_concat.py b/blender/arm/logicnode/array/LN_array_concat.py new file mode 100644 index 0000000000..7e0a828f05 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_concat.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ArrayConcatNode(ArmLogicTreeNode): + """Join arrays.""" + + bl_idname = 'LNArrayConcatNode' + bl_label = 'Array Concat' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketArray', 'Array') + self.add_output('ArmIntSocket', 'Length') diff --git a/blender/arm/logicnode/array/LN_array_contains.py b/blender/arm/logicnode/array/LN_array_contains.py new file mode 100644 index 0000000000..c81ec46a31 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_contains.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayContainsNode(ArmLogicTreeNode): + """Returns whether the given array contains the given value.""" + bl_idname = 'LNArrayInArrayNode' + bl_label = 'Array Contains' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmBoolSocket', 'Contains') diff --git a/blender/arm/logicnode/array/LN_array_count.py b/blender/arm/logicnode/array/LN_array_count.py new file mode 100644 index 0000000000..9ad960fb3c --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_count.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class ArrayCountNode(ArmLogicTreeNode): + """Returns an array with the item counts of the given array.""" + bl_idname = 'LNArrayCountNode' + bl_label = 'Array Count' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketArray', 'Count') diff --git a/blender/arm/logicnode/array/LN_array_display.py b/blender/arm/logicnode/array/LN_array_display.py new file mode 100644 index 0000000000..6dd6cb31e4 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_display.py @@ -0,0 +1,31 @@ +from arm.logicnode.arm_nodes import * + +class ArrayDisplayNode(ArmLogicTreeNode): + """Returns the length of the given array.""" + bl_idname = 'LNArrayDisplayNode' + bl_label = 'Array Display' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 2: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Item Field': + self.add_input('ArmStringSocket', 'Item Field') + if self.property0 == 'Item Property': + self.add_input('ArmStringSocket', 'Item Property') + + property0: HaxeEnumProperty( + 'property0', + items = [('Item', 'Item', 'Array Item'), + ('Item Field', 'Item Field', 'Object Item Field, ie: name, uid, visible, parent, length, etc.'), + ('Item Property', 'Item Property', 'Object Item Property')], + name='', default='Item', update=remove_extra_inputs) + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmStringSocket', 'Separator') + + self.add_output('ArmStringSocket', 'Items') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') \ No newline at end of file diff --git a/blender/arm/logicnode/array/LN_array_distinct.py b/blender/arm/logicnode/array/LN_array_distinct.py new file mode 100644 index 0000000000..06d3cebecb --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_distinct.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayDistinctNode(ArmLogicTreeNode): + """Returns the Distinct and Duplicated items of the given array.""" + bl_idname = 'LNArrayDistinctNode' + bl_label = 'Array Distinct' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketArray', 'Distinct') + self.add_output('ArmNodeSocketArray', 'Duplicated') diff --git a/blender/arm/logicnode/array/LN_array_filter.py b/blender/arm/logicnode/array/LN_array_filter.py new file mode 100644 index 0000000000..5d4204e136 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_filter.py @@ -0,0 +1,49 @@ +from arm.logicnode.arm_nodes import * + +class ArrayFilterNode(ArmLogicTreeNode): + """Returns the length of the given array.""" + bl_idname = 'LNArrayFilterNode' + bl_label = 'Array Filter' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Item Field': + self.add_input('ArmStringSocket', 'Item Field') + if self.property0 == 'Item Property': + self.add_input('ArmStringSocket', 'Item Property') + self.add_input('ArmDynamicSocket', 'Value') + if self.property1 == 'Between': + self.add_input('ArmDynamicSocket', 'Value') + + property0: HaxeEnumProperty( + 'property0', + items = [('Item', 'Item', 'Array Item'), + ('Item Field', 'Item Field', 'Object Item Field, ie: Name, Uid, Visible, Parent, Length, etc.'), + ('Item Property', 'Item Property', 'Object Item Property')], + name='', default='Item', update=remove_extra_inputs) + + property1: HaxeEnumProperty( + 'property1', + items = [('Equal', 'Equal', 'Equal'), + ('Not Equal', 'Not Equal', 'Not Equal'), + ('Greater', 'Greater', 'Greater'), + ('Greater Equal', 'Greater Equal', 'Greater Equal'), + ('Less', 'Less', 'Less'), + ('Less Equal', 'Less Equal', 'Less Equal'), + ('Between', 'Between', 'Input 1 Between Input 2 and Input 3 inclusive'), + ('Contains', 'Contains', 'Contains'), + ('Starts With', 'Starts With', 'Starts With'), + ('Ends With', 'Ends With', 'Ends With')], + name='', default='Equal', update=remove_extra_inputs) + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketArray', 'Array') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/array/LN_array_float.py b/blender/arm/logicnode/array/LN_array_float.py new file mode 100644 index 0000000000..9b417de449 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_float.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class FloatArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of float elements as a variable.""" + bl_idname = 'LNArrayFloatNode' + bl_label = 'Array Float' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(FloatArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmFloatSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmFloatSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_get.py b/blender/arm/logicnode/array/LN_array_get.py new file mode 100644 index 0000000000..598a60f5c4 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_get.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayGetNode(ArmLogicTreeNode): + """Returns the value of the given array at the given index.""" + bl_idname = 'LNArrayGetNode' + bl_label = 'Array Get' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Index') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_get_PreviousNext.py b/blender/arm/logicnode/array/LN_array_get_PreviousNext.py new file mode 100644 index 0000000000..620e16278e --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_get_PreviousNext.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayGetPreviousNextNode(ArmLogicTreeNode): + """Returns the previous or next value to be retrieve by looping the array according to the boolean condition.""" + bl_idname = 'LNArrayGetPreviousNextNode' + bl_label = 'Array Get Previous/Next' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmBoolSocket', 'Previous: 0 / Next: 1') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_get_next.py b/blender/arm/logicnode/array/LN_array_get_next.py new file mode 100644 index 0000000000..491d831172 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_get_next.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class ArrayGetNextNode(ArmLogicTreeNode): + """Returns the next value to be retrieve by looping the array.""" + bl_idname = 'LNArrayGetNextNode' + bl_label = 'Array Get Next' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_index.py b/blender/arm/logicnode/array/LN_array_index.py new file mode 100644 index 0000000000..d859cb61c2 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_index.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayIndexNode(ArmLogicTreeNode): + """Returns the array index of the given value.""" + bl_idname = 'LNArrayIndexNode' + bl_label = 'Array Index' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmIntSocket', 'Index') diff --git a/blender/arm/logicnode/array/LN_array_integer.py b/blender/arm/logicnode/array/LN_array_integer.py new file mode 100644 index 0000000000..5f57f7a0b8 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_integer.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class IntegerArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of integer elements as a variable.""" + bl_idname = 'LNArrayIntegerNode' + bl_label = 'Array Integer' + arm_version = 4 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(IntegerArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmIntSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmIntSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 3): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_length.py b/blender/arm/logicnode/array/LN_array_length.py new file mode 100644 index 0000000000..3ef4e9f101 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_length.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class ArrayLengthNode(ArmLogicTreeNode): + """Returns the length of the given array.""" + bl_idname = 'LNArrayLengthNode' + bl_label = 'Array Length' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmIntSocket', 'Length') diff --git a/blender/arm/logicnode/array/LN_array_loop_node.py b/blender/arm/logicnode/array/LN_array_loop_node.py new file mode 100644 index 0000000000..aefd733193 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_loop_node.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +class ArrayLoopNode(ArmLogicTreeNode): + """Loops through each item of the given array.""" + bl_idname = 'LNArrayLoopNode' + bl_label = 'Array Loop' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketAction', 'Loop') + self.add_output('ArmDynamicSocket', 'Value') + self.add_output('ArmIntSocket', 'Index') + self.add_output('ArmNodeSocketAction', 'Done') diff --git a/blender/arm/logicnode/array/LN_array_object.py b/blender/arm/logicnode/array/LN_array_object.py new file mode 100644 index 0000000000..686a184929 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_object.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class ObjectArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of object elements as a variable.""" + bl_idname = 'LNArrayObjectNode' + bl_label = 'Array Object' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(ObjectArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmNodeSocketObject' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmNodeSocketObject', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].default_value_raw + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_pop.py b/blender/arm/logicnode/array/LN_array_pop.py new file mode 100644 index 0000000000..37b9ad5ca5 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_pop.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class ArrayPopNode(ArmLogicTreeNode): + """Removes the last element of the given array. + + @see [Haxe API](https://api.haxe.org/Array.html#pop)""" + bl_idname = 'LNArrayPopNode' + bl_label = 'Array Pop' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_remove_by_index.py b/blender/arm/logicnode/array/LN_array_remove_by_index.py new file mode 100644 index 0000000000..efc69b4361 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_remove_by_index.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class ArrayRemoveIndexNode(ArmLogicTreeNode): + """Removes the element from the given array by its index. + + @seeNode Array Remove by Value""" + bl_idname = 'LNArrayRemoveNode' + bl_label = 'Array Remove by Index' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Index') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_remove_by_value.py b/blender/arm/logicnode/array/LN_array_remove_by_value.py new file mode 100644 index 0000000000..5314b7145d --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_remove_by_value.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + +class ArrayRemoveValueNode(ArmLogicTreeNode): + """Removes the element from the given array by its value. + + @seeNode Array Remove by Index""" + bl_idname = 'LNArrayRemoveValueNode' + bl_label = 'Array Remove by Value' + arm_version = 1 + + # def __init__(self): + # array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Value') + + # def draw_buttons(self, context, layout): + # row = layout.row(align=True) + + # op = row.operator('arm.node_add_input_value', text='New', icon='PLUS', emboss=True) + # op.node_index = str(id(self)) + # op.socket_type = 'ArmDynamicSocket' + # op2 = row.operator('arm.node_remove_input_value', text='', icon='X', emboss=True) + # op2.node_index = str(id(self)) diff --git a/blender/arm/logicnode/array/LN_array_resize.py b/blender/arm/logicnode/array/LN_array_resize.py new file mode 100644 index 0000000000..2fc26e53fd --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_resize.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +class ArrayResizeNode(ArmLogicTreeNode): + """Resize the array to the given length. For more details, please + take a look at the documentation of `Array.resize()` in the + [Haxe API](https://api.haxe.org/Array.html#resize). + """ + bl_idname = 'LNArrayResizeNode' + bl_label = 'Array Resize' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Length') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/array/LN_array_reverse.py b/blender/arm/logicnode/array/LN_array_reverse.py new file mode 100644 index 0000000000..020685fc62 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_reverse.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayReverseNode(ArmLogicTreeNode): + """Reverse the items order of the array.""" + + bl_idname = 'LNArrayReverseNode' + bl_label = 'Array Reverse' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketArray', 'Array') diff --git a/blender/arm/logicnode/array/LN_array_sample.py b/blender/arm/logicnode/array/LN_array_sample.py new file mode 100644 index 0000000000..d987fdad78 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_sample.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ArraySampleNode(ArmLogicTreeNode): + """Take a sample of n items from an array (boolean option to remove those items from original array) + """ + bl_idname = 'LNArraySampleNode' + bl_label = 'Array Sample' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Sample') + self.add_input('ArmBoolSocket', 'Remove') + + self.add_output('ArmNodeSocketArray', 'Array') diff --git a/blender/arm/logicnode/array/LN_array_set.py b/blender/arm/logicnode/array/LN_array_set.py new file mode 100644 index 0000000000..e1eac97702 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_set.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ArraySetNode(ArmLogicTreeNode): + """Sets the value of the given array at the given index.""" + bl_idname = 'LNArraySetNode' + bl_label = 'Array Set' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Index') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/array/LN_array_shift.py b/blender/arm/logicnode/array/LN_array_shift.py new file mode 100644 index 0000000000..15ee7b78c9 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_shift.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class ArrayShiftNode(ArmLogicTreeNode): + """Removes the first element of the given array. + + @see [Haxe API](https://api.haxe.org/Array.html#shift)""" + bl_idname = 'LNArrayShiftNode' + bl_label = 'Array Shift' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/array/LN_array_shuffle.py b/blender/arm/logicnode/array/LN_array_shuffle.py new file mode 100644 index 0000000000..65cb7b0c4e --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_shuffle.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ArrayShuffleNode(ArmLogicTreeNode): + """Shuffle the items in the array and return a new array + """ + bl_idname = 'LNArrayShuffleNode' + bl_label = 'Array Shuffle' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketArray', 'Array') diff --git a/blender/arm/logicnode/array/LN_array_slice.py b/blender/arm/logicnode/array/LN_array_slice.py new file mode 100644 index 0000000000..be2b1bd4e3 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_slice.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class ArraySliceNode(ArmLogicTreeNode): + """Creates a shallow copy of the given array in the specified range. + + @see [Haxe API](https://api.haxe.org/Array.html#slice)""" + bl_idname = 'LNArraySliceNode' + bl_label = 'Array Slice' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Index') + self.add_input('ArmIntSocket', 'End') + + self.add_output('ArmNodeSocketArray', 'Array') diff --git a/blender/arm/logicnode/array/LN_array_sort.py b/blender/arm/logicnode/array/LN_array_sort.py new file mode 100644 index 0000000000..5cd56abb01 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_sort.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class ArraySortNode(ArmLogicTreeNode): + """Sort the items order of the array by ascending or descending.""" + + bl_idname = 'LNArraySortNode' + bl_label = 'Array Sort' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmBoolSocket', 'Descending', default_value=False) + + self.add_output('ArmNodeSocketArray', 'Array') diff --git a/blender/arm/logicnode/array/LN_array_splice.py b/blender/arm/logicnode/array/LN_array_splice.py new file mode 100644 index 0000000000..1f367cc533 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_splice.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class ArraySpliceNode(ArmLogicTreeNode): + """Removes the given amount of elements from the given array. + + @see [Haxe API](https://api.haxe.org/Array.html#splice)""" + bl_idname = 'LNArraySpliceNode' + bl_label = 'Array Splice' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + self.add_input('ArmIntSocket', 'Index') + self.add_input('ArmIntSocket', 'Length') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/array/LN_array_string.py b/blender/arm/logicnode/array/LN_array_string.py new file mode 100644 index 0000000000..87d39f0764 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_string.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class StringArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of string elements as a variable.""" + bl_idname = 'LNArrayStringNode' + bl_label = 'Array String' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(StringArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmStringSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmStringSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/LN_array_vector.py b/blender/arm/logicnode/array/LN_array_vector.py new file mode 100644 index 0000000000..55d51392f0 --- /dev/null +++ b/blender/arm/logicnode/array/LN_array_vector.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class VectorArrayNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores an array of vector elements as a variable.""" + bl_idname = 'LNArrayVectorNode' + bl_label = 'Array Vector' + arm_version = 3 + arm_section = 'variable' + min_inputs = 0 + + def __init__(self): + super(VectorArrayNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array', is_var=True) + self.add_output('ArmIntSocket', 'Length') + + def draw_content(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_input', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmVectorSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + return super().draw_label() + + return f'{super().draw_label()} [{len(self.inputs)}]' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs.clear() + for i in range(len(master_node.inputs)): + inp = self.add_input('ArmVectorSocket', master_node.inputs[i].name) + inp.hide = self.arm_logic_id != '' + inp.enabled = self.arm_logic_id == '' + inp.default_value_raw = master_node.inputs[i].get_default_value() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/array/__init__.py b/blender/arm/logicnode/array/__init__.py new file mode 100644 index 0000000000..fa2ed7a5ca --- /dev/null +++ b/blender/arm/logicnode/array/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='variable', category='Array') +add_node_section(name='default', category='Array') diff --git a/blender/arm/logicnode/camera/LN_get_camera_active.py b/blender/arm/logicnode/camera/LN_get_camera_active.py new file mode 100644 index 0000000000..9956d45e84 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_active.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class ActiveCameraNode(ArmLogicTreeNode): + """Returns the active camera. + + @seeNode Set Active Camera""" + bl_idname = 'LNActiveCameraNode' + bl_label = 'Get Camera Active' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketObject', 'Camera') diff --git a/blender/arm/logicnode/camera/LN_get_camera_aspect.py b/blender/arm/logicnode/camera/LN_get_camera_aspect.py new file mode 100644 index 0000000000..9cded8a68b --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_aspect.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class GetCameraAspectNode(ArmLogicTreeNode): + """Returns the aspect of the given camera.""" + bl_idname = 'LNGetCameraAspectNode' + bl_label = 'Get Camera Aspect' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmFloatSocket', 'Aspect') diff --git a/blender/arm/logicnode/camera/LN_get_camera_fov.py b/blender/arm/logicnode/camera/LN_get_camera_fov.py new file mode 100644 index 0000000000..b5e7046657 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_fov.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetCameraFovNode(ArmLogicTreeNode): + """Returns the field of view (FOV) of the given camera. + + @seeNode Set Camera FOV""" + bl_idname = 'LNGetCameraFovNode' + bl_label = 'Get Camera FOV' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmFloatSocket', 'FOV') diff --git a/blender/arm/logicnode/camera/LN_get_camera_scale.py b/blender/arm/logicnode/camera/LN_get_camera_scale.py new file mode 100644 index 0000000000..ef108a27fb --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_scale.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetCameraScaleNode(ArmLogicTreeNode): + """Returns the scale of the given camera.""" + bl_idname = 'LNGetCameraScaleNode' + bl_label = 'Get Camera Ortho Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmFloatSocket', 'Ortho Scale') +2 \ No newline at end of file diff --git a/blender/arm/logicnode/camera/LN_get_camera_start_end.py b/blender/arm/logicnode/camera/LN_get_camera_start_end.py new file mode 100644 index 0000000000..168da24866 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_start_end.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetCameraStartEndNode(ArmLogicTreeNode): + """Returns the Start & End of the given camera. + + @seeNode Set Camera Start & End""" + bl_idname = 'LNGetCameraStartEndNode' + bl_label = 'Get Camera Start End' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmFloatSocket', 'Start') + self.add_output('ArmFloatSocket', 'End') \ No newline at end of file diff --git a/blender/arm/logicnode/camera/LN_get_camera_type.py b/blender/arm/logicnode/camera/LN_get_camera_type.py new file mode 100644 index 0000000000..8f4b017d45 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_get_camera_type.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetCameraTypeNode(ArmLogicTreeNode): + """Returns the camera Type: + 0 : Perspective + 1 : Ortographic + .""" + bl_idname = 'LNGetCameraTypeNode' + bl_label = 'Get Camera Type' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmBoolSocket', 'Type') diff --git a/blender/arm/logicnode/camera/LN_set_camera_active.py b/blender/arm/logicnode/camera/LN_set_camera_active.py new file mode 100644 index 0000000000..fa078d10a7 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_active.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraNode(ArmLogicTreeNode): + """Sets the active camera. + + @seeNode Get Active Camera""" + bl_idname = 'LNSetCameraNode' + bl_label = 'Set Camera Active' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/camera/LN_set_camera_aspect.py b/blender/arm/logicnode/camera/LN_set_camera_aspect.py new file mode 100644 index 0000000000..fa0190ec9c --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_aspect.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraAspectNode(ArmLogicTreeNode): + """Sets the aspect of the given camera.""" + bl_idname = 'LNSetCameraAspectNode' + bl_label = 'Set Camera Aspect' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmFloatSocket', 'Aspect', default_value=1.7) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/camera/LN_set_camera_fov.py b/blender/arm/logicnode/camera/LN_set_camera_fov.py new file mode 100644 index 0000000000..1974c0d0b5 --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_fov.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraFovNode(ArmLogicTreeNode): + """Sets the field of view (FOV) of the given camera. + + @seeNode Get Camera FOV""" + bl_idname = 'LNSetCameraFovNode' + bl_label = 'Set Camera FOV' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmFloatSocket', 'FOV', default_value=0.9) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/camera/LN_set_camera_scale.py b/blender/arm/logicnode/camera/LN_set_camera_scale.py new file mode 100644 index 0000000000..3ac11ad10a --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_scale.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraScaleNode(ArmLogicTreeNode): + """Sets the aspect of the given camera.""" + bl_idname = 'LNSetCameraScaleNode' + bl_label = 'Set Camera Ortho Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmFloatSocket', 'Ortho Scale', default_value=1.7) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/camera/LN_set_camera_start_end.py b/blender/arm/logicnode/camera/LN_set_camera_start_end.py new file mode 100644 index 0000000000..423929fabd --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_start_end.py @@ -0,0 +1,39 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraStartEndNode(ArmLogicTreeNode): + """Sets the Start & End of the given camera. + + @seeNode Get Camera Start & End""" + bl_idname = 'LNSetCameraStartEndNode' + bl_label = 'Set Camera Start End' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 2: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Start': + self.add_input('ArmFloatSocket', 'Start') + if self.property0 == 'End': + self.add_input('ArmFloatSocket', 'End') + if self.property0 == 'Start&End': + self.add_input('ArmFloatSocket', 'Start') + self.add_input('ArmFloatSocket', 'End') + + property0: HaxeEnumProperty( + 'property0', + items = [('Start&End', 'Start&End', 'Start&End'), + ('Start', 'Start', 'Start'), + ('End', 'End', 'End')], + name='', default='Start&End', update=remove_extra_inputs) + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmFloatSocket', 'Start') + self.add_input('ArmFloatSocket', 'End') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/camera/LN_set_camera_type.py b/blender/arm/logicnode/camera/LN_set_camera_type.py new file mode 100644 index 0000000000..516ddcf69e --- /dev/null +++ b/blender/arm/logicnode/camera/LN_set_camera_type.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + +class SetCameraTypeNode(ArmLogicTreeNode): + """Sets the camera type.""" + bl_idname = 'LNSetCameraTypeNode' + bl_label = 'Set Camera Type' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 2: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Perspective': + self.add_input('ArmFloatSocket', 'Fov') + self.add_input('ArmFloatSocket', 'Start') + self.add_input('ArmFloatSocket', 'End') + if self.property0 == 'Orthographic': + self.add_input('ArmFloatSocket', 'Scale') + + property0: HaxeEnumProperty( + 'property0', + items = [('Perspective', 'Perspective', 'Perspective'), + ('Orthographic', 'Orthographic', 'Orthographic')], + name='', default='Perspective', update=remove_extra_inputs) + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmFloatSocket', 'Fov') + + 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/camera/__init__.py b/blender/arm/logicnode/camera/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py b/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py new file mode 100644 index 0000000000..de3cee1d59 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetCheckboxNode(ArmLogicTreeNode): + """Returns whether the given UI checkbox is checked.""" + bl_idname = 'LNCanvasGetCheckboxNode' + bl_label = 'Get Canvas Checkbox' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmBoolSocket', 'Is Checked') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py b/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py new file mode 100644 index 0000000000..e8d1a7d378 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetInputTextNode(ArmLogicTreeNode): + """Returns the input text of the given UI element.""" + bl_idname = 'LNCanvasGetInputTextNode' + bl_label = 'Get Canvas Input Text' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmStringSocket', 'Text') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_location.py b/blender/arm/logicnode/canvas/LN_get_canvas_location.py new file mode 100644 index 0000000000..71e9491f93 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_location.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetLocationNode(ArmLogicTreeNode): + """Returns the location of the given UI element (pixels).""" + bl_idname = 'LNCanvasGetLocationNode' + bl_label = 'Get Canvas Location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'X') + self.add_output('ArmIntSocket', 'Y') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_position.py b/blender/arm/logicnode/canvas/LN_get_canvas_position.py new file mode 100644 index 0000000000..47efa750da --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_position.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetPositionNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNCanvasGetPositionNode' + bl_label = 'Get Canvas Position' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmIntSocket', 'Position') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py b/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py new file mode 100644 index 0000000000..5e9435ca34 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetPBNode(ArmLogicTreeNode): + """Returns the value of the given UI progress bar.""" + bl_idname = 'LNCanvasGetPBNode' + bl_label = 'Get Canvas Progress Bar' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'At') + self.add_output('ArmIntSocket', 'Max') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py b/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py new file mode 100644 index 0000000000..3b7ffa9ecf --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetRotationNode(ArmLogicTreeNode): + """Returns the rotation of the given UI element.""" + bl_idname = 'LNCanvasGetRotationNode' + bl_label = 'Get Canvas Rotation' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmFloatSocket', 'Rad') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_scale.py b/blender/arm/logicnode/canvas/LN_get_canvas_scale.py new file mode 100644 index 0000000000..9e65ea3931 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_scale.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetScaleNode(ArmLogicTreeNode): + """Returns the scale of the given UI element.""" + bl_idname = 'LNCanvasGetScaleNode' + bl_label = 'Get Canvas Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Height') + self.add_output('ArmIntSocket', 'Width') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_slider.py b/blender/arm/logicnode/canvas/LN_get_canvas_slider.py new file mode 100644 index 0000000000..c0d09cc401 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_slider.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetSliderNode(ArmLogicTreeNode): + """Returns the value of the given UI slider.""" + bl_idname = 'LNCanvasGetSliderNode' + bl_label = 'Get Canvas Slider' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmFloatSocket', 'Float') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_text.py b/blender/arm/logicnode/canvas/LN_get_canvas_text.py new file mode 100644 index 0000000000..8abbcef86a --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_text.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasGetTextNode(ArmLogicTreeNode): + """Sets the text of the given UI element.""" + bl_idname = 'LNCanvasGetTextNode' + bl_label = 'Get Canvas Text' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_visible.py b/blender/arm/logicnode/canvas/LN_get_canvas_visible.py new file mode 100644 index 0000000000..3fb4ae1ba9 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_canvas_visible.py @@ -0,0 +1,15 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class CanvasGetVisibleNode(ArmLogicTreeNode): + """Returns whether the given UI element is visible.""" + bl_idname = 'LNCanvasGetVisibleNode' + bl_label = 'Get Canvas Visible' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmStringSocket', 'Element') + + self.outputs.new('ArmBoolSocket', 'Is Visible') diff --git a/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py b/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py new file mode 100644 index 0000000000..a9b1f477a6 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + + +class GetGlobalCanvasFontSizeNode(ArmLogicTreeNode): + """Returns the font size of the entire UI Canvas.""" + bl_idname = 'LNGetGlobalCanvasFontSizeNode' + bl_label = 'Get Global Canvas Font Size' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Size') diff --git a/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py b/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py new file mode 100644 index 0000000000..ca832f21e0 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + + +class GetGlobalCanvasScaleNode(ArmLogicTreeNode): + """Returns the scale of the entire UI Canvas.""" + bl_idname = 'LNGetGlobalCanvasScaleNode' + bl_label = 'Get Global Canvas Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Scale') diff --git a/blender/arm/logicnode/canvas/LN_on_canvas_element.py b/blender/arm/logicnode/canvas/LN_on_canvas_element.py new file mode 100644 index 0000000000..fe65ad0cb1 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_on_canvas_element.py @@ -0,0 +1,37 @@ +from arm.logicnode.arm_nodes import * + +class OnCanvasElementNode(ArmLogicTreeNode): + """Activates the output whether an action over the given UI element is done.""" + bl_idname = 'LNOnCanvasElementNode' + bl_label = 'On Canvas Element' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items=[('click', 'Click', 'Listen to mouse clicks'), + ('hover', 'Hover', 'Listen to mouse hover')], + name='Listen to', default='click') + property1: HaxeEnumProperty( + 'property1', + items=[('started', 'Started', 'Started'), + ('down', 'Down', 'Down'), + ('released', 'Released', 'Released')], + name='Status', default='started') + property2: HaxeEnumProperty( + 'property2', + items=[('left', 'Left', 'Left mouse button'), + ('middle', 'Middle', 'Middle mouse button'), + ('right', 'Right', 'Right mouse button')], + name='Mouse Button', default='left') + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Element') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + if self.property0 == "click": + layout.prop(self, 'property1') + layout.prop(self, 'property2') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_asset.py b/blender/arm/logicnode/canvas/LN_set_canvas_asset.py new file mode 100644 index 0000000000..bf734e709c --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_asset.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetAssetNode(ArmLogicTreeNode): + """Sets the asset of the given UI element.""" + bl_idname = 'LNCanvasSetAssetNode' + bl_label = 'Set Canvas Asset' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmStringSocket', 'Asset') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py b/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py new file mode 100644 index 0000000000..ad41a7ae8c --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetCheckBoxNode(ArmLogicTreeNode): + """Sets the state of the given UI checkbox.""" + bl_idname = 'LNCanvasSetCheckBoxNode' + bl_label = 'Set Canvas Checkbox' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmBoolSocket', 'Check') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_color.py b/blender/arm/logicnode/canvas/LN_set_canvas_color.py new file mode 100644 index 0000000000..f5f25aa419 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_color.py @@ -0,0 +1,41 @@ +from arm.logicnode.arm_nodes import * + + +class CanvasSetColorNode(ArmLogicTreeNode): + """Sets the selected color attribute of the given UI element. + + This node does not override theme colors, only colors of individual + elements are affected. + + @input Element: The name of the canvas element whose color to set. + @input Color: The color to set. + + @option Attribute: The color attribute to set by this node. Not all + attributes work for all canvas element types. If in doubt, see + [`CanvasScript.hx`](https://github.com/armory3d/armory/blob/main/Sources/armory/ui/Canvas.hx) + for details. + """ + bl_idname = 'LNCanvasSetColorNode' + bl_label = 'Set Canvas Color' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items=[ + ('color', 'Color', 'Set the element\'s color attribute'), + ('color_text', 'Text Color', 'Set the element\'s color_text attribute'), + ('color_hover', 'Hover Color', 'Set the element\'s color_hover attribute'), + ('color_press', 'Pressed Color', 'Set the element\'s color_press attribute'), + ('color_progress', 'Progress Color', 'Set the element\'s color_progress attribute'), + ], + name='Attribute', default='color', description='The color attribute to set by this node') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0', text='') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py b/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py new file mode 100644 index 0000000000..8140191672 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetInputTextNode(ArmLogicTreeNode): + """Sets the input text of the given UI element.""" + bl_idname = 'LNCanvasSetInputTextNode' + bl_label = 'Set Canvas Input Text' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmStringSocket', 'Text') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py b/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py new file mode 100644 index 0000000000..a4869c16f4 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetInputTextFocusNode(ArmLogicTreeNode): + """Sets the input text focus of the given UI element.""" + bl_idname = 'LNCanvasSetInputTextFocusNode' + bl_label = 'Set Canvas Input Text Focus' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmBoolSocket', 'Focus') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_location.py b/blender/arm/logicnode/canvas/LN_set_canvas_location.py new file mode 100644 index 0000000000..034021171d --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_location.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetLocationNode(ArmLogicTreeNode): + """Sets the location of the given UI element.""" + bl_idname = 'LNCanvasSetLocationNode' + bl_label = 'Set Canvas Location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py b/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py new file mode 100644 index 0000000000..b5d5908518 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetPBNode(ArmLogicTreeNode): + """Sets the value of the given UI progress bar.""" + bl_idname = 'LNCanvasSetPBNode' + bl_label = 'Set Canvas Progress Bar' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmIntSocket', 'At') + self.add_input('ArmIntSocket', 'Max') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py b/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py new file mode 100644 index 0000000000..ee9f6502bc --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetRotationNode(ArmLogicTreeNode): + """Sets the rotation of the given UI element.""" + bl_idname = 'LNCanvasSetRotationNode' + bl_label = 'Set Canvas Rotation' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmFloatSocket', 'Rad') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_scale.py b/blender/arm/logicnode/canvas/LN_set_canvas_scale.py new file mode 100644 index 0000000000..7afbf7abe0 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_scale.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetScaleNode(ArmLogicTreeNode): + """Sets the scale of the given UI element.""" + bl_idname = 'LNCanvasSetScaleNode' + bl_label = 'Set Canvas Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmIntSocket', 'Height') + self.add_input('ArmIntSocket', 'Width') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_slider.py b/blender/arm/logicnode/canvas/LN_set_canvas_slider.py new file mode 100644 index 0000000000..9988266b62 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_slider.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetSliderNode(ArmLogicTreeNode): + """Sets the value of the given UI slider.""" + bl_idname = 'LNCanvasSetSliderNode' + bl_label = 'Set Canvas Slider' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmFloatSocket', 'Float') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_text.py b/blender/arm/logicnode/canvas/LN_set_canvas_text.py new file mode 100644 index 0000000000..462b7ba2da --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_text.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetTextNode(ArmLogicTreeNode): + """Sets the text of the given UI element.""" + bl_idname = 'LNCanvasSetTextNode' + bl_label = 'Set Canvas Text' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmStringSocket', 'Text') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_visible.py b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py new file mode 100644 index 0000000000..dbe88b26bc --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class CanvasSetVisibleNode(ArmLogicTreeNode): + """Sets whether the given UI element is visibile.""" + bl_idname = 'LNCanvasSetVisibleNode' + bl_label = 'Set Canvas Visible' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmBoolSocket', 'Visible') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py new file mode 100644 index 0000000000..b6d016c419 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class SetGlobalCanvasFontSizeNode(ArmLogicTreeNode): + """Sets the font size of the entire UI Canvas.""" + bl_idname = 'LNSetGlobalCanvasFontSizeNode' + bl_label = 'Set Global Canvas Font Size' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Size', default_value=10) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py new file mode 100644 index 0000000000..3a86f84a10 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class SetGlobalCanvasScaleNode(ArmLogicTreeNode): + """Sets the scale of the entire UI Canvas.""" + bl_idname = 'LNSetGlobalCanvasScaleNode' + bl_label = 'Set Global Canvas Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Scale', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/canvas/__init__.py b/blender/arm/logicnode/canvas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/deprecated/LN_get_mouse_lock.py b/blender/arm/logicnode/deprecated/LN_get_mouse_lock.py new file mode 100644 index 0000000000..83cea8fafa --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_get_mouse_lock.py @@ -0,0 +1,27 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + + +@deprecated('Get Cursor State') +class GetMouseLockNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Get Cursor State' node instead.""" + bl_idname = 'LNGetMouseLockNode' + bl_label = 'Get Mouse Lock' + bl_description = "Please use the \"Get Cursor State\" node instead" + arm_version = 2 + arm_category = 'Input' + arm_section = 'mouse' + + def arm_init(self, context): + self.outputs.new('ArmBoolSocket', 'Is Locked') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNGetMouseLockNode', self.arm_version, 'LNGetCursorStateNode', 1, + in_socket_mapping={}, out_socket_mapping={0: 2} + ) diff --git a/blender/arm/logicnode/deprecated/LN_get_mouse_visible.py b/blender/arm/logicnode/deprecated/LN_get_mouse_visible.py new file mode 100644 index 0000000000..95c5d0f188 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_get_mouse_visible.py @@ -0,0 +1,30 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + + +@deprecated('Get Cursor State') +class GetMouseVisibleNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Get Cursor State' node instead.""" + bl_idname = 'LNGetMouseVisibleNode' + bl_label = 'Get Mouse Visible' + bl_description = "Please use the \"Get Cursor State\" node instead" + arm_category = 'Input' + arm_section = 'mouse' + arm_version = 2 + + def arm_init(self, context): + self.outputs.new('ArmBoolSocket', 'Is Visible') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + mainnode = node_tree.nodes.new('LNGetCursorStateNode') + secondnode = node_tree.nodes.new('LNNotNode') + node_tree.links.new(mainnode.outputs[2], secondnode.inputs[0]) + for link in self.outputs[0].links: + node_tree.links.new(secondnode.outputs[0], link.to_socket) + + return [mainnode, secondnode] diff --git a/blender/arm/logicnode/deprecated/LN_group_nodes.py b/blender/arm/logicnode/deprecated/LN_group_nodes.py new file mode 100644 index 0000000000..6c62f7a2b9 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_group_nodes.py @@ -0,0 +1,16 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +@deprecated('Group Input Node') +class GroupOutputNode(ArmLogicTreeNode): + """Sets the connected chain of nodes as a group of nodes.""" + bl_idname = 'LNGroupOutputNode' + bl_label = 'Group Nodes' + arm_category = 'Miscellaneous' + arm_section = 'group' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') diff --git a/blender/arm/logicnode/deprecated/LN_mouse_coords.py b/blender/arm/logicnode/deprecated/LN_mouse_coords.py new file mode 100644 index 0000000000..60b58cddcd --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_mouse_coords.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Get Cursor Location') +class MouseCoordsNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use 'Get Cursor Location' node and the 'Get Mouse Movement' node instead.""" + bl_idname = 'LNMouseCoordsNode' + bl_label = 'Mouse Coords' + bl_description = "Please use the \"Get Cursor Location\" and \"Get Mouse Movement\" nodes instead" + arm_category = 'Input' + arm_section = 'mouse' + arm_version = 2 + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'Coords') + self.add_output('ArmVectorSocket', 'Movement') + self.add_output('ArmIntSocket', 'Wheel') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + all_new_nodes = [] + if len(self.outputs[0].links) > 0: + # "coords": use the cursor coordinates + newmain = node_tree.nodes.new('LNGetCursorLocationNode') + new_secondary = node_tree.nodes.new('LNVectorNode') + node_tree.links.new(newmain.outputs[0], new_secondary.inputs[0]) + node_tree.links.new(newmain.outputs[1], new_secondary.inputs[1]) + for link in self.outputs[0].links: + node_tree.links.new(new_secondary.outputs[0], link.to_socket) + all_new_nodes += [newmain, new_secondary] + + if len(self.outputs[1].links) > 0 or len(self.outputs[2].links) > 0: + # "movement": use the mouse movement + # "wheel": use data from mouse movement as well + newmain = node_tree.nodes.new('LNGetMouseMovementNode') + all_new_nodes.append(newmain) + if len(self.outputs[1].links) > 0: + new_secondary = node_tree.nodes.new('LNVectorNode') + all_new_nodes.append(new_secondary) + node_tree.links.new(newmain.outputs[0], new_secondary.inputs[0]) + node_tree.links.new(newmain.outputs[1], new_secondary.inputs[1]) + for link in self.outputs[1].links: + node_tree.links.new(new_secondary.outputs[0], link.to_socket) + + for link in self.outputs[2].links: + node_tree.links.new(newmain.outputs[2], link.to_socket) + + return all_new_nodes diff --git a/blender/arm/logicnode/deprecated/LN_on_gamepad.py b/blender/arm/logicnode/deprecated/LN_on_gamepad.py new file mode 100644 index 0000000000..3de6a2842c --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_on_gamepad.py @@ -0,0 +1,62 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Gamepad') +class OnGamepadNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Gamepad' node instead.""" + bl_idname = 'LNOnGamepadNode' + bl_label = "On Gamepad" + bl_description = "Please use the \"Gamepad\" node instead" + arm_category = 'Input' + arm_section = 'gamepad' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('Down', 'Down', 'Down'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released')], + # ('Moved Left', 'Moved Left', 'Moved Left'), + # ('Moved Right', 'Moved Right', 'Moved Right'),], + name='', default='Started') + + property1: HaxeEnumProperty( + 'property1', + items = [('cross', 'cross / a', 'cross / a'), + ('circle', 'circle / b', 'circle / b'), + ('square', 'square / x', 'square / x'), + ('triangle', 'triangle / y', 'triangle / y'), + ('l1', 'l1', 'l1'), + ('r1', 'r1', 'r1'), + ('l2', 'l2', 'l2'), + ('r2', 'r2', 'r2'), + ('share', 'share', 'share'), + ('options', 'options', 'options'), + ('l3', 'l3', 'l3'), + ('r3', 'r3', 'r3'), + ('up', 'up', 'up'), + ('down', 'down', 'down'), + ('left', 'left', 'left'), + ('right', 'right', 'right'), + ('home', 'home', 'home'), + ('touchpad', 'touchpad', 'touchpad'),], + name='', default='cross') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_input('ArmIntSocket', 'Gamepad') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + "LNOnGamepadNode", self.arm_version, + "LNMergedGamepadNode", 1, + in_socket_mapping={0: 0}, out_socket_mapping={0: 0}, + property_mapping={"property0": "property0", "property1": "property1"} + ) diff --git a/blender/arm/logicnode/deprecated/LN_on_keyboard.py b/blender/arm/logicnode/deprecated/LN_on_keyboard.py new file mode 100644 index 0000000000..7769945189 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_on_keyboard.py @@ -0,0 +1,93 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Keyboard') +class OnKeyboardNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Keyboard' node instead.""" + bl_idname = 'LNOnKeyboardNode' + bl_label = "On Keyboard" + bl_description = "Please use the \"Keyboard\" node instead" + arm_category = 'Input' + arm_section = 'keyboard' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('Down', 'Down', 'Down'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released')], + name='', default='Started') + + property1: HaxeEnumProperty( + 'property1', + items = [('a', 'a', 'a'), + ('b', 'b', 'b'), + ('c', 'c', 'c'), + ('d', 'd', 'd'), + ('e', 'e', 'e'), + ('f', 'f', 'f'), + ('g', 'g', 'g'), + ('h', 'h', 'h'), + ('i', 'i', 'i'), + ('j', 'j', 'j'), + ('k', 'k', 'k'), + ('l', 'l', 'l'), + ('m', 'm', 'm'), + ('n', 'n', 'n'), + ('o', 'o', 'o'), + ('p', 'p', 'p'), + ('q', 'q', 'q'), + ('r', 'r', 'r'), + ('s', 's', 's'), + ('t', 't', 't'), + ('u', 'u', 'u'), + ('v', 'v', 'v'), + ('w', 'w', 'w'), + ('x', 'x', 'x'), + ('y', 'y', 'y'), + ('z', 'z', 'z'), + ('0', '0', '0'), + ('1', '1', '1'), + ('2', '2', '2'), + ('3', '3', '3'), + ('4', '4', '4'), + ('5', '5', '5'), + ('6', '6', '6'), + ('7', '7', '7'), + ('8', '8', '8'), + ('9', '9', '9'), + ('.', 'period', 'period'), + (',', 'comma', 'comma'), + ('space', 'space', 'space'), + ('backspace', 'backspace', 'backspace'), + ('tab', 'tab', 'tab'), + ('enter', 'enter', 'enter'), + ('shift', 'shift', 'shift'), + ('control', 'control', 'control'), + ('alt', 'alt', 'alt'), + ('escape', 'escape', 'escape'), + ('delete', 'delete', 'delete'), + ('back', 'back', 'back'), + ('up', 'up', 'up'), + ('right', 'right', 'right'), + ('left', 'left', 'left'), + ('down', 'down', 'down'),], + name='', default='space') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + "LNOnKeyboardNode", self.arm_version, + "LNMergedKeyboardNode", 1, + in_socket_mapping={}, out_socket_mapping={0: 0}, + property_mapping={"property0": "property0", "property1": "property1"} + ) diff --git a/blender/arm/logicnode/deprecated/LN_on_mouse.py b/blender/arm/logicnode/deprecated/LN_on_mouse.py new file mode 100644 index 0000000000..1ed967a59c --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_on_mouse.py @@ -0,0 +1,47 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Mouse') +class OnMouseNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Mouse' node instead.""" + bl_idname = 'LNOnMouseNode' + bl_label = "On Mouse" + bl_description = "Please use the \"Mouse\" node instead" + arm_category = 'Input' + arm_section = 'mouse' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('Down', 'Down', 'Down'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released'), + ('Moved', 'Moved', 'Moved')], + name='', default='Down') + property1: HaxeEnumProperty( + 'property1', + items = [('left', 'left', 'left'), + ('right', 'right', 'right'), + ('middle', 'middle', 'middle')], + name='', default='left') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + 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('LNMergedMouseNode') + + newnode.property0 = self.property0.lower() + newnode.property1 = self.property1 + + NodeReplacement.replace_output_socket(node_tree, self.outputs[0], newnode.outputs[0]) + + return newnode + diff --git a/blender/arm/logicnode/deprecated/LN_on_surface.py b/blender/arm/logicnode/deprecated/LN_on_surface.py new file mode 100644 index 0000000000..706c45c6a9 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_on_surface.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Surface') +class OnSurfaceNode(ArmLogicTreeNode): + """Deprecated. Is recommended to use the 'Surface' node instead.""" + bl_idname = 'LNOnSurfaceNode' + bl_label = 'On Surface' + bl_description = "Please use the \"Surface\" node instead" + arm_category = 'Input' + arm_section = 'surface' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('Touched', 'Touched', 'Touched'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released'), + ('Moved', 'Moved', 'Moved')], + name='', default='Touched') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/deprecated/LN_on_virtual_button.py b/blender/arm/logicnode/deprecated/LN_on_virtual_button.py new file mode 100644 index 0000000000..e2377d0b2a --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_on_virtual_button.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Virtual Button') +class OnVirtualButtonNode(ArmLogicTreeNode): + """Deprecated. Is recommended to use 'Virtual Button' node instead.""" + bl_idname = 'LNOnVirtualButtonNode' + bl_label = 'On Virtual Button' + bl_description = "Please use the \"Virtual Button\" node instead" + arm_category = 'Input' + arm_section = 'virtual' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('Down', 'Down', 'Down'), + ('Started', 'Started', 'Started'), + ('Released', 'Released', 'Released')], + name='', default='Started') + property1: HaxeStringProperty('property1', name='', default='button') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/deprecated/LN_pause_action.py b/blender/arm/logicnode/deprecated/LN_pause_action.py new file mode 100644 index 0000000000..b515ec0d60 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_pause_action.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Action Paused') +class PauseActionNode(ArmLogicTreeNode): + """Pauses the given action.""" + bl_idname = 'LNPauseActionNode' + bl_label = 'Pause Action' + bl_description = "Please use the \"Set Action Paused\" node instead" + arm_category = 'Animation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_pause_tilesheet.py b/blender/arm/logicnode/deprecated/LN_pause_tilesheet.py new file mode 100644 index 0000000000..38313026b4 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_pause_tilesheet.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Tilesheet Paused') +class PauseTilesheetNode(ArmLogicTreeNode): + """Pauses the given tilesheet action.""" + bl_idname = 'LNPauseTilesheetNode' + bl_label = 'Pause Tilesheet' + bl_description = "Please use the \"Set Tilesheet Paused\" node instead" + arm_category = 'Animation' + arm_section = 'tilesheet' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_pause_trait.py b/blender/arm/logicnode/deprecated/LN_pause_trait.py new file mode 100644 index 0000000000..b3aecd2c79 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_pause_trait.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Trait Paused') +class PauseTraitNode(ArmLogicTreeNode): + """Pauses the given trait.""" + bl_idname = 'LNPauseTraitNode' + bl_label = 'Pause Trait' + bl_description = "Please use the \"Set Trait Paused\" node instead" + arm_category = 'Trait' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Trait') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_play_action.py b/blender/arm/logicnode/deprecated/LN_play_action.py new file mode 100644 index 0000000000..5ac411c9b0 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_play_action.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Play Action From') +class PlayActionNode(ArmLogicTreeNode): + """Plays the given action.""" + bl_idname = 'LNPlayActionNode' + bl_label = 'Play Action' + bl_description = "Please use the \"Play Action From\" node instead" + arm_category = 'Animation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketAnimAction', 'Action') + self.add_input('ArmFloatSocket', 'Blend', default_value=0.2) + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Done') diff --git a/blender/arm/logicnode/deprecated/LN_quaternion.py b/blender/arm/logicnode/deprecated/LN_quaternion.py new file mode 100644 index 0000000000..b0c1de9689 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_quaternion.py @@ -0,0 +1,75 @@ +from arm.logicnode.arm_nodes import * +from mathutils import Vector + + +@deprecated(message='Do not use quaternion sockets') +class QuaternionNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNQuaternionNode' + bl_label = 'Quaternion' + bl_description = 'Create a quaternion variable (transported through a vector socket)' + arm_category = 'Variable' + arm_section = 'quaternions' + arm_version = 2 # deprecate + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Z') + self.add_input('ArmFloatSocket', 'W', default_value=1.0) + + self.add_output('ArmVectorSocket', 'Quaternion') + self.add_output('ArmVectorSocket', 'XYZ') + self.add_output('ArmVectorSocket', 'W') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + + newnodes = [] + + rawlinks = self.outputs[0].links + xyzlinks = self.outputs[1].links + wlinks = self.outputs[2].links + if len(rawlinks)>0 or len(xyzlinks)>0: + xyzcomb = node_tree.nodes.new('LNVectorNode') + newnodes.append(xyzcomb) + + xyzcomb.inputs[0].default_value = self.inputs[0].default_value + xyzcomb.inputs[1].default_value = self.inputs[1].default_value + xyzcomb.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[0]) + for link in self.inputs[1].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[1]) + for link in self.inputs[2].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[2]) + + for link in xyzlinks: + node_tree.links.new(xyzcomb.outputs[0], link.to_socket) + if len(rawlinks)>0: + rotnode = node_tree.nodes.new('LNRotationNode') + newnodes.append(rotnode) + rotnode.property0 = 'Quaternion' + rotnode.inputs[0].default_value = Vector( + (self.inputs[0].default_value, + self.inputs[1].default_value, + self.inputs[2].default_value)) + rotnode.inputs[1].default_value = self.inputs[3].default_value + node_tree.links.new(xyzcomb.outputs[0], rotnode.inputs[0]) + for link in self.inputs[3].links: # 0 or 1 + node_tree.links.new(link.from_socket, rotnode.inputs[1]) + for link in rawlinks: + node_tree.links.new(rotnode.outputs[0], link.to_socket) + + if len(self.inputs[3].links)>0: + fromval = self.inputs[3].links[0].from_socket + for link in self.outputs[2].links: + node_tree.links.new(fromval, link.to_socket) + else: + for link in self.outputs[2].links: + link.to_socket.default_value = self.inputs[3].default_value + + return newnodes diff --git a/blender/arm/logicnode/deprecated/LN_resume_action.py b/blender/arm/logicnode/deprecated/LN_resume_action.py new file mode 100644 index 0000000000..9af44ae07e --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_resume_action.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Action Paused') +class ResumeActionNode(ArmLogicTreeNode): + """Resumes the given action.""" + bl_idname = 'LNResumeActionNode' + bl_label = 'Resume Action' + bl_description = "Please use the \"Set Action Paused\" node instead" + arm_category = 'Animation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_resume_tilesheet.py b/blender/arm/logicnode/deprecated/LN_resume_tilesheet.py new file mode 100644 index 0000000000..b807dcc99e --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_resume_tilesheet.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Tilesheet Paused') +class ResumeTilesheetNode(ArmLogicTreeNode): + """Resumes the given tilesheet action.""" + bl_idname = 'LNResumeTilesheetNode' + bl_label = 'Resume Tilesheet' + bl_description = "Please use the \"Set Tilesheet Paused\" node instead" + arm_category = 'Animation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_resume_trait.py b/blender/arm/logicnode/deprecated/LN_resume_trait.py new file mode 100644 index 0000000000..89c15bdfc6 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_resume_trait.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Trait Paused') +class ResumeTraitNode(ArmLogicTreeNode): + """Resumes the given trait.""" + bl_idname = 'LNResumeTraitNode' + bl_label = 'Resume Trait' + bl_description = "Please use the \"Set Trait Paused\" node instead" + arm_category = 'Trait' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Trait') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_rotate_object_around_axis.py b/blender/arm/logicnode/deprecated/LN_rotate_object_around_axis.py new file mode 100644 index 0000000000..104f774bd3 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_rotate_object_around_axis.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Rotate Object') +class RotateObjectAroundAxisNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Rotate Object' node instead.""" + bl_idname = 'LNRotateObjectAroundAxisNode' + bl_label = 'Rotate Object Around Axis' + bl_description = "Please use the \"Rotate Object\" node instead" + arm_category = 'Transform' + arm_section = 'rotation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Axis', default_value=[0, 0, 1]) + self.add_input('ArmFloatSocket', '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( + 'LNRotateObjectAroundAxisNode', self.arm_version, 'LNRotateObjectNode', 1, + in_socket_mapping = {0:0, 1:1, 2:2, 3:3}, out_socket_mapping={0:0}, + property_defaults={'property0': "Angle Axies (Radians)"} + ) diff --git a/blender/arm/logicnode/deprecated/LN_scale_object.py b/blender/arm/logicnode/deprecated/LN_scale_object.py new file mode 100644 index 0000000000..29d5ae3d28 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_scale_object.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Object Scale') +class ScaleObjectNode(ArmLogicTreeNode): + """Deprecated. 'Use Set Object Scale' instead.""" + bl_idname = 'LNScaleObjectNode' + bl_label = 'Scale Object' + bl_description = "Please use the \"Set Object Scale\" node instead" + arm_category = 'Transform' + arm_section = 'scale' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Scale') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_separate_quaternion.py b/blender/arm/logicnode/deprecated/LN_separate_quaternion.py new file mode 100644 index 0000000000..a0529854d5 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_separate_quaternion.py @@ -0,0 +1,45 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated(message='Do not use quaternion sockets') +class SeparateQuaternionNode(ArmLogicTreeNode): + """Splits the given quaternion into X, Y, Z and W.""" + bl_idname = 'LNSeparateQuaternionNode' + bl_label = "Separate Quaternion (do not use: quaternions sockets have been phased out entirely)" + bl_description = "Separate a quaternion object (transported through a vector socket) into its four compoents." + arm_category = 'Math' + arm_section = 'quaternions' + arm_version = 2 # deprecate + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Quaternion') + + self.add_output('ArmFloatSocket', 'X') + self.add_output('ArmFloatSocket', 'Y') + self.add_output('ArmFloatSocket', 'Z') + self.add_output('ArmFloatSocket', 'W') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + newself = node_tree.nodes.new('LNSeparateRotationNode') + separator = node_tree.nodes.new('LNSeparateVectorNode') + + newself.property0 = 'Quaternion' + newself.property1 = 'Rad' # bogus + newself.property2 = 'XYZ' # bogus + + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, newself.inputs[0]) + node_tree.links.new(newself.outputs[0], separator.inputs[0]) + for link in self.outputs[0].links: + node_tree.links.new(separator.outputs[0], link.to_socket) + for link in self.outputs[1].links: + node_tree.links.new(separator.outputs[1], link.to_socket) + for link in self.outputs[2].links: + node_tree.links.new(separator.outputs[2], link.to_socket) + for link in self.outputs[3].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + return [newself, separator] diff --git a/blender/arm/logicnode/deprecated/LN_set_canvas_progress_bar_color.py b/blender/arm/logicnode/deprecated/LN_set_canvas_progress_bar_color.py new file mode 100644 index 0000000000..2d60d3e9b4 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_set_canvas_progress_bar_color.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Canvas Color') +class CanvasSetProgressBarColorNode(ArmLogicTreeNode): + """Sets the color of the given UI element.""" + bl_idname = 'LNCanvasSetProgressBarColorNode' + bl_label = 'Set Canvas Progress Bar Color' + arm_version = 2 + arm_category = 'Canvas' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmColorSocket', 'Color In', default_value=[1.0, 1.0, 1.0, 1.0]) + + 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() + + newnode = node_tree.nodes.new('LNCanvasSetColorNode') + + newnode.property0 = 'color_progress' + + NodeReplacement.replace_input_socket(node_tree, self.inputs[0], newnode.inputs[0]) + NodeReplacement.replace_input_socket(node_tree, self.inputs[1], newnode.inputs[1]) + NodeReplacement.replace_input_socket(node_tree, self.inputs[2], newnode.inputs[2]) + + NodeReplacement.replace_output_socket(node_tree, self.outputs[0], newnode.outputs[0]) + + return newnode diff --git a/blender/arm/logicnode/deprecated/LN_set_canvas_text_color.py b/blender/arm/logicnode/deprecated/LN_set_canvas_text_color.py new file mode 100644 index 0000000000..a7c2f61811 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_set_canvas_text_color.py @@ -0,0 +1,49 @@ +from arm.logicnode.arm_nodes import * +import arm.node_utils as node_utils + + +@deprecated('Set Canvas Color') +class CanvasSetTextColorNode(ArmLogicTreeNode): + """Sets the text color of the given UI element.""" + bl_idname = 'LNCanvasSetTextColorNode' + bl_label = 'Set Canvas Text Color' + arm_version = 2 + arm_category = 'Canvas' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Element') + self.add_input('ArmFloatSocket', 'R') + self.add_input('ArmFloatSocket', 'G') + self.add_input('ArmFloatSocket', 'B') + self.add_input('ArmFloatSocket', 'A') + + 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() + + newnode = node_tree.nodes.new('LNCanvasSetColorNode') + + newnode.property0 = 'color_text' + + NodeReplacement.replace_input_socket(node_tree, self.inputs[0], newnode.inputs[0]) + NodeReplacement.replace_input_socket(node_tree, self.inputs[1], newnode.inputs[1]) + + # We do not have a RGBA to Color node or a Vec4 node currently, + # so we cannot reconnect color inputs... So unfortunately we can only + # use the socket default colors here + node_utils.set_socket_default( + newnode.inputs[2], + [ + node_utils.get_socket_default(self.inputs[2]), + node_utils.get_socket_default(self.inputs[3]), + node_utils.get_socket_default(self.inputs[4]), + node_utils.get_socket_default(self.inputs[5]) + ] + ) + + NodeReplacement.replace_output_socket(node_tree, self.outputs[0], newnode.outputs[0]) + + return newnode diff --git a/blender/arm/logicnode/deprecated/LN_set_mouse_lock.py b/blender/arm/logicnode/deprecated/LN_set_mouse_lock.py new file mode 100644 index 0000000000..a27f42c31f --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_set_mouse_lock.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Cursor State') +class SetMouseLockNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Set Cursor State' node instead.""" + bl_idname = 'LNSetMouseLockNode' + bl_label = 'Set Mouse Lock' + bl_description = "Please use the \"Set Cursor State\" node instead" + arm_category = 'Input' + arm_section = 'mouse' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Lock') + 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( + 'LNSetMouseLockNode', self.arm_version, 'LNSetCursorStateNode', 1, + in_socket_mapping = {0:0, 1:1}, out_socket_mapping={0:0}, + property_defaults={'property0': "Lock"} + ) diff --git a/blender/arm/logicnode/deprecated/LN_set_mouse_visible.py b/blender/arm/logicnode/deprecated/LN_set_mouse_visible.py new file mode 100644 index 0000000000..73e40a5b68 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_set_mouse_visible.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Cursor State') +class ShowMouseNode(ArmLogicTreeNode): + """Deprecated. It is recommended to use the 'Set Cursor State' node instead.""" + bl_idname = 'LNShowMouseNode' + bl_label = "Set Mouse Visible" + bl_description = "Please use the \"Set Cursor State\" node instead" + arm_category = 'Input' + arm_section = 'mouse' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Show') + 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() + + if len(self.inputs[1].links) == 0: + # if the value is 'hard-coded', then use a simple replacement. Otherwise, use a Not node. + return NodeReplacement( + 'LNShowMouseNode', self.arm_version, 'LNSetCursorStateNode', 1, + in_socket_mapping={0:0}, out_socket_mapping={0:0}, # deliberately forgetting input 1 here: it is taken care of in the next line + input_defaults={1: not self.inputs[1].default_value}, + property_defaults={'property0': 'Hide'} + ) + + new_main = node_tree.nodes.new('LNSetCursorStateNode') + new_secondary = node_tree.nodes.new('LNNotNode') + new_main.property0 = 'Hide' + + node_tree.links.new(self.inputs[0].links[0].from_socket, new_main.inputs[0]) # Action in + node_tree.links.new(self.inputs[1].links[0].from_socket, new_secondary.inputs[0]) # Value in + node_tree.links.new(new_secondary.outputs[0], new_main.inputs[1]) # Value in, part 2 + + for link in self.outputs[0].links: + node_tree.links.new(new_main.outputs[0], link.to_socket) # Action out + + return [new_main, new_secondary] diff --git a/blender/arm/logicnode/deprecated/LN_set_object_material.py b/blender/arm/logicnode/deprecated/LN_set_object_material.py new file mode 100644 index 0000000000..5d01c13566 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_set_object_material.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Set Object Material Slot') +class SetMaterialNode(ArmLogicTreeNode): + """Sets the material of the given object.""" + bl_idname = 'LNSetMaterialNode' + bl_label = 'Set Object Material' + bl_description = "Please use the \"Set Object Material Slot\" node instead" + arm_category = 'Material' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Material') + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/deprecated/LN_surface_coords.py b/blender/arm/logicnode/deprecated/LN_surface_coords.py new file mode 100644 index 0000000000..f84f0ce963 --- /dev/null +++ b/blender/arm/logicnode/deprecated/LN_surface_coords.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +@deprecated('Get Touch Movement', 'Get Touch Location') +class SurfaceCoordsNode(ArmLogicTreeNode): + """Deprecated. Is recommended to use 'Get Touch Location' and 'Get Touch Movement' node instead.""" + bl_idname = 'LNSurfaceCoordsNode' + bl_label = 'Surface Coords' + bl_description = "Please use the \"Get Touch Movement\" and \"Get Touch Location\" nodes instead" + arm_category = 'Input' + arm_section = 'surface' + arm_version = 2 + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'Coords') + self.add_output('ArmVectorSocket', 'Movement') diff --git a/blender/arm/logicnode/deprecated/__init__.py b/blender/arm/logicnode/deprecated/__init__.py new file mode 100644 index 0000000000..5a501fdb62 --- /dev/null +++ b/blender/arm/logicnode/deprecated/__init__.py @@ -0,0 +1 @@ +"""dummy file to include the code fro the deprecated nodes""" diff --git a/blender/arm/logicnode/draw/LN_draw_Text_Area_string.py b/blender/arm/logicnode/draw/LN_draw_Text_Area_string.py new file mode 100644 index 0000000000..2eb93b619c --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_Text_Area_string.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class DrawTextAreaStringNode(ArmLogicTreeNode): + """Draws a string. + + @input Length: length of the text area string can be determined by the amount of lines desired or the amount of characters in a line. + @input Draw: Activate to draw the string on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input String: The string to draw as a text area. + @input Font File: The filename of the font (including the extension). + If empty and Zui is _enabled_, the default font is used. If empty + and Zui is _disabled_, nothing is rendered. + + @length: value according to specified property above. + @line Spacing: changes the separation between lines. + @input Font Size: The size of the font in pixels. + @input Color Font: The color of the string, supports alpha. + @input Color Background: The color background of the text area, supports alpha, if no color is wanted used alpha 0. + @input X/Y: Position of the string, in pixels from the top left corner. + + @see [`kha.graphics2.Graphics.drawString()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawString). + """ + bl_idname = 'LNDrawTextAreaStringNode' + bl_label = 'Draw Text Area String' + arm_section = 'draw' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Lines', 'Length of Lines', 'Length of Lines'), + ('Chars', 'Length of Characters', 'Chars'),], + name='', default='Lines') + + property1: HaxeEnumProperty( + 'property1', + items = [('TextLeft', 'Hor. Align. Left', 'Hor. Align. Left'), + ('TextCenter', 'Hor. Align. Center', 'Hor. Align. Center'), + ('TextRight', 'Hor. Align. Right', 'Hor. Align. Right'),], + name='', default='TextLeft') + + property2: HaxeEnumProperty( + 'property2', + items = [('TextTop', 'Ver. Align. Top', 'Ver. Align. Top'), + ('TextMiddle', 'Ver. Align. Middle', 'Ver. Align. Middle'), + ('TextBottom', 'Ver. Align. Bottom', 'Ver. Align. Bottom'),], + name='', default='TextTop') + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmStringSocket', 'String') + self.add_input('ArmStringSocket', 'Font File') + self.add_input('ArmIntSocket', 'Length', default_value=3) + self.add_input('ArmFloatSocket', 'Line Spacing', default_value=1.0) + self.add_input('ArmIntSocket', 'Font Size', default_value=16) + self.add_input('ArmColorSocket', 'Color Font', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmColorSocket', 'Color Background', default_value=[0.0, 0.0, 0.0, 1.0]) + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + layout.prop(self, 'property2') diff --git a/blender/arm/logicnode/draw/LN_draw_arc.py b/blender/arm/logicnode/draw/LN_draw_arc.py new file mode 100644 index 0000000000..0ca38d8e58 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_arc.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + + +class DrawArcNode(ArmLogicTreeNode): + """Draws an arc (part of a circle). + + @input Draw: Activate to draw the arc on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the arc. + @input Filled: Whether the arc is filled or only the outline is drawn. + @input Strength: The line strength if the arc is not filled. + @input Segments: How many line segments should be used to draw the + arc. 0 (default) = automatic. + @input Center X/Y: The position of the arc's center, in pixels from the top left corner. + @input Radius: The radius of the arc in pixels. + @input Start Angle/End Angle: The angles in radians where the + arc starts/ends, starting right of the arc's center. + @input Exterior Angle: Whether the angles describe an exterior angle. + + @output Out: Activated after the arc has been drawn. + + @see [`kha.graphics2.GraphicsExtension.drawArc()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#drawArc). + @see [`kha.graphics2.GraphicsExtension.fillArc()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#fillArc). + """ + bl_idname = 'LNDrawArcNode' + bl_label = 'Draw Arc' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', 'Segments') + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmFloatSocket', 'Radius', default_value=20.0) + self.add_input('ArmFloatSocket', 'Start Angle') + self.add_input('ArmFloatSocket', 'End Angle') + self.add_input('ArmBoolSocket', 'Exterior Angle', default_value=False) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_camera.py b/blender/arm/logicnode/draw/LN_draw_camera.py new file mode 100644 index 0000000000..5ec085f682 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_camera.py @@ -0,0 +1,68 @@ +from arm.logicnode.arm_nodes import * + +class DrawCameraNode(ArmLogicTreeNode): + """Renders the scene from the view of specified cameras and draws + the render targets to the screen. + + @input Start: Evaluate the inputs and start drawing the camera render targets. + @input Stop: Stops the rendering and drawing of the camera render targets. + @input Camera: The camera from which to render. + @input X/Y: Position where the camera's render target is drawn, in pixels from the top left corner. + @input Width/Height: Size of the camera's render target in pixels. + + @output On Start: Activated after the `Start` input has been activated. + @output On Stop: Activated after the `Stop` input has been activated. + """ + bl_idname = 'LNDrawCameraNode' + bl_label = 'Draw Camera' + arm_section = 'draw' + arm_version = 2 + min_inputs = 7 + + num_choices: IntProperty(default=0, min=0) + + def __init__(self): + super(DrawCameraNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_sockets() + + self.add_output('ArmNodeSocketAction', 'On Start') + self.add_output('ArmNodeSocketAction', 'On Stop') + + def add_sockets(self): + self.num_choices += 1 + self.add_input('ArmNodeSocketObject', 'Camera ' + str(self.num_choices)) + self.add_input('ArmIntSocket', 'X') + self.add_input('ArmIntSocket', 'Y') + self.add_input('ArmIntSocket', 'Width') + self.add_input('ArmIntSocket', 'Height') + + def remove_sockets(self): + if self.num_choices > 1: + for _ in range(5): + self.inputs.remove(self.inputs.values()[-1]) + self.num_choices -= 1 + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_call_func', text='Add Camera', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'add_sockets' + + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'remove_sockets' + if len(self.inputs) == self.min_inputs: + column.enabled = False + + 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/draw/LN_draw_camera_texture.py b/blender/arm/logicnode/draw/LN_draw_camera_texture.py new file mode 100644 index 0000000000..ba5d1ded0e --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_camera_texture.py @@ -0,0 +1,31 @@ +from arm.logicnode.arm_nodes import * + + +class DrawCameraTextureNode(ArmLogicTreeNode): + """Renders the scene from the view of a specified camera and draws + its render target to the diffuse texture of the given material. + + @input Start: Evaluate the inputs and start drawing the camera render target. + @input Stop: Stops the rendering and drawing of the camera render target. + @input Camera: The camera from which to render. + @input Object: Object of which to choose the material in the `Material Slot` input. + @input Material Slot: Index of the material slot of which the diffuse + texture is replaced with the camera's render target. + + @output On Start: Activated after the `Start` input has been activated. + @output On Stop: Activated after the `Stop` input has been activated. + """ + bl_idname = 'LNDrawCameraTextureNode' + bl_label = 'Draw Camera to Texture' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmNodeSocketObject', 'Camera') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmIntSocket', 'Material Slot') + + self.add_output('ArmNodeSocketAction', 'On Start') + self.add_output('ArmNodeSocketAction', 'On Stop') diff --git a/blender/arm/logicnode/draw/LN_draw_circle.py b/blender/arm/logicnode/draw/LN_draw_circle.py new file mode 100644 index 0000000000..d4a6c22a29 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_circle.py @@ -0,0 +1,37 @@ +from arm.logicnode.arm_nodes import * + + +class DrawCircleNode(ArmLogicTreeNode): + """Draws a circle. + + @input Draw: Activate to draw the circle on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the circle. + @input Filled: Whether the circle is filled or only the outline is drawn. + @input Strength: The line strength if the circle is not filled. + @input Segments: How many line segments should be used to draw the + circle. 0 (default) = automatic. + @input Center X/Y: The position of the circle's center, in pixels from the top left corner. + @input Radius: The radius of the circle in pixels. + + @output Out: Activated after the circle has been drawn. + + @see [`kha.graphics2.GraphicsExtension.drawCircle()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#drawCircle). + @see [`kha.graphics2.GraphicsExtension.fillCircle()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#fillCircle). + """ + bl_idname = 'LNDrawCircleNode' + bl_label = 'Draw Circle' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', 'Segments') + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmFloatSocket', 'Radius') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_curve.py b/blender/arm/logicnode/draw/LN_draw_curve.py new file mode 100644 index 0000000000..04cd75a78f --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_curve.py @@ -0,0 +1,39 @@ +from arm.logicnode.arm_nodes import * + + +class DrawCurveNode(ArmLogicTreeNode): + """Draws a cubic bezier curve with two control points. + + @input Draw: Activate to draw the curve on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the curve. + @input Strength: The line strength. + @input Segments: How many line segments should be used to draw the curve. + @input Start Point X/Y: The position of starting point of the curve, in pixels from the top left corner. + @input Control Point 1/2 X/Y: The position of control points of the curve, in pixels from the top left corner. + @input End Point X/Y: The position of end point of the curve, in pixels from the top left corner. + + @output Out: Activated after the curve has been drawn. + + @see [`kha.graphics2.GraphicsExtension.drawCubicBezier()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#drawCubicBezier). + """ + bl_idname = 'LNDrawCurveNode' + bl_label = 'Draw Curve' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', 'Segments', default_value=20) + self.add_input('ArmFloatSocket', 'Start Point X') + self.add_input('ArmFloatSocket', 'Start Point Y') + self.add_input('ArmFloatSocket', 'Control Point 1 X') + self.add_input('ArmFloatSocket', 'Control Point 1 Y') + self.add_input('ArmFloatSocket', 'Control Point 2 X') + self.add_input('ArmFloatSocket', 'Control Point 2 Y') + self.add_input('ArmFloatSocket', 'End Point X') + self.add_input('ArmFloatSocket', 'End Point Y') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_ellipse.py b/blender/arm/logicnode/draw/LN_draw_ellipse.py new file mode 100644 index 0000000000..30f0828432 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_ellipse.py @@ -0,0 +1,41 @@ +from arm.logicnode.arm_nodes import * + + +class DrawEllipseNode(ArmLogicTreeNode): + """Draws an ellipse. + + @input Draw: Activate to draw the ellipse on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the ellipse. + @input Filled: Whether the ellipse is filled or only the outline is drawn. + @input Strength: The line strength if the ellipse is not filled. + @input Segments: How many line segments should be used to draw the + ellipse. 0 (default) = automatic. + @input Center X/Y: The position of the ellipse's center in pixels. + @input Width: Width of the ellipse in pixels. + @input Height: Height of the ellipse in pixels. + @input Angle: Rotation angle in radians. Rotation is clockwise. + + @output Out: Activated after the circle has been drawn. + + @see [`kha.graphics2.GraphicsExtension.drawCircle()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#drawCircle). + @see [`kha.graphics2.GraphicsExtension.fillCircle()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#fillCircle). + """ + bl_idname = 'LNDrawEllipseNode' + bl_label = 'Draw Ellipse' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', 'Segments') + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmFloatSocket', 'Angle') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_image.py b/blender/arm/logicnode/draw/LN_draw_image.py new file mode 100644 index 0000000000..1575ba8656 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_image.py @@ -0,0 +1,52 @@ +from arm.logicnode.arm_nodes import * + + +class DrawImageNode(ArmLogicTreeNode): + """Draws an image. + + @input Draw: Activate to draw the image on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Image: The filename of the image. + @input Color: The color that the image's pixels are multiplied with. + @input Left/Center/Right: Horizontal anchor point of the image. + 0 = Left, 1 = Center, 2 = Right + @input Top/Middle/Bottom: Vertical anchor point of the image. + 0 = Top, 1 = Middle, 2 = Bottom + @input X/Y: Position of the anchor point in pixels. + @input Width/Height: Size of the image in pixels. + @input Angle: Rotation angle in radians. Image will be rotated cloclwiswe + at the anchor point. + + @output Out: Activated after the image has been drawn. + + @see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage). + """ + bl_idname = 'LNDrawImageNode' + bl_label = 'Draw Image' + arm_section = 'draw' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmStringSocket', 'Image File') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmIntSocket', '0/1/2 = Left/Center/Right', default_value=0) + self.add_input('ArmIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0) + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmFloatSocket', '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( + "LNDrawImageNode", self.arm_version, + "LNDrawImageNode", 2, + in_socket_mapping={0:0, 1:1, 2:2, 3:5, 4:6, 5:7, 6:8}, + out_socket_mapping={0:0}, + ) diff --git a/blender/arm/logicnode/draw/LN_draw_image_sequence.py b/blender/arm/logicnode/draw/LN_draw_image_sequence.py new file mode 100644 index 0000000000..8084b9a7e1 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_image_sequence.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class DrawImageSequenceNode(ArmLogicTreeNode): + """Draws a sequence of image (images changing over time). The file names + of images used in a sequence need to follow a certain pattern: + `.`, `` is an arbitrary filename + that must be constant for the entire sequence, `` + corresponds to the frame number of the image in the sequence. + `` is the file extension (without a period "."). + + Image file names for a valid 2-frame sequence would for example + look like this: + + ``` + myImage1.png + myImage2.png + ``` + + @input Start: Evaluate the image filenames and start the sequence. + If the sequence is currently running, nothing happens. If the + sequence has finished and `Loop` is false, this input restarts + the sequence. + @input Stop: Stops the sequence and its drawing. + @input Image File Prefix: See `` above. + @input Image File Extension: See `` above. + @input Color: The color that the pixels of the images are multiplied with. + @input X/Y: Position of the images, in pixels from the top left corner. + @input Width/Height: Size of the images in pixels. The images + grow towards the bottom right corner. + @input Start Index: The first `` of the sequence (inclusive). + @input End Index: The last `` of the sequence (inclusive). + @input Frame Duration: Duration of a frame in seconds. + @input Loop: Whether the sequence starts again from the first frame after the last frame. + @input Wait For Load: If true, start the sequence only after all + image files have been loaded. If false, the sequence starts immediately, + but images that are not yet loaded are not rendered. + + @output On Start: Activated after the sequence has started. This output + is influenced by the `Wait For Load` input. + @output On Stop: Activated if the sequence ends or the `Stop` input + is activated. This is not activated when the sequence restarts + due to looping. + """ + bl_idname = 'LNDrawImageSequenceNode' + bl_label = 'Draw Image Sequence' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmStringSocket', 'Image File Prefix') + self.add_input('ArmStringSocket', 'Image File Extension') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmIntSocket', 'Start Index') + self.add_input('ArmIntSocket', 'End Index', default_value=1) + self.add_input('ArmFloatSocket', 'Frame Duration', default_value=1.0) + self.add_input('ArmBoolSocket', 'Loop', default_value=True) + self.add_input('ArmBoolSocket', 'Wait For Load', default_value=True) + + self.add_output('ArmNodeSocketAction', 'On Start') + self.add_output('ArmNodeSocketAction', 'On Stop') diff --git a/blender/arm/logicnode/draw/LN_draw_line.py b/blender/arm/logicnode/draw/LN_draw_line.py new file mode 100644 index 0000000000..735b54cc90 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_line.py @@ -0,0 +1,31 @@ +from arm.logicnode.arm_nodes import * + + +class DrawLineNode(ArmLogicTreeNode): + """Draws a line. + + @input Draw: Activate to draw the line on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the line. + @input Strength: The line strength. + @input X1/Y1/X2/Y2: The position of line's two end points, in pixels from the top left corner. + + @output Out: Activated after the line has been drawn. + + @see [`kha.graphics2.Graphics.drawLine()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawLine). + """ + bl_idname = 'LNDrawLineNode' + bl_label = 'Draw Line' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', 'X1') + self.add_input('ArmIntSocket', 'Y1') + self.add_input('ArmIntSocket', 'X2') + self.add_input('ArmIntSocket', 'Y2') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_polygon.py b/blender/arm/logicnode/draw/LN_draw_polygon.py new file mode 100644 index 0000000000..f87b18b574 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_polygon.py @@ -0,0 +1,72 @@ +from arm.logicnode.arm_nodes import * + +class DrawPolygonNode(ArmLogicTreeNode): + """Draws a polygon. + + @input Draw: Activate to draw the polygon on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the polygon. + @input Filled: Whether the polygon is filled or only the outline is drawn. + @input Strength: The line strength if the polygon is not filled. + @input Origin X/Origin Y: The origin position of the polygon, in pixels + from the top left corner. This position is added to all other + points, so they are defined relative to this position. + @input Xn/Yn: The position of polygon's points, in pixels from `Origin X`/`Origin Y`. + + @output Out: Activated after the polygon has been drawn. + + @see [`kha.graphics2.GraphicsExtension.drawPolygon()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#drawPolygon). + @see [`kha.graphics2.GraphicsExtension.fillPolygon()`](http://kha.tech/api/kha/graphics2/GraphicsExtension.html#fillPolygon). + """ + bl_idname = 'LNDrawPolygonNode' + bl_label = 'Draw Polygon' + arm_section = 'draw' + arm_version = 2 + min_inputs = 6 + + num_choices: IntProperty(default=1, min=0) + + def __init__(self): + super(DrawPolygonNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmFloatSocket', 'Origin X') + self.add_input('ArmFloatSocket', 'Origin Y') + + self.add_output('ArmNodeSocketAction', 'Out') + + def add_sockets(self): + self.add_input('ArmFloatSocket', 'X' + str(self.num_choices)) + self.add_input('ArmFloatSocket', 'Y' + str(self.num_choices)) + self.num_choices += 1 + + def remove_sockets(self): + if self.num_choices > 1: + self.inputs.remove(self.inputs.values()[-1]) + self.inputs.remove(self.inputs.values()[-1]) + self.num_choices -= 1 + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_call_func', text='Add Point', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'add_sockets' + + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'remove_sockets' + if len(self.inputs) == self.min_inputs: + column.enabled = False + + 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/draw/LN_draw_rect.py b/blender/arm/logicnode/draw/LN_draw_rect.py new file mode 100644 index 0000000000..edb2e8ee46 --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_rect.py @@ -0,0 +1,55 @@ +from arm.logicnode.arm_nodes import * + + +class DrawRectNode(ArmLogicTreeNode): + """Draws a rectangle. + + @input Draw: Activate to draw the rectangle on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the rectangle. + @input Filled: Whether the rectangle is filled or only the outline is drawn. + @input Strength: The line strength if the rectangle is not filled. + @input Left/Center/Right: Horizontal anchor point of the rectangel. + 0 = Left, 1 = Center, 2 = Right + @input Top/Middle/Bottom: Vertical anchor point of the rectangel. + 0 = Top, 1 = Middle, 2 = Bottom + @input X/Y: Position of the anchor point in pixels. + @input Width/Height: Size of the rectangle in pixels. + @input Angle: Rotation angle in radians. Rectangle will be rotated cloclwiswe + at the anchor point. + + @output Out: Activated after the rectangle has been drawn. + + @see [`kha.graphics2.Graphics.drawRect()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawRect). + @see [`kha.graphics2.Graphics.fillRect()`](http://kha.tech/api/kha/graphics2/Graphics.html#fillRect). + """ + bl_idname = 'LNDrawRectNode' + bl_label = 'Draw Rect' + arm_section = 'draw' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmIntSocket', '0/1/2 = Left/Center/Right', default_value=0) + self.add_input('ArmIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0) + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmFloatSocket', '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( + "LNDrawRectNode", self.arm_version, + "LNDrawRectNode", 2, + in_socket_mapping={0:0, 1:1, 2:2, 3:3, 4:6, 5:7, 6:8, 7:9}, + out_socket_mapping={0:0}, + ) diff --git a/blender/arm/logicnode/draw/LN_draw_string.py b/blender/arm/logicnode/draw/LN_draw_string.py new file mode 100644 index 0000000000..00763a11fd --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_string.py @@ -0,0 +1,35 @@ +from arm.logicnode.arm_nodes import * + + +class DrawStringNode(ArmLogicTreeNode): + """Draws a string. + + @input Draw: Activate to draw the string on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input String: The string to draw. + @input Font File: The filename of the font (including the extension). + If empty and Zui is _enabled_, the default font is used. If empty + and Zui is _disabled_, nothing is rendered. + @input Font Size: The size of the font in pixels. + @input Color: The color of the string. + @input X/Y: Position of the string, in pixels from the top left corner. + + @output Out: Activated after the string has been drawn. + + @see [`kha.graphics2.Graphics.drawString()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawString). + """ + bl_idname = 'LNDrawStringNode' + bl_label = 'Draw String' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmStringSocket', 'String') + self.add_input('ArmStringSocket', 'Font File') + self.add_input('ArmIntSocket', 'Font Size', default_value=16) + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/LN_draw_to_material_image.py b/blender/arm/logicnode/draw/LN_draw_to_material_image.py new file mode 100644 index 0000000000..c1088f637c --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_to_material_image.py @@ -0,0 +1,39 @@ +from arm.logicnode.arm_nodes import * + + +class DrawToMaterialImageNode(ArmLogicTreeNode): + """Sets the image render target to draw to. The render target must be created using the `Create Render Target Node` first. + @seeNode Create Render Target Node + + @input In: Executes a 2D draw sequence connected to this node + + @input Object: Object whose material image should be drawn to. + Use `Get Scene Root` node to draw globally (all objects that share this image, and not per-object). + + @input Material: Material whose image to be drawn to. + + @input Node: Name of the parameter. + + @input Clear Image: Clear the image before drawing to it + + @output Out: Action output to be connected to other Draw Nodes + + @output Width: Width of the image + + @output Height: Height of the image + """ + bl_idname = 'LNDrawToMaterialImageNode' + bl_label = 'Draw To Material Image' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Material') + self.add_input('ArmStringSocket', 'Node') + self.add_input('ArmBoolSocket', 'Clear Image') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Width') + self.add_output('ArmIntSocket', 'Height') \ No newline at end of file diff --git a/blender/arm/logicnode/draw/LN_draw_triangle.py b/blender/arm/logicnode/draw/LN_draw_triangle.py new file mode 100644 index 0000000000..4b9bc87d7f --- /dev/null +++ b/blender/arm/logicnode/draw/LN_draw_triangle.py @@ -0,0 +1,35 @@ +from arm.logicnode.arm_nodes import * + + +class DrawTriangleNode(ArmLogicTreeNode): + """Draws a triangle. + + @input Draw: Activate to draw the triangle on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Color: The color of the triangle. + @input Filled: Whether the triangle is filled or only the outline is drawn. + @input Strength: The line strength if the triangle is not filled. + @input X/Y: Positions of the vertices of the triangle, in pixels from the top left corner. + + @output Out: Activated after the triangle has been drawn. + + @see [`kha.graphics2.Graphics.fillTriangle()`](http://kha.tech/api/kha/graphics2/Graphics.html#fillTriangle). + """ + bl_idname = 'LNDrawTriangleNode' + bl_label = 'Draw Triangle' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Draw') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmBoolSocket', 'Filled', default_value=False) + self.add_input('ArmFloatSocket', 'Strength', default_value=1.0) + self.add_input('ArmFloatSocket', 'X1') + self.add_input('ArmFloatSocket', 'Y1') + self.add_input('ArmFloatSocket', 'X2') + self.add_input('ArmFloatSocket', 'Y2') + self.add_input('ArmFloatSocket', 'X3') + self.add_input('ArmFloatSocket', 'Y3') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/draw/__init__.py b/blender/arm/logicnode/draw/__init__.py new file mode 100644 index 0000000000..47ed1ae328 --- /dev/null +++ b/blender/arm/logicnode/draw/__init__.py @@ -0,0 +1,3 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='draw') \ No newline at end of file diff --git a/blender/arm/logicnode/event/LN_on_application_state.py b/blender/arm/logicnode/event/LN_on_application_state.py new file mode 100644 index 0000000000..d38f8255f1 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_application_state.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class OnApplicationStateNode(ArmLogicTreeNode): + """Listens to different application state changes.""" + bl_idname = 'LNOnApplicationStateNode' + bl_label = 'On Application State' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'On Foreground') + self.add_output('ArmNodeSocketAction', 'On Background') + self.add_output('ArmNodeSocketAction', 'On Shutdown') diff --git a/blender/arm/logicnode/event/LN_on_event.py b/blender/arm/logicnode/event/LN_on_event.py new file mode 100644 index 0000000000..10c7d05562 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_event.py @@ -0,0 +1,70 @@ +from arm.logicnode.arm_nodes import * + +class OnEventNode(ArmLogicTreeNode): + """Activates the output when the given event is received. + + @seeNode Send Event to Object + @seeNode Send Global Event""" + bl_idname = 'LNOnEventNode' + bl_label = 'On Event' + arm_version = 2 + arm_section = 'custom' + + operators = { + 'init': 'Init', + 'update': 'Update', + 'custom': 'Custom' + } + + def set_mode(self, context): + if self.property1 != 'custom': + if len(self.inputs) > 1: + self.inputs.remove(self.inputs[0]) + else: + if len(self.inputs) < 2: + self.add_input('ArmNodeSocketAction', 'In') + self.inputs.move(1, 0) + + # Use a new property to preserve compatibility + property1: HaxeEnumProperty( + 'property1', + items=[ + ('init', 'Init', 'Assigns an Event listener at runtime'), + ('update', 'Update', 'Assigns an Event listener continuously'), + None, + ('custom', 'Custom', 'Assigns an Event listener everytime input is detected'), + ], + name='', + description='Chosen method for assigning an Event listener', + default='init', + update=set_mode + ) + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String In') + + self.add_output('ArmNodeSocketAction', 'Out') + + self.set_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'property1', text='') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.operators[self.property1]}' + + 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('LNOnEventNode') + + try: + newnode.inputs[0].default_value_raw = self["property0"] + except: + pass + + 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/event/LN_on_init.py b/blender/arm/logicnode/event/LN_on_init.py new file mode 100644 index 0000000000..b724625899 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_init.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class OnInitNode(ArmLogicTreeNode): + """Activates the output on the first frame of execution of the logic tree.""" + bl_idname = 'LNOnInitNode' + bl_label = 'On Init' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/event/LN_on_render2d.py b/blender/arm/logicnode/event/LN_on_render2d.py new file mode 100644 index 0000000000..8cf8ec2a9d --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_render2d.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class OnRender2DNode(ArmLogicTreeNode): + """Registers a 2D rendering callback to activate its output on each + frame after the frame has been drawn and other non-2D render callbacks + have been executed. + """ + bl_idname = 'LNOnRender2DNode' + bl_label = 'On Render2D' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/event/LN_on_timer.py b/blender/arm/logicnode/event/LN_on_timer.py new file mode 100644 index 0000000000..f5c98e3e60 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_timer.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class OnTimerNode(ArmLogicTreeNode): + """Activates the output when a given time elapsed (optionally repeating the timer). + + @input Duration: the time in seconds after which to activate the output + @input Repeat: whether to repeat the timer""" + bl_idname = 'LNOnTimerNode' + bl_label = 'On Timer' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Duration') + self.add_input('ArmBoolSocket', 'Repeat') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/event/LN_on_update.py b/blender/arm/logicnode/event/LN_on_update.py new file mode 100644 index 0000000000..c73ac019f5 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_update.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + +class OnUpdateNode(ArmLogicTreeNode): + """Activates the output on every frame. + + @option Update: (default) activates the output every frame. + @option Late Update: activates the output after all non-late updates are calculated. + @option Physics Pre-Update: activates the output before calculating the physics. + Only available when using a physics engine.""" + bl_idname = 'LNOnUpdateNode' + bl_label = 'On Update' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('Update', 'Update', 'Update'), + ('Late Update', 'Late Update', 'Late Update'), + ('Physics Pre-Update', 'Physics Pre-Update', 'Physics Pre-Update')], + name='On', default='Update') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/event/LN_send_event_to_object.py b/blender/arm/logicnode/event/LN_send_event_to_object.py new file mode 100644 index 0000000000..8470a7049e --- /dev/null +++ b/blender/arm/logicnode/event/LN_send_event_to_object.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class SendEventNode(ArmLogicTreeNode): + """Sends a event to the given object. + + @seeNode Send Event + @seeNode On Event + + @input Event: the identifier of the event + @input Object: the receiving object""" + bl_idname = 'LNSendEventNode' + bl_label = 'Send Event to Object' + arm_section = 'custom' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Event') + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/event/LN_send_global_event.py b/blender/arm/logicnode/event/LN_send_global_event.py new file mode 100644 index 0000000000..aba0ac0ef6 --- /dev/null +++ b/blender/arm/logicnode/event/LN_send_global_event.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + +class SendGlobalEventNode(ArmLogicTreeNode): + """Sends the given event to all objects in the scene. + + @seeNode Send Event to Object + @seeNode On Event + + @input Event: the identifier of the event""" + bl_idname = 'LNSendGlobalEventNode' + bl_label = 'Send Global Event' + arm_version = 1 + arm_section = 'custom' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Event') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/event/__init__.py b/blender/arm/logicnode/event/__init__.py new file mode 100644 index 0000000000..e92081d268 --- /dev/null +++ b/blender/arm/logicnode/event/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Event') +add_node_section(name='custom', category='Event') diff --git a/blender/arm/logicnode/input/LN_cursor_in_region.py b/blender/arm/logicnode/input/LN_cursor_in_region.py new file mode 100644 index 0000000000..f2b0b8ee28 --- /dev/null +++ b/blender/arm/logicnode/input/LN_cursor_in_region.py @@ -0,0 +1,37 @@ +from arm.logicnode.arm_nodes import * + +class CursorInRegionNode(ArmLogicTreeNode): + """Detect cursor in specific region. + + @input Center X/Y: The position of the center in pixels. + @input Width: Width of the region in pixels. + @input Height: Height of the region in pixels. + @input Angle: Rotation angle in radians. Rotation is clockwise. + + @output On Enter: Activated after the cursor enters the region. + @output On Exit: Activated after the cursor exits the region. + @output Is Inside: True if inside the region. False otherwise. + """ + bl_idname = 'LNCursorInRegionNode' + bl_label = 'Cursor In Region' + arm_section = 'mouse' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('rectangle', 'Rectangle', 'Rectangular region'), + ('ellipse', 'Ellipse', 'Elliptical or Circular region')], + name='', default='rectangle') + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmFloatSocket', 'Angle') + self.add_output('ArmNodeSocketAction', 'On Enter') + self.add_output('ArmNodeSocketAction', 'On Exit') + self.add_output('ArmBoolSocket', 'Is Inside') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/input/LN_gamepad.py b/blender/arm/logicnode/input/LN_gamepad.py new file mode 100644 index 0000000000..e64406082b --- /dev/null +++ b/blender/arm/logicnode/input/LN_gamepad.py @@ -0,0 +1,64 @@ +from arm.logicnode.arm_nodes import * + +class GamepadNode(ArmLogicTreeNode): + """Activates the output on the given gamepad event. + + @seeNode Gamepad Coords + + @input Gamepad: the ID of the gamepad. + + @option State: the state of the gamepad button to listen to. + @option Button: the gamepad button that should activate the output. + """ + bl_idname = 'LNMergedGamepadNode' + bl_label = 'Gamepad' + arm_version = 1 + arm_section = 'gamepad' + + property0: HaxeEnumProperty( + 'property0', + items = [('started', 'Started', 'The gamepad button starts to be pressed'), + ('down', 'Down', 'The gamepad button is pressed'), + ('released', 'Released', 'The gamepad button stops being pressed')], + # ('Moved Left', 'Moved Left', 'Moved Left'), + # ('Moved Right', 'Moved Right', 'Moved Right'),], + name='', default='down') + + property1: HaxeEnumProperty( + 'property1', + items = [('cross', 'cross / a', 'cross / a'), + ('circle', 'circle / b', 'circle / b'), + ('square', 'square / x', 'square / x'), + ('triangle', 'triangle / y', 'triangle / y'), + ('l1', 'l1 / lb', 'l1 / lb'), + ('r1', 'r1 / rb', 'r1 / rb'), + ('l2', 'l2 / lt', 'l2 / lt'), + ('r2', 'r2 / rt', 'r2 / rt'), + ('share', 'share', 'share'), + ('options', 'options', 'options'), + ('l3', 'l3', 'l3'), + ('r3', 'r3', 'r3'), + ('up', 'up', 'up'), + ('down', 'down', 'down'), + ('left', 'left', 'left'), + ('right', 'right', 'right'), + ('home', 'home', 'home'), + ('touchpad', 'touchpad', 'touchpad'),], + name='', default='cross') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + self.add_input('ArmIntSocket', 'Gamepad') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def draw_label(self) -> str: + inp_gamepad = self.inputs['Gamepad'] + if inp_gamepad.is_linked: + return f'{self.bl_label}: {self.property1}' + + return f'{self.bl_label} {inp_gamepad.default_value_raw}: {self.property1}' diff --git a/blender/arm/logicnode/input/LN_gamepad_coords.py b/blender/arm/logicnode/input/LN_gamepad_coords.py new file mode 100644 index 0000000000..2648479a09 --- /dev/null +++ b/blender/arm/logicnode/input/LN_gamepad_coords.py @@ -0,0 +1,22 @@ +from arm.logicnode.arm_nodes import * + +class GamepadCoordsNode(ArmLogicTreeNode): + """Returns the coordinates of the given gamepad. + + @seeNode Gamepad + + @input Gamepad: the ID of the gamepad.""" + bl_idname = 'LNGamepadCoordsNode' + bl_label = 'Gamepad Coords' + arm_version = 1 + arm_section = 'gamepad' + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'Left Stick') + self.add_output('ArmVectorSocket', 'Right Stick') + self.add_output('ArmVectorSocket', 'Left Movement') + self.add_output('ArmVectorSocket', 'Right Movement') + self.add_output('ArmFloatSocket', 'Left Trigger') + self.add_output('ArmFloatSocket', 'Right Trigger') + + self.add_input('ArmIntSocket', 'Gamepad') diff --git a/blender/arm/logicnode/input/LN_gamepad_sticks.py b/blender/arm/logicnode/input/LN_gamepad_sticks.py new file mode 100644 index 0000000000..59b9c84838 --- /dev/null +++ b/blender/arm/logicnode/input/LN_gamepad_sticks.py @@ -0,0 +1,52 @@ +from arm.logicnode.arm_nodes import * + +class GamepadSticksNode(ArmLogicTreeNode): + """Activates the output on the given gamepad event. + + @seeNode Gamepad Coords + + @input Gamepad: the ID of the gamepad. + + @option state: the state of the gamepad stick to listen to. + @option stick: the gamepad stick that should activate the output. + @option axis: the gamepad stick axis value + """ + bl_idname = 'LNGamepadSticksNode' + bl_label = 'Gamepad Sticks' + arm_version = 1 + arm_section = 'gamepad' + + property0: HaxeEnumProperty( + 'property0', + items = [('Started', 'Started', 'Started'), + ('Down', 'Down', 'Down'), + ('Released', 'Released', 'Released'),], + name='', default='Down') + + property1: HaxeEnumProperty( + 'property1', + items = [('LeftStick', 'LeftStick', 'LeftStick'), ('RightStick', 'RightStick', 'RightStick'),], + name='', default='LeftStick') + + property2: HaxeEnumProperty( + 'property2', + items = [('up', 'up', 'up'), + ('down', 'down', 'down'), + ('left', 'left', 'left'), + ('right', 'right', 'right'), + ('up-left', 'up-left', 'up-left'), + ('up-right', 'up-right', 'up-right'), + ('down-left', 'down-left', 'down-left'), + ('down-right', 'down-right', 'down-right'),], + name='', default='up') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + self.add_input('ArmIntSocket', 'Gamepad') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + layout.prop(self, 'property2') \ No newline at end of file diff --git a/blender/arm/logicnode/input/LN_get_cursor_location.py b/blender/arm/logicnode/input/LN_get_cursor_location.py new file mode 100644 index 0000000000..6e6eedacf4 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_cursor_location.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetCursorLocationNode(ArmLogicTreeNode): + """Returns the mouse cursor location in screen coordinates (pixels).""" + bl_idname = 'LNGetCursorLocationNode' + bl_label = 'Get Cursor Location' + arm_section = 'mouse' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'X') + self.add_output('ArmIntSocket', 'Y') + self.add_output('ArmIntSocket', 'Inverted X') + self.add_output('ArmIntSocket', 'Inverted Y') diff --git a/blender/arm/logicnode/input/LN_get_cursor_state.py b/blender/arm/logicnode/input/LN_get_cursor_state.py new file mode 100644 index 0000000000..0d1c7c8f1f --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_cursor_state.py @@ -0,0 +1,22 @@ +import bpy +from bpy.props import * +from bpy.types import Node, NodeSocket +from arm.logicnode.arm_nodes import * + +class GetCursorStateNode(ArmLogicTreeNode): + """Returns the state of the mouse cursor. + + @seeNode Set Cursor State + + @output Is Hidden Locked: `true` if the mouse cursor is both hidden and locked. + @output Is Hidden: `true` if the mouse cursor is hidden. + @output Is Locked: `true` if the mouse cursor is locked.""" + bl_idname = 'LNGetCursorStateNode' + bl_label = 'Get Cursor State' + arm_section = 'mouse' + arm_version = 1 + + def arm_init(self, context): + self.outputs.new('ArmBoolSocket', 'Is Hidden Locked') + self.outputs.new('ArmBoolSocket', 'Is Hidden') + self.outputs.new('ArmBoolSocket', 'Is Locked') 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 0000000000..08fd5fd033 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_gamepad_started.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetGamepadStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetGamepadStartedNode' + bl_label = 'Get Gamepad Started' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmIntSocket', 'Index') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmStringSocket', '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 0000000000..5368a9d3cd --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_input_map_key.py @@ -0,0 +1,14 @@ +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 arm_init(self, context): + self.add_input('ArmStringSocket', 'Input Map') + self.add_input('ArmStringSocket', 'Key') + + self.add_output('ArmFloatSocket', 'Scale', default_value = 1.0) + self.add_output('ArmFloatSocket', 'Deadzone') 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 0000000000..1810c6e0ed --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_keyboard_started.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetKeyboardStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetKeyboardStartedNode' + bl_label = 'Get Keyboard Started' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmStringSocket', 'Key') diff --git a/blender/arm/logicnode/input/LN_get_mouse_movement.py b/blender/arm/logicnode/input/LN_get_mouse_movement.py new file mode 100644 index 0000000000..fbef5311cd --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_mouse_movement.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + + +class GetMouseMovementNode(ArmLogicTreeNode): + """Get the movement coordinates of the mouse and the mouse wheel delta. + The multiplied output variants default to -1 to invert the values.""" + bl_idname = 'LNGetMouseMovementNode' + bl_label = 'Get Mouse Movement' + arm_section = 'mouse' + arm_version = 1 + + def arm_init(self, context): + + self.add_input('ArmFloatSocket', 'X Multiplier', default_value=-1.0) + self.add_input('ArmFloatSocket', 'Y Multiplier', default_value=-1.0) + self.add_input('ArmFloatSocket', 'Wheel Delta Multiplier', default_value=-1.0) + + self.add_output('ArmFloatSocket', 'X') + self.add_output('ArmFloatSocket', 'Y') + self.add_output('ArmFloatSocket', 'Multiplied X') + self.add_output('ArmFloatSocket', 'Multiplied Y') + self.add_output('ArmIntSocket', 'Wheel Delta') + self.add_output('ArmFloatSocket', 'Multiplied Wheel Delta') 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 0000000000..9a6fce1f3f --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_mouse_started.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetMouseStartedNode(ArmLogicTreeNode): + """.""" + bl_idname = 'LNGetMouseStartedNode' + bl_label = 'Get Mouse Started' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmStringSocket', 'Button') diff --git a/blender/arm/logicnode/input/LN_get_touch_location.py b/blender/arm/logicnode/input/LN_get_touch_location.py new file mode 100644 index 0000000000..3aea57b68f --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_touch_location.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetTouchLocationNode(ArmLogicTreeNode): + """Returns the location of the last touch event in screen coordinates (pixels).""" + bl_idname = 'LNGetTouchLocationNode' + bl_label = 'Get Touch Location' + arm_section = 'surface' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'X') + self.add_output('ArmIntSocket', 'Y') + self.add_output('ArmIntSocket', 'Inverted X') + self.add_output('ArmIntSocket', 'Inverted Y') diff --git a/blender/arm/logicnode/input/LN_get_touch_movement.py b/blender/arm/logicnode/input/LN_get_touch_movement.py new file mode 100644 index 0000000000..8c37c844e1 --- /dev/null +++ b/blender/arm/logicnode/input/LN_get_touch_movement.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class GetTouchMovementNode(ArmLogicTreeNode): + """Returns the movement values of the current touch event.""" + bl_idname = 'LNGetTouchMovementNode' + bl_label = 'Get Touch Movement' + arm_section = 'surface' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'X Multiplier', default_value=-1.0) + self.add_input('ArmFloatSocket', 'Y Multiplier', default_value=-1.0) + + self.add_output('ArmFloatSocket', 'X') + self.add_output('ArmFloatSocket', 'Y') + self.add_output('ArmFloatSocket', 'Multiplied X') + self.add_output('ArmFloatSocket', 'Multiplied Y') diff --git a/blender/arm/logicnode/input/LN_keyboard.py b/blender/arm/logicnode/input/LN_keyboard.py new file mode 100644 index 0000000000..50504e820c --- /dev/null +++ b/blender/arm/logicnode/input/LN_keyboard.py @@ -0,0 +1,89 @@ +from arm.logicnode.arm_nodes import * + +class KeyboardNode(ArmLogicTreeNode): + """Activates the output on the given keyboard button event.""" + bl_idname = 'LNMergedKeyboardNode' + bl_label = 'Keyboard' + arm_section = 'keyboard' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('started', 'Started', 'The keyboard button starts to be pressed'), + ('down', 'Down', 'The keyboard button is pressed'), + ('released', 'Released', 'The keyboard button stops being pressed')], + name='', default='down') + + property1: HaxeEnumProperty( + 'property1', + items = [('a', 'a', 'a'), + ('b', 'b', 'b'), + ('c', 'c', 'c'), + ('d', 'd', 'd'), + ('e', 'e', 'e'), + ('f', 'f', 'f'), + ('g', 'g', 'g'), + ('h', 'h', 'h'), + ('i', 'i', 'i'), + ('j', 'j', 'j'), + ('k', 'k', 'k'), + ('l', 'l', 'l'), + ('m', 'm', 'm'), + ('n', 'n', 'n'), + ('o', 'o', 'o'), + ('p', 'p', 'p'), + ('q', 'q', 'q'), + ('r', 'r', 'r'), + ('s', 's', 's'), + ('t', 't', 't'), + ('u', 'u', 'u'), + ('v', 'v', 'v'), + ('w', 'w', 'w'), + ('x', 'x', 'x'), + ('y', 'y', 'y'), + ('z', 'z', 'z'), + ('0', '0', '0'), + ('1', '1', '1'), + ('2', '2', '2'), + ('3', '3', '3'), + ('4', '4', '4'), + ('5', '5', '5'), + ('6', '6', '6'), + ('7', '7', '7'), + ('8', '8', '8'), + ('9', '9', '9'), + ('.', 'period', 'period'), + (',', 'comma', 'comma'), + ('space', 'space', 'space'), + ('backspace', 'backspace', 'backspace'), + ('tab', 'tab', 'tab'), + ('enter', 'enter', 'enter'), + ('shift', 'shift', 'shift'), + ('control', 'control', 'control'), + ('alt', 'alt', 'alt'), + ('capslock', 'capslock', 'capslock'), + ('escape', 'escape', 'escape'), + ('delete', 'delete', 'delete'), + ('back', 'back', 'back'), + ('up', 'up', 'up'), + ('right', 'right', 'right'), + ('left', 'left', 'left'), + ('down', 'down', 'down'),], + name='', default='space') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property1}' + + 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/input/LN_mouse.py b/blender/arm/logicnode/input/LN_mouse.py new file mode 100644 index 0000000000..1b4953cb04 --- /dev/null +++ b/blender/arm/logicnode/input/LN_mouse.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + + +class MouseNode(ArmLogicTreeNode): + """Activates the output on the given mouse event.""" + bl_idname = 'LNMergedMouseNode' + bl_label = 'Mouse' + arm_section = 'mouse' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('started', 'Started', 'The mouse button begins to be pressed'), + ('down', 'Down', 'The mouse button is pressed'), + ('released', 'Released', 'The mouse button stops being pressed'), + ('moved', 'Moved', 'Moved')], + name='', default='down') + property1: HaxeEnumProperty( + 'property1', + items = [('left', 'Left', 'Left mouse button'), + ('middle', 'Middle', 'Middle mouse button'), + ('right', 'Right', 'Right mouse button'), + ('side1', 'Side 1', 'Side 1 mouse button'), + ('side2', 'Side 2', 'Side 2 mouse button')], + name='', default='left') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property1}' + + 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/input/LN_on_input_map.py b/blender/arm/logicnode/input/LN_on_input_map.py new file mode 100644 index 0000000000..c851eefeed --- /dev/null +++ b/blender/arm/logicnode/input/LN_on_input_map.py @@ -0,0 +1,15 @@ +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 arm_init(self, context): + self.add_input('ArmStringSocket', 'Input Map') + + self.add_output('ArmNodeSocketAction', 'Started') + self.add_output('ArmNodeSocketAction', 'Released') + self.add_output('ArmFloatSocket', 'Value') + self.add_output('ArmStringSocket', 'Key Pressed') diff --git a/blender/arm/logicnode/input/LN_on_swipe.py b/blender/arm/logicnode/input/LN_on_swipe.py new file mode 100644 index 0000000000..d72b502981 --- /dev/null +++ b/blender/arm/logicnode/input/LN_on_swipe.py @@ -0,0 +1,98 @@ +from arm.logicnode.arm_nodes import * + +# Custom class for add output parameters (in 4 directions) +class NodeAddOutputButton(bpy.types.Operator): + """Add 4 States""" + bl_idname = 'arm.add_output_4_parameters' + bl_label = 'Add 4 States' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='ArmDynamicSocket') + name_format: StringProperty(name='Name Format', default='Output {0}') + index_name_offset: IntProperty(name='Index Name Offset', default=0) + + # Get name State + def get_name_state(self, id, min_outputs): + states = ['UP', 'DOWN', 'LEFT', 'RIGHT', 'UP-LEFT', 'UP-RIGHT', 'DOWN-LEFT', 'DOWN-RIGHT'] + if ((id - min_outputs) < len(states)): return states[id - min_outputs] + return '' + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + outs = node.outputs + outs.new('ArmBoolSocket', self.get_name_state(len(outs), node.min_outputs)) + outs.new('ArmBoolSocket', self.get_name_state(len(outs), node.min_outputs)) + outs.new('ArmBoolSocket', self.get_name_state(len(outs), node.min_outputs)) + outs.new('ArmBoolSocket', self.get_name_state(len(outs), node.min_outputs)) + return{'FINISHED'} + +# Custom class for remove output parameters (in 4 directions) +class NodeRemoveOutputButton(bpy.types.Operator): + """Remove 4 last states""" + bl_idname = 'arm.remove_output_4_parameters' + bl_label = 'Remove 4 States' + bl_options = {'UNDO', 'INTERNAL'} + node_index: StringProperty(name='Node Index', default='') + + def execute(self, context): + global array_nodes + node = array_nodes[self.node_index] + outs = node.outputs + min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs + if len(outs) > min_outs: + for i in range(4): + outs.remove(outs.values()[-1]) + return{'FINISHED'} + +# Class SwipeNode +class OnSwipeNode(ArmLogicTreeNode): + """Activates the output on the given swipe event.""" + bl_idname = 'LNOnSwipeNode' + bl_label = 'On Swipe' + arm_version = 1 + arm_section = 'Input' + min_outputs = 4 + max_outputs = 12 + + def __init__(self): + super(OnSwipeNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Time', default_value=0.15) + self.add_input('ArmIntSocket', 'Min Length (px)', default_value=100) + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmVectorSocket', 'Direction') + self.add_output('ArmIntSocket', 'Length (px)') + self.add_output('ArmIntSocket', 'Angle (0-360)') + + # Draw node buttons + def draw_buttons(self, context, layout): + row = layout.row(align=True) + column = row.column(align=True) + # Button add output + op = column.operator('arm.add_output_4_parameters', text='Add 4 States', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + # Disable/Enabled button + if (len(self.outputs) == self.max_outputs): + column.enabled = False + # Button remove output + column = row.column(align=True) + op2 = column.operator('arm.remove_output_4_parameters', text='', icon='X', emboss=True) + op2.node_index = str(id(self)) + # Disable/Enabled button + if (len(self.outputs) == self.min_outputs): + column.enabled = False + + @classmethod + def on_register(cls): + bpy.utils.register_class(NodeAddOutputButton) + bpy.utils.register_class(NodeRemoveOutputButton) + super().on_register() + + @classmethod + def on_unregister(cls): + super().on_unregister() + bpy.utils.unregister_class(NodeRemoveOutputButton) + bpy.utils.unregister_class(NodeAddOutputButton) diff --git a/blender/arm/logicnode/input/LN_on_tap_screen.py b/blender/arm/logicnode/input/LN_on_tap_screen.py new file mode 100644 index 0000000000..ac7cdb5510 --- /dev/null +++ b/blender/arm/logicnode/input/LN_on_tap_screen.py @@ -0,0 +1,30 @@ +from arm.logicnode.arm_nodes import * + + +class OnTapScreen(ArmLogicTreeNode): + """Activates the output on tap screen event. + + @input Duration: touching time + @input Interval: interval between taps + @input Repeat: repetitions amount to validate + + @output Done: the sequence success + @output Fail: the the sequence failure + @output Tap Number: number of the last tap + @output Coords: the coordinates of the last tap + """ + bl_idname = 'LNOnTapScreen' + bl_label = 'On Tap Screen' + arm_section = 'Input' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Duration', default_value=0.3) + self.add_input('ArmFloatSocket', 'Interval', default_value=0.0) + self.add_input('ArmIntSocket', 'Repeat', default_value=2) + + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmNodeSocketAction', 'Fail') + self.add_output('ArmNodeSocketAction', 'Tap') + self.add_output('ArmIntSocket', 'Tap Number') + self.add_output('ArmVectorSocket', 'Coords') 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 0000000000..8da8562b3f --- /dev/null +++ b/blender/arm/logicnode/input/LN_remove_input_map_key.py @@ -0,0 +1,14 @@ +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 arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Input Map') + self.add_input('ArmStringSocket', 'Key') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/input/LN_sensor_coords.py b/blender/arm/logicnode/input/LN_sensor_coords.py new file mode 100644 index 0000000000..a92f38f07e --- /dev/null +++ b/blender/arm/logicnode/input/LN_sensor_coords.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class SensorCoordsNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNSensorCoordsNode' + bl_label = 'Sensor Coords' + arm_section = 'sensor' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'Coords') diff --git a/blender/arm/logicnode/input/LN_set_cursor_state.py b/blender/arm/logicnode/input/LN_set_cursor_state.py new file mode 100644 index 0000000000..0c8c2d6e27 --- /dev/null +++ b/blender/arm/logicnode/input/LN_set_cursor_state.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + +class SetCursorStateNode(ArmLogicTreeNode): + """Sets the state of the mouse cursor. + + @seeNode Get Cursor State + + @option Hide Locked: hide and lock or unhide and unlock the mouse cursor. + @option Hide: hide/unhide the mouse cursor. + @option Lock: lock/unlock the mouse cursor. + """ + bl_idname = 'LNSetCursorStateNode' + bl_label = 'Set Cursor State' + arm_section = 'mouse' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('hide locked', 'Hide Locked', 'The mouse cursor is hidden and locked'), + ('hide', 'Hide', 'The mouse cursor is hidden'), + ('lock', 'Lock', 'The mouse cursor is locked'), + ], + name='', default='hide locked') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'State') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + 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 0000000000..75997d7d46 --- /dev/null +++ b/blender/arm/logicnode/input/LN_set_input_map_key.py @@ -0,0 +1,27 @@ +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: HaxeEnumProperty( + 'property0', + items = [('keyboard', 'Keyboard', 'Keyboard input'), + ('mouse', 'Mouse', 'Mouse input'), + ('gamepad', 'Gamepad', 'Gamepad input')], + name='', default='keyboard') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Input Map') + self.add_input('ArmStringSocket', 'Key') + self.add_input('ArmFloatSocket', 'Scale', default_value=1.0) + self.add_input('ArmFloatSocket', 'Deadzone') + self.add_input('ArmIntSocket', 'Index') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/input/LN_touch.py b/blender/arm/logicnode/input/LN_touch.py new file mode 100644 index 0000000000..f6ff0593c0 --- /dev/null +++ b/blender/arm/logicnode/input/LN_touch.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class SurfaceNode(ArmLogicTreeNode): + """Activates the output on the given touch event.""" + bl_idname = 'LNMergedSurfaceNode' + bl_label = 'Touch' + arm_section = 'surface' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('started', 'Started', 'The screen surface starts to be touched'), + ('down', 'Down', 'The screen surface is touched'), + ('released', 'Released', 'The screen surface stops being touched'), + ('moved', 'Moved', 'Moved')], + name='', default='down') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/input/LN_touch_in_region.py b/blender/arm/logicnode/input/LN_touch_in_region.py new file mode 100644 index 0000000000..c65d353d9e --- /dev/null +++ b/blender/arm/logicnode/input/LN_touch_in_region.py @@ -0,0 +1,37 @@ +from arm.logicnode.arm_nodes import * + +class TouchInRegionNode(ArmLogicTreeNode): + """Detect touch in specific region. + + @input Center X/Y: The position of the center in pixels. + @input Width: Width of the region in pixels. + @input Height: Height of the region in pixels. + @input Angle: Rotation angle in radians. Rotation is clockwise. + + @output On Enter: Activated after the cursor enters the region. + @output On Exit: Activated after the cursor exits the region. + @output Is Inside: True if inside the region. False otherwise. + """ + bl_idname = 'LNTouchInRegionNode' + bl_label = 'Touch In Region' + arm_section = 'surface' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('rectangle', 'Rectangle', 'Rectangular region'), + ('ellipse', 'Ellipse', 'Elliptical or Circular region')], + name='', default='rectangle') + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmFloatSocket', 'Width') + self.add_input('ArmFloatSocket', 'Height') + self.add_input('ArmFloatSocket', 'Angle') + self.add_output('ArmNodeSocketAction', 'On Enter') + self.add_output('ArmNodeSocketAction', 'On Exit') + self.add_output('ArmBoolSocket', 'Is Inside') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/input/LN_virtual_button.py b/blender/arm/logicnode/input/LN_virtual_button.py new file mode 100644 index 0000000000..3850a26eeb --- /dev/null +++ b/blender/arm/logicnode/input/LN_virtual_button.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + +class VirtualButtonNode(ArmLogicTreeNode): + """Activates the output on the given virtual button event.""" + bl_idname = 'LNMergedVirtualButtonNode' + bl_label = 'Virtual Button' + arm_section = 'virtual' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('started', 'Started', 'The virtual button starts to be pressed'), + ('down', 'Down', 'The virtual button is pressed'), + ('released', 'Released', 'The virtual button stops being pressed')], + name='', default='down') + property1: HaxeStringProperty('property1', name='', default='button') + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmBoolSocket', 'State') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property1}' diff --git a/blender/arm/logicnode/input/__init__.py b/blender/arm/logicnode/input/__init__.py new file mode 100644 index 0000000000..c8c68cf61b --- /dev/null +++ b/blender/arm/logicnode/input/__init__.py @@ -0,0 +1,8 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='keyboard', category='Input') +add_node_section(name='mouse', category='Input') +add_node_section(name='gamepad', category='Input') +add_node_section(name='surface', category='Input') +add_node_section(name='sensor', category='Input') +add_node_section(name='virtual', category='Input') diff --git a/blender/arm/logicnode/light/LN_set_area_light_size.py b/blender/arm/logicnode/light/LN_set_area_light_size.py new file mode 100644 index 0000000000..1ed53494ed --- /dev/null +++ b/blender/arm/logicnode/light/LN_set_area_light_size.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetAreaLightSizeNode(ArmLogicTreeNode): + """Sets the size of the given area light.""" + bl_idname = 'LNSetAreaLightSizeNode' + bl_label = 'Set Area Light Size' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Light') + self.add_input('ArmFloatSocket', 'Size X') + self.add_input('ArmFloatSocket', 'Size Y') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/light/LN_set_light_color.py b/blender/arm/logicnode/light/LN_set_light_color.py new file mode 100644 index 0000000000..5af7cde77b --- /dev/null +++ b/blender/arm/logicnode/light/LN_set_light_color.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetLightColorNode(ArmLogicTreeNode): + """Sets the color of the given light.""" + bl_idname = 'LNSetLightColorNode' + bl_label = 'Set Light Color' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Light') + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/light/LN_set_light_strength.py b/blender/arm/logicnode/light/LN_set_light_strength.py new file mode 100644 index 0000000000..bc9fd67e19 --- /dev/null +++ b/blender/arm/logicnode/light/LN_set_light_strength.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetLightStrengthNode(ArmLogicTreeNode): + """Sets the strenght of the given light.""" + bl_idname = 'LNSetLightStrengthNode' + bl_label = 'Set Light Strength' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Light') + self.add_input('ArmFloatSocket', 'Strength', default_value=250) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/light/LN_set_spot_light_blend.py b/blender/arm/logicnode/light/LN_set_spot_light_blend.py new file mode 100644 index 0000000000..887e014f19 --- /dev/null +++ b/blender/arm/logicnode/light/LN_set_spot_light_blend.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetSpotLightBlendNode(ArmLogicTreeNode): + """Sets the blend of the given spot light.""" + bl_idname = 'LNSetSpotLightBlendNode' + bl_label = 'Set Spot Light Blend' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Light') + self.add_input('ArmFloatSocket', 'Blend') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/light/LN_set_spot_light_size.py b/blender/arm/logicnode/light/LN_set_spot_light_size.py new file mode 100644 index 0000000000..f07b060fb3 --- /dev/null +++ b/blender/arm/logicnode/light/LN_set_spot_light_size.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetSpotLightSizeNode(ArmLogicTreeNode): + """Sets the size of the given spot light.""" + bl_idname = 'LNSetSpotLightSizeNode' + bl_label = 'Set Spot Light Size' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Light') + self.add_input('ArmFloatSocket', 'Size') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/light/__init__.py b/blender/arm/logicnode/light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/logic/LN_alternate_output.py b/blender/arm/logicnode/logic/LN_alternate_output.py new file mode 100644 index 0000000000..326dbddf1e --- /dev/null +++ b/blender/arm/logicnode/logic/LN_alternate_output.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + +class AlternateNode(ArmLogicTreeNode): + """Activates the outputs alternating every time it is active.""" + bl_idname = 'LNAlternateNode' + bl_label = 'Alternate Output' + arm_section = 'flow' + arm_version = 2 + + def __init__(self): + super(AlternateNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_output', text='New', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmNodeSocketAction' + op2 = row.operator('arm.node_remove_output', text='', icon='X', emboss=True) + op2.node_index = str(id(self)) + diff --git a/blender/arm/logicnode/logic/LN_branch.py b/blender/arm/logicnode/logic/LN_branch.py new file mode 100644 index 0000000000..5693f8825b --- /dev/null +++ b/blender/arm/logicnode/logic/LN_branch.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class BranchNode(ArmLogicTreeNode): + """Activates its `true` or `false` output, according + to the state of the plugged-in boolean.""" + bl_idname = 'LNBranchNode' + bl_label = 'Branch' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Bool') + + self.add_output('ArmNodeSocketAction', 'True') + self.add_output('ArmNodeSocketAction', 'False') diff --git a/blender/arm/logicnode/logic/LN_call_function.py b/blender/arm/logicnode/logic/LN_call_function.py new file mode 100644 index 0000000000..1e4ab14177 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_call_function.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + + +class CallFunctionNode(ArmLogicTreeNode): + """Calls the given function that was created by the [Function](#function) node.""" + bl_idname = 'LNCallFunctionNode' + bl_label = 'Call Function' + bl_description = 'Calls a function that was created by the Function node.' + arm_section = 'function' + arm_version = 2 + min_inputs = 3 + + def __init__(self): + super(CallFunctionNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Trait/Any') + self.add_input('ArmStringSocket', 'Function') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Result') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('arm.node_add_input', text='Add Arg', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmDynamicSocket' + op.name_format = "Arg {0}" + op.index_name_offset = -2 + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + 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/logic/LN_case_index.py b/blender/arm/logicnode/logic/LN_case_index.py new file mode 100644 index 0000000000..f53e20dfd8 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_case_index.py @@ -0,0 +1,60 @@ +from arm.logicnode.arm_nodes import * + + +class CaseIndexNode(ArmLogicTreeNode): + """Compare the given `Compare` value with the other inputs for equality + and return the index of the first match. This is particularly helpful + in combination with the `Select` node. + + @seeNode Select + + @input Compare: the value to be compared + @input Value: values for the dynamic comparison + + @output Index: the index of the first equal value, or `null` if no + equal value was found. + """ + bl_idname = 'LNCaseIndexNode' + bl_label = 'Case Index' + arm_version = 1 + min_inputs = 2 + + num_choices: IntProperty(default=0, min=0) + + def __init__(self): + super().__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Compare') + self.add_input_func() + + self.add_output('ArmIntSocket', 'Index') + + def draw_buttons(self, context, layout): + 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' + + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'remove_input_func' + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def add_input_func(self): + self.add_input('ArmDynamicSocket', f'Value {self.num_choices}') + self.num_choices += 1 + + def remove_input_func(self): + 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/logic/LN_function.py b/blender/arm/logicnode/logic/LN_function.py new file mode 100644 index 0000000000..f8e3ae1712 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_function.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + + +class FunctionNode(ArmLogicTreeNode): + """Creates a reusable function that can be called by the + [Call Function](#call-function) node.""" + bl_idname = 'LNFunctionNode' + bl_label = 'Function' + bl_description = 'Creates a reusable function that can be called by the Call Function node' + arm_section = 'function' + arm_version = 2 + min_outputs = 1 + + def __init__(self): + super(FunctionNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + function_name: StringProperty(name="Name") + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + row.prop(self, 'function_name') + row = layout.row(align=True) + op = row.operator('arm.node_add_output', text='Add Arg', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmDynamicSocket' + op.name_format = "Arg {0}" + op.index_name_offset = 0 + column = row.column(align=True) + op = column.operator('arm.node_remove_output', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.outputs) == self.min_outputs: + column.enabled = False + + 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/logic/LN_function_output.py b/blender/arm/logicnode/logic/LN_function_output.py new file mode 100644 index 0000000000..073b364ffd --- /dev/null +++ b/blender/arm/logicnode/logic/LN_function_output.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + + +class FunctionOutputNode(ArmLogicTreeNode): + """Sets the return value for the given function. + + @seeNode Function""" + bl_idname = 'LNFunctionOutputNode' + bl_label = 'Function Output' + arm_section = 'function' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Value') + + function_name: StringProperty(name="Name") + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + row.prop(self, 'function_name') diff --git a/blender/arm/logicnode/logic/LN_gate.py b/blender/arm/logicnode/logic/LN_gate.py new file mode 100644 index 0000000000..9ab7cde859 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_gate.py @@ -0,0 +1,78 @@ +from arm.logicnode.arm_nodes import * + + +def remove_extra_inputs(self, context): + if not any(p == self.property0 for p in ['Or', 'And']): + while len(self.inputs) > self.min_inputs: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Between': + self.add_input('ArmDynamicSocket', 'Input 3') + +class GateNode(ArmLogicTreeNode): + """Logic nodes way to do "if" statements. When activated, it + compares if its two inputs are being Equal, Greater Equal, + Less Equal, Not Equal, or Between, regardless of variable type, and passes + through its active input to the output that matches the result of + the comparison. + + "And" and "Or" are being used for booleans only, and pass through + the input when both booleans are true (And) or at least one (Or).""" + bl_idname = 'LNGateNode' + bl_label = 'Gate' + arm_version = 3 + min_inputs = 3 + + property0: HaxeEnumProperty( + 'property0', + items = [('Equal', 'Equal', 'Equal'), + ('Not Equal', 'Not Equal', 'Not Equal'), + ('Almost Equal', 'Almost Equal', 'Almost Equal'), + ('Greater', 'Greater', 'Greater'), + ('Greater Equal', 'Greater Equal', 'Greater Equal'), + ('Less', 'Less', 'Less'), + ('Less Equal', 'Less Equal', 'Less Equal'), + ('Between', 'Between', 'Input 1 Between Input 2 and Input 3 inclusive'), + ('Or', 'Or', 'Or'), + ('And', 'And', 'And')], + name='', default='Equal', + update=remove_extra_inputs) + property1: HaxeFloatProperty('property1', name='Tolerance', description='Precision for float compare', default=0.0001) + + def __init__(self): + super(GateNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Input 1') + self.add_input('ArmDynamicSocket', 'Input 2') + + self.add_output('ArmNodeSocketAction', 'True') + self.add_output('ArmNodeSocketAction', 'False') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + if self.property0 == 'Almost Equal': + layout.prop(self, 'property1') + + if any(p == self.property0 for p in ['Or', 'And']): + 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 = 'ArmDynamicSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + if self.arm_version == 1 or self.arm_version == 2: + return NodeReplacement( + 'LNGateNode', self.arm_version, 'LNGateNode', 2, + in_socket_mapping={0:0, 1:1, 2:2}, out_socket_mapping={0:0, 1:1} + ) \ No newline at end of file diff --git a/blender/arm/logicnode/logic/LN_invert_boolean.py b/blender/arm/logicnode/logic/LN_invert_boolean.py new file mode 100644 index 0000000000..c981c7cc35 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_invert_boolean.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class NotNode(ArmLogicTreeNode): + """Inverts the plugged-in boolean. If its input is `true` it outputs `false`.""" + bl_idname = 'LNNotNode' + bl_label = 'Invert Boolean' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmBoolSocket', 'Bool In') + + self.add_output('ArmBoolSocket', 'Bool Out') diff --git a/blender/arm/logicnode/logic/LN_invert_output.py b/blender/arm/logicnode/logic/LN_invert_output.py new file mode 100644 index 0000000000..7a30dbb8f3 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_invert_output.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class InverseNode(ArmLogicTreeNode): + """Activates the output if the input is not active.""" + bl_idname = 'LNInverseNode' + bl_label = 'Invert Output' + arm_section = 'flow' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_is_false.py b/blender/arm/logicnode/logic/LN_is_false.py new file mode 100644 index 0000000000..304c025d28 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_is_false.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +class IsFalseNode(ArmLogicTreeNode): + """Passes through its activation only if the plugged-in boolean + equals `false`. + + @seeNode Is True""" + bl_idname = 'LNIsFalseNode' + bl_label = 'Is False' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Bool') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_is_not_null.py b/blender/arm/logicnode/logic/LN_is_not_null.py new file mode 100644 index 0000000000..e18ee5a414 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_is_not_null.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class IsNotNoneNode(ArmLogicTreeNode): + """Passes through its activation only if the plugged-in value is + not `null`. + + @seeNode Is Null""" + bl_idname = 'LNIsNotNoneNode' + bl_label = 'Is Not Null' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_is_null.py b/blender/arm/logicnode/logic/LN_is_null.py new file mode 100644 index 0000000000..da2e120c08 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_is_null.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +class IsNoneNode(ArmLogicTreeNode): + """Passes through its activation only if the plugged-in value is + `null` (no value). + + @seeNode Is Not Null""" + bl_idname = 'LNIsNoneNode' + bl_label = 'Is Null' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_is_true.py b/blender/arm/logicnode/logic/LN_is_true.py new file mode 100644 index 0000000000..17f0b47bb4 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_is_true.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class IsTrueNode(ArmLogicTreeNode): + """Passes through its activation only if the plugged-in boolean + equals `true`. + + @seeNode Is False""" + bl_idname = 'LNIsTrueNode' + bl_label = 'Is True' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Bool') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_loop.py b/blender/arm/logicnode/logic/LN_loop.py new file mode 100644 index 0000000000..af8efc8d58 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_loop.py @@ -0,0 +1,41 @@ +from arm.logicnode.arm_nodes import * + +class LoopNode(ArmLogicTreeNode): + """Resembles a for-loop (`for (i in from...to)`) that is executed at + once when this node is activated. + + @seeNode While + @seeNode Loop Break + + @input From: The value to start the loop from (inclusive) + @input To: The value to end the loop at (exclusive) + + @output Loop: Active at every iteration of the loop + @output Index: The index for the current iteration + @output Done: Activated once when the looping is done + """ + bl_idname = 'LNLoopNode' + bl_label = 'Loop' + bl_description = 'Resembles a for-loop that is executed at once when this node is activated' + arm_section = 'flow' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmIntSocket', 'From') + self.add_input('ArmIntSocket', 'To') + + self.add_output('ArmNodeSocketAction', 'Loop') + self.add_output('ArmIntSocket', 'Index') + self.add_output('ArmNodeSocketAction', 'Done') + + def draw_label(self) -> str: + inp_from = self.inputs['From'] + inp_to = self.inputs['To'] + if inp_from.is_linked and inp_to.is_linked: + return self.bl_label + + val_from = 'x' if inp_from.is_linked else inp_from.default_value_raw + val_to = 'y' if inp_to.is_linked else inp_to.default_value_raw + + return f'{self.bl_label}: {val_from}...{val_to}' diff --git a/blender/arm/logicnode/logic/LN_loop_break.py b/blender/arm/logicnode/logic/LN_loop_break.py new file mode 100644 index 0000000000..326d15b8ad --- /dev/null +++ b/blender/arm/logicnode/logic/LN_loop_break.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class LoopBreakNode(ArmLogicTreeNode): + """Terminates the currently executing loop (only one loop is + executed at once). + + @seeNode Loop + @seeNode While + """ + bl_idname = 'LNLoopBreakNode' + bl_label = 'Loop Break' + arm_section = 'flow' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') diff --git a/blender/arm/logicnode/logic/LN_loop_continue.py b/blender/arm/logicnode/logic/LN_loop_continue.py new file mode 100644 index 0000000000..5504fcff5b --- /dev/null +++ b/blender/arm/logicnode/logic/LN_loop_continue.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class LoopContinueNode(ArmLogicTreeNode): + """continues to the next loop. + + @seeNode Loop + @seeNode While + """ + bl_idname = 'LNLoopContinueNode' + bl_label = 'Loop Continue' + arm_section = 'flow' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') diff --git a/blender/arm/logicnode/logic/LN_merge.py b/blender/arm/logicnode/logic/LN_merge.py new file mode 100644 index 0000000000..7217135f12 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_merge.py @@ -0,0 +1,91 @@ +from arm.logicnode.arm_nodes import * + + +class MergeNode(ArmLogicTreeNode): + """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 = 3 + min_inputs = 0 + + def update_exec_mode(self, context): + self.outputs['Active Input Index'].hide = self.property0 == 'once_per_frame' + + property0: HaxeEnumProperty( + 'property0', + 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__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Active Input Index') + + def draw_buttons(self, context, layout): + 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' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def draw_label(self) -> str: + if len(self.inputs) == self.min_inputs: + 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, 2): + raise LookupError() + + if self.arm_version == 1 or self.arm_version == 2: + 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 \ No newline at end of file diff --git a/blender/arm/logicnode/logic/LN_null.py b/blender/arm/logicnode/logic/LN_null.py new file mode 100644 index 0000000000..ed0d960147 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_null.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class NoneNode(ArmLogicTreeNode): + """A `null` value that can be used in comparisons and conditions.""" + bl_idname = 'LNNoneNode' + bl_label = 'Null' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Null') 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 0000000000..56c82fc5f3 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_once_per_frame.py @@ -0,0 +1,15 @@ +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 arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/logic/LN_output_sequence.py b/blender/arm/logicnode/logic/LN_output_sequence.py new file mode 100644 index 0000000000..0afa941b23 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_output_sequence.py @@ -0,0 +1,34 @@ +from arm.logicnode.arm_nodes import * + +class SequenceNode(ArmLogicTreeNode): + """Activates the outputs one by one sequentially and repeatedly.""" + bl_idname = 'LNSequenceNode' + bl_label = 'Output Sequence' + arm_section = 'flow' + arm_version = 2 + min_outputs = 0 + + def __init__(self): + super(SequenceNode, self).__init__() + array_nodes[self.get_id_str()] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_output', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmNodeSocketAction' + column = row.column(align=True) + op = column.operator('arm.node_remove_output', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.outputs) == self.min_outputs: + column.enabled = False + + 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/logic/LN_output_to_boolean.py b/blender/arm/logicnode/logic/LN_output_to_boolean.py new file mode 100644 index 0000000000..d03c79cf76 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_output_to_boolean.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ToBoolNode(ArmLogicTreeNode): + """Converts a signal to a boolean value. If the input signal is + active, the boolean is `true`; if not, the boolean is `false`.""" + bl_idname = 'LNToBoolNode' + bl_label = 'Output to Boolean' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmBoolSocket', 'Bool') diff --git a/blender/arm/logicnode/logic/LN_pulse.py b/blender/arm/logicnode/logic/LN_pulse.py new file mode 100644 index 0000000000..d207bbf97f --- /dev/null +++ b/blender/arm/logicnode/logic/LN_pulse.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + +class PulseNode(ArmLogicTreeNode): + """Sends a signal repeatedly between the given time interval until you stop it. + + @input Start: Starts to send the signals + @input Stop: Stops to send the signals + @input Interval: The interval between the signals + """ + bl_idname = 'LNPulseNode' + bl_label = 'Pulse' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmFloatSocket', 'Interval', default_value=0.1) + + self.add_output('ArmNodeSocketAction', 'Out') \ No newline at end of file diff --git a/blender/arm/logicnode/logic/LN_select.py b/blender/arm/logicnode/logic/LN_select.py new file mode 100644 index 0000000000..ccde6643fc --- /dev/null +++ b/blender/arm/logicnode/logic/LN_select.py @@ -0,0 +1,125 @@ +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 = 2 + min_inputs = 2 + + def update_exec_mode(self, context): + self.set_mode() + + property0: HaxeEnumProperty( + 'property0', + 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 arm_init(self, context): + self.set_mode() + + def set_mode(self): + self.inputs.clear() + self.outputs.clear() + + if self.property0 == 'from_index': + self.add_input('ArmIntSocket', 'Index') + self.add_input('ArmDynamicSocket', '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('ArmDynamicSocket', 'Value 0') + self.num_choices = 1 + + self.add_output('ArmNodeSocketAction', 'Out') + + self.add_output('ArmDynamicSocket', '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' + + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'remove_input_func' + if len(self.inputs) == self.min_inputs: + column.enabled = False + + 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('ArmDynamicSocket', 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}]' + + 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/logic/LN_switch_output.py b/blender/arm/logicnode/logic/LN_switch_output.py new file mode 100644 index 0000000000..b72e88cee2 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_switch_output.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + +class SwitchNode(ArmLogicTreeNode): + """Activates the outputs depending of the value. If the "value" is equal to "case 1", the output "case 1" will be activated. + + @output Default: Activated if the input value does not match any case. + """ + bl_idname = 'LNSwitchNode' + bl_label = 'Switch Output' + arm_version = 4 + min_inputs = 2 + + def __init__(self): + super(SwitchNode, self).__init__() + array_nodes[self.get_id_str()] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Default') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('arm.node_add_input_output', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.in_socket_type = 'ArmDynamicSocket' + op.out_socket_type = 'ArmNodeSocketAction' + op.in_name_format = 'Case {0}' + op.out_name_format = 'Case {0}' + op.in_index_name_offset = -1 + op.out_index_name_offset = -1 + column = row.column(align=True) + op = column.operator('arm.node_remove_input_output', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 3): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/logic/LN_value_changed.py b/blender/arm/logicnode/logic/LN_value_changed.py new file mode 100644 index 0000000000..3ae3ecea84 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_value_changed.py @@ -0,0 +1,38 @@ +from arm.logicnode.arm_nodes import * + + +class ValueChangedNode(ArmLogicTreeNode): + """Upon activation through the `In` input, this node checks whether + the given value is different than the value from the last execution + of this node. + + @output Changed: Activates if the value has changed compared to the + last time the node was executed or if the node is executed for + the first time and there is no value for comparison yet. + @output Unchanged: Activates if the value is the same as it was when + the node was executed the last time. + @output Is Initial: Activates if the value is equal to the value at + the first time the node was executed or if the node is executed + for the first time. This output works independently of the + `Changed` or `Unchanged` outputs. + """ + bl_idname = 'LNValueChangedNode' + bl_label = 'Value Changed' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Changed') + self.add_output('ArmNodeSocketAction', 'Unchanged') + self.add_output('ArmNodeSocketAction', 'Is Initial') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNValueChangedNode', self.arm_version, 'LNValueChangedNode', 2, + in_socket_mapping={0: 0, 1: 1}, out_socket_mapping={0: 0, 1: 2} + ) diff --git a/blender/arm/logicnode/logic/LN_while_true.py b/blender/arm/logicnode/logic/LN_while_true.py new file mode 100644 index 0000000000..b2a7b670e2 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_while_true.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + +class WhileNode(ArmLogicTreeNode): + """Loops while the condition is `true`. + + @seeNode Loop + @seeNode Loop Break + + @input Condition: boolean that resembles the result of the condition + + @output Loop: Activated on every iteration step + @output Done: Activated when the loop is done executing""" + bl_idname = 'LNWhileNode' + bl_label = 'While True' + arm_section = 'flow' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Condition') + + self.add_output('ArmNodeSocketAction', 'Loop') + self.add_output('ArmNodeSocketAction', 'Done') diff --git a/blender/arm/logicnode/logic/__init__.py b/blender/arm/logicnode/logic/__init__.py new file mode 100644 index 0000000000..6321e21333 --- /dev/null +++ b/blender/arm/logicnode/logic/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='flow', category='Logic') +add_node_section(name='function', category='Logic') diff --git a/blender/arm/logicnode/map/LN_clear_map.py b/blender/arm/logicnode/map/LN_clear_map.py new file mode 100644 index 0000000000..156c5907a8 --- /dev/null +++ b/blender/arm/logicnode/map/LN_clear_map.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class ClearMapNode(ArmLogicTreeNode): + """Clear Map""" + bl_idname = 'LNClearMapNode' + bl_label = 'Clear Map' + arm_version = 1 + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/map/LN_create_map.py b/blender/arm/logicnode/map/LN_create_map.py new file mode 100644 index 0000000000..90ca240240 --- /dev/null +++ b/blender/arm/logicnode/map/LN_create_map.py @@ -0,0 +1,38 @@ +from arm.logicnode.arm_nodes import * + +class CreateMapNode(ArmLogicTreeNode): + """Create Map""" + bl_idname = 'LNCreateMapNode' + bl_label = 'Create Map' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('string', 'String', 'String Map Key Type'), + ('int', 'Int', 'Int Map key type'), + ('enumvalue', 'EnumValue', 'EnumValue Map key type'), + ('object', 'Object', 'Object Map key type')], + name='Key', + default='string') + + property1: HaxeEnumProperty( + 'property1', + items = [('string', 'String', 'String Map Value Type'), + ('vector', 'Vector', 'Vector Map Value Type'), + ('float', 'Float', 'Float Map Value Type'), + ('integer', 'Integer', 'Integer Map Value Type'), + ('boolean', 'Boolean', 'Boolean Map Value Type'), + ('dynamic', 'Dynamic', 'Dynamic Map Value Type')], + name='Value', + default='string') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Map') diff --git a/blender/arm/logicnode/map/LN_get_map_value.py b/blender/arm/logicnode/map/LN_get_map_value.py new file mode 100644 index 0000000000..3bc7851c2c --- /dev/null +++ b/blender/arm/logicnode/map/LN_get_map_value.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class GetMapValueNode(ArmLogicTreeNode): + """Get Map Value""" + bl_idname = 'LNGetMapValueNode' + bl_label = 'Get Map Value' + arm_version = 1 + + def init(self, context): + self.add_input('ArmDynamicSocket', 'Map') + self.add_input('ArmDynamicSocket', 'Key') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/map/LN_map_key_exists.py b/blender/arm/logicnode/map/LN_map_key_exists.py new file mode 100644 index 0000000000..a3fb74a874 --- /dev/null +++ b/blender/arm/logicnode/map/LN_map_key_exists.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class MapKeyExistsNode(ArmLogicTreeNode): + """Map Key Exists""" + bl_idname = 'LNMapKeyExistsNode' + bl_label = 'Map Key Exists' + arm_version = 1 + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + self.add_input('ArmDynamicSocket', 'Key') + + self.add_output('ArmNodeSocketAction', 'True') + self.add_output('ArmNodeSocketAction', 'False') \ No newline at end of file diff --git a/blender/arm/logicnode/map/LN_map_loop.py b/blender/arm/logicnode/map/LN_map_loop.py new file mode 100644 index 0000000000..1959bc0145 --- /dev/null +++ b/blender/arm/logicnode/map/LN_map_loop.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +class MapLoopNode(ArmLogicTreeNode): + """Map Loop""" + bl_idname = 'LNMapLoopNode' + bl_label = 'Map Loop' + arm_version = 1 + + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + + self.add_output('ArmNodeSocketAction', 'Loop') + self.add_output('ArmDynamicSocket', 'Key') + self.add_output('ArmDynamicSocket', 'Value') + self.add_output('ArmNodeSocketAction', 'Done') diff --git a/blender/arm/logicnode/map/LN_remove_map_key.py b/blender/arm/logicnode/map/LN_remove_map_key.py new file mode 100644 index 0000000000..4a47a2f219 --- /dev/null +++ b/blender/arm/logicnode/map/LN_remove_map_key.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + + +class RemoveMapKeyNode(ArmLogicTreeNode): + """Remove Map Key""" + bl_idname = 'LNRemoveMapKeyNode' + bl_label = 'Remove Map Key' + arm_version = 1 + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + self.add_input('ArmDynamicSocket', 'Key') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/map/LN_set_map_value.py b/blender/arm/logicnode/map/LN_set_map_value.py new file mode 100644 index 0000000000..081f1c5318 --- /dev/null +++ b/blender/arm/logicnode/map/LN_set_map_value.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class SetMapValueNode(ArmLogicTreeNode): + """Set Map Value""" + bl_idname = 'LNSetMapValueNode' + bl_label = 'Set Map Value' + arm_version = 1 + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + self.add_input('ArmDynamicSocket', 'Key') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/map/__init__.py b/blender/arm/logicnode/map/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/blender/arm/logicnode/map/__init__.py @@ -0,0 +1 @@ + diff --git a/blender/arm/logicnode/material/LN_get_object_material.py b/blender/arm/logicnode/material/LN_get_object_material.py new file mode 100644 index 0000000000..8e64630df5 --- /dev/null +++ b/blender/arm/logicnode/material/LN_get_object_material.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetMaterialNode(ArmLogicTreeNode): + """Returns the material of the given object.""" + bl_idname = 'LNGetMaterialNode' + bl_label = 'Get Object Material' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmIntSocket', 'Slot') + + self.add_output('ArmDynamicSocket', 'Material') diff --git a/blender/arm/logicnode/material/LN_material.py b/blender/arm/logicnode/material/LN_material.py new file mode 100644 index 0000000000..46a4a25b71 --- /dev/null +++ b/blender/arm/logicnode/material/LN_material.py @@ -0,0 +1,30 @@ +import bpy + +import arm.utils +from arm.logicnode.arm_nodes import * + + +class MaterialNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given material as a variable.""" + bl_idname = 'LNMaterialNode' + bl_label = 'Material' + arm_version = 1 + + @property + def property0_get(self): + if self.property0 == None: + return '' + if self.property0.name not in bpy.data.materials: + return self.property0.name + return arm.utils.asset_name(bpy.data.materials[self.property0.name]) + + property0: HaxePointerProperty('property0', name='', type=bpy.types.Material) + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Material', is_var=True) + + def draw_content(self, context, layout): + layout.prop_search(self, 'property0', bpy.data, 'materials', icon='NONE', text='') + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.property0 = master_node.property0 diff --git a/blender/arm/logicnode/material/LN_set_material_image_param.py b/blender/arm/logicnode/material/LN_set_material_image_param.py new file mode 100644 index 0000000000..686562c16e --- /dev/null +++ b/blender/arm/logicnode/material/LN_set_material_image_param.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + +class SetMaterialImageParamNode(ArmLogicTreeNode): + """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 = 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 new file mode 100644 index 0000000000..607c0ed276 --- /dev/null +++ b/blender/arm/logicnode/material/LN_set_material_rgb_param.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + +class SetMaterialRgbParamNode(ArmLogicTreeNode): + """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 = 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 new file mode 100644 index 0000000000..e3f2164ca9 --- /dev/null +++ b/blender/arm/logicnode/material/LN_set_material_value_param.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + +class SetMaterialValueParamNode(ArmLogicTreeNode): + """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 = 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/material/LN_set_object_material_slot.py b/blender/arm/logicnode/material/LN_set_object_material_slot.py new file mode 100644 index 0000000000..ccfab36696 --- /dev/null +++ b/blender/arm/logicnode/material/LN_set_object_material_slot.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetMaterialSlotNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNSetMaterialSlotNode' + bl_label = 'Set Object Material Slot' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Material') + self.add_input('ArmIntSocket', 'Slot') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/material/__init__.py b/blender/arm/logicnode/material/__init__.py new file mode 100644 index 0000000000..170731fe14 --- /dev/null +++ b/blender/arm/logicnode/material/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Material') +add_node_section(name='params', category='Material') diff --git a/blender/arm/logicnode/math/LN_bitwise_math.py b/blender/arm/logicnode/math/LN_bitwise_math.py new file mode 100644 index 0000000000..bb0154bce0 --- /dev/null +++ b/blender/arm/logicnode/math/LN_bitwise_math.py @@ -0,0 +1,58 @@ +from arm.logicnode.arm_nodes import * + + +class BitwiseMathNode(ArmLogicTreeNode): + """Perform bitwise math on integer values.""" + bl_idname = 'LNBitwiseMathNode' + bl_label = 'Bitwise Math' + arm_version = 1 + + operators = { + 'negation': '~', + 'and': '&', + 'or': '|', + 'xor': '^', + 'left_shift': '<<', + 'right_shift': '>>', + 'unsigned_right_shift': '>>>' + } + + def set_mode(self, context): + if self.property0 == 'negation': + self.inputs[0].name = 'Operand' + self.inputs.remove(self.inputs[1]) + else: + self.inputs[0].name = 'Operand 1' + if len(self.inputs) < 2: + self.add_input('ArmIntSocket', 'Operand 2') + + property0: HaxeEnumProperty( + 'property0', + items=[ + ('negation', 'Negation (~)', 'Performs bitwise negation on the input, so a 0-bit becomes a 1-bit and vice versa'), + None, + ('and', 'And (&)', 'A bit in the result is 1 if both bits at the same digit in the operands are 1, else it is 0'), + ('or', 'Or (|)', 'A bit in the result is 1 if at least one bit at the same digit in the operands is 1, else it is 0'), + ('xor', 'Xor (^)', 'A bit in the result is 1 if exactly one bit at the same digit in the operands is 1, else it is 0'), + None, + ('left_shift', 'Left Shift (<<)', 'Shifts the bits of operand 1 to the left by the amount of operand 2. The result is undefined if operand 2 is negative'), + ('right_shift', 'Right Shift (>>)', 'Shifts the bits of operand 1 to the right by the amount of operand 2 and keeps the sign of operand 1 (the most significant bit does not change). The result is undefined if operand 2 is negative'), + ('unsigned_right_shift', 'Unsigned Right Shift (>>>)', 'Shifts the bits of operand 1 to the right by the amount of operand 2, and the most significant bit is set to 0. The result is undefined if operand 2 is negative'), + ], + name='Operation', + description='The operation to perform on the input(s)', + default='negation', + update=set_mode + ) + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Operand 1') + self.add_output('ArmIntSocket', 'Result') + + self.set_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0', text='') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.operators[self.property0]}' diff --git a/blender/arm/logicnode/math/LN_clamp.py b/blender/arm/logicnode/math/LN_clamp.py new file mode 100644 index 0000000000..257c3802ad --- /dev/null +++ b/blender/arm/logicnode/math/LN_clamp.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class ClampNode(ArmLogicTreeNode): + """Keeps the value inside the given bound. + + @seeNode Map Range + """ + bl_idname = 'LNClampNode' + bl_label = 'Clamp' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Value') + self.add_input('ArmFloatSocket', 'Min') + self.add_input('ArmFloatSocket', 'Max') + + self.add_output('ArmFloatSocket', 'Result') diff --git a/blender/arm/logicnode/math/LN_combine_hsv.py b/blender/arm/logicnode/math/LN_combine_hsv.py new file mode 100644 index 0000000000..dab5e1a39c --- /dev/null +++ b/blender/arm/logicnode/math/LN_combine_hsv.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + + +class CombineColorNode(ArmLogicTreeNode): + """Combines the given HSVA() components to a color value. + If any input is `null`, the respective channel of the output color is set to `0.0`. + formula from // https://stackoverflow.com/a/17243070 + """ + bl_idname = 'LNCombineColorHSVNode' + bl_label = 'Combine HSVA' + arm_section = 'color' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'H', default_value=0.0) + self.add_input('ArmFloatSocket', 'S', default_value=0.0) + self.add_input('ArmFloatSocket', 'V', default_value=0.0) + self.add_input('ArmFloatSocket', 'A', default_value=1.0) + + self.add_output('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) diff --git a/blender/arm/logicnode/math/LN_combine_rgb.py b/blender/arm/logicnode/math/LN_combine_rgb.py new file mode 100644 index 0000000000..3ece17af42 --- /dev/null +++ b/blender/arm/logicnode/math/LN_combine_rgb.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + + +class CombineColorNode(ArmLogicTreeNode): + """Combines the given RGBA (red, green, blue, and alpha) components to a color value. + If any input is `null`, the respective channel of the output color is set to `0.0`. + """ + bl_idname = 'LNCombineColorNode' + bl_label = 'Combine RGBA' + arm_section = 'color' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'R', default_value=0.0) + self.add_input('ArmFloatSocket', 'G', default_value=0.0) + self.add_input('ArmFloatSocket', 'B', default_value=0.0) + self.add_input('ArmFloatSocket', 'A', default_value=1.0) + + self.add_output('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) diff --git a/blender/arm/logicnode/math/LN_compare.py b/blender/arm/logicnode/math/LN_compare.py new file mode 100644 index 0000000000..d5d6599f43 --- /dev/null +++ b/blender/arm/logicnode/math/LN_compare.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + +def remove_extra_inputs(self, context): + if not any(p == self.property0 for p in ['Or', 'And']): + while len(self.inputs) > self.min_inputs: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Between': + self.add_input('ArmDynamicSocket', 'Input 3') + +class CompareNode(ArmLogicTreeNode): + """Compares values.""" + bl_idname = 'LNCompareNode' + bl_label = 'Compare' + arm_version = 3 + property0: HaxeEnumProperty( + 'property0', + items = [('Equal', 'Equal', 'Equal'), + ('Not Equal', 'Not Equal', 'Not Equal'), + ('Almost Equal', 'Almost Equal', 'Almost Equal'), + ('Greater', 'Greater', 'Greater'), + ('Greater Equal', 'Greater Equal', 'Greater Equal'), + ('Less', 'Less', 'Less'), + ('Less Equal', 'Less Equal', 'Less Equal'), + ('Between', 'Between', 'Input 1 Between Input 2 and Input 3 inclusive'), + ('Or', 'Or', 'Or'), + ('And', 'And', 'And')], + name='', default='Equal', + update=remove_extra_inputs) + min_inputs = 2 + property1: HaxeFloatProperty('property1', name='Tolerance', description='Precision for float compare', default=0.0001) + + def __init__(self): + super(CompareNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Value') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmBoolSocket', 'Bool') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + if self.property0 == 'Almost Equal': + layout.prop(self, 'property1') + + if any(p == self.property0 for p in ['Or', 'And']): + 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 = 'ArmDynamicSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + if self.arm_version == 1 or self.arm_version == 2: + return NodeReplacement( + 'LNGateNode', self.arm_version, 'LNGateNode', 2, + in_socket_mapping={0:0, 1:1, 2:2}, out_socket_mapping={0:0, 1:1} + ) \ No newline at end of file diff --git a/blender/arm/logicnode/math/LN_deg_to_rad.py b/blender/arm/logicnode/math/LN_deg_to_rad.py new file mode 100644 index 0000000000..6ee81e6128 --- /dev/null +++ b/blender/arm/logicnode/math/LN_deg_to_rad.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class DegToRadNode(ArmLogicTreeNode): + """Converts degrees to radians.""" + bl_idname = 'LNDegToRadNode' + bl_label = 'Deg to Rad' + arm_version = 1 + arm_section = 'angle' + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Degrees') + + self.add_output('ArmFloatSocket', 'Radians') diff --git a/blender/arm/logicnode/math/LN_float_delta_interpolate.py b/blender/arm/logicnode/math/LN_float_delta_interpolate.py new file mode 100644 index 0000000000..d8aa20ad86 --- /dev/null +++ b/blender/arm/logicnode/math/LN_float_delta_interpolate.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + +class FloatDeltaInterpolateNode(ArmLogicTreeNode): + """Linearly interpolate to a new value with specified interpolation `Rate`. + @input From: Value to interpolate from. + @input To: Value to interpolate to. + @input Delta Time: Delta Time. + @input Rate: Rate of interpolation. + """ + bl_idname = 'LNFloatDeltaInterpolateNode' + bl_label = 'Float Delta Interpolate' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'From', default_value=0.0) + self.add_input('ArmFloatSocket', 'To', default_value=1.0) + self.add_input('ArmFloatSocket', 'Delta Time') + self.add_input('ArmFloatSocket', 'Rate') + + self.add_output('ArmFloatSocket', 'Result') diff --git a/blender/arm/logicnode/math/LN_key_interpolate.py b/blender/arm/logicnode/math/LN_key_interpolate.py new file mode 100644 index 0000000000..4d25b4efc3 --- /dev/null +++ b/blender/arm/logicnode/math/LN_key_interpolate.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class KeyInterpolateNode(ArmLogicTreeNode): + """Linearly interpolate to 1.0 if input is true and interpolate to 0.0 if input is false. + @input Key State: Interpolate to 1.0 if true and 0.0 if false. + @input Init: Initial value in the range 0.0 to 1.0. + @input Rate: Rate of interpolation. + """ + bl_idname = 'LNKeyInterpolateNode' + bl_label = 'Key Interpolate Node' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmBoolSocket', 'Key State') + self.add_input('ArmFloatSocket', 'Init', default_value=0.0) + self.add_input('ArmFloatSocket', 'Rate') + + self.add_output('ArmFloatSocket', 'Result') diff --git a/blender/arm/logicnode/math/LN_map_range.py b/blender/arm/logicnode/math/LN_map_range.py new file mode 100644 index 0000000000..0008ce88d5 --- /dev/null +++ b/blender/arm/logicnode/math/LN_map_range.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + +class MapRangeNode(ArmLogicTreeNode): + """Converts the given value from a range to another range. + + @seeNode Clamp + """ + bl_idname = 'LNMapRangeNode' + bl_label = 'Map Range' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Value', default_value=1.0) + self.add_input('ArmFloatSocket', 'From Min') + self.add_input('ArmFloatSocket', 'From Max', default_value=1.0) + self.add_input('ArmFloatSocket', 'To Min') + self.add_input('ArmFloatSocket', 'To Max', default_value=1.0) + + self.add_output('ArmFloatSocket', 'Result') diff --git a/blender/arm/logicnode/math/LN_math.py b/blender/arm/logicnode/math/LN_math.py new file mode 100644 index 0000000000..5a728c9625 --- /dev/null +++ b/blender/arm/logicnode/math/LN_math.py @@ -0,0 +1,138 @@ +from arm.logicnode.arm_nodes import * + +class MathNode(ArmLogicTreeNode): + """Mathematical operations on values.""" + bl_idname = 'LNMathNode' + bl_label = 'Math' + arm_version = 3 + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(operation_name): + return { + 'Add': 0, + 'Subtract': 0, + 'Multiply': 0, + 'Divide': 0, + 'Sine': 1, + 'Cosine': 1, + 'Abs': 1, + 'Tangent': 1, + 'Arcsine': 1, + 'Arccosine': 1, + 'Arctangent': 1, + 'Logarithm': 1, + 'Round': 2, + 'Floor': 1, + 'Ceil': 1, + 'Square Root': 1, + 'Fract': 1, + 'Exponent': 1, + 'Max': 2, + 'Min': 2, + 'Power': 2, + 'Arctan2': 2, + 'Modulo': 2, + 'Less Than': 2, + 'Greater Than': 2, + 'Ping-Pong': 2 + }.get(operation_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of another operation + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + if select_prev != select_current: + # Many arguments: Add, Subtract, Multiply, Divide + if (self.get_count_in(select_current) == 0): + while (len(self.inputs) < 2): + self.add_input('ArmFloatSocket', 'Value ' + str(len(self.inputs))) + # 2 arguments: Max, Min, Power, Arctan2, Modulo, Less Than, Greater Than, Ping-Pong + if (self.get_count_in(select_current) == 2): + while (len(self.inputs) > 2): + self.inputs.remove(self.inputs.values()[-1]) + while (len(self.inputs) < 2): + self.add_input('ArmFloatSocket', 'Value ' + str(len(self.inputs))) + # 1 argument: Sine, Cosine, Abs, Tangent, Arcsine, Arccosine, Arctangent, Logarithm, Round, Floor, Ceil, Square Root, Fract, Exponent + if (self.get_count_in(select_current) == 1): + while (len(self.inputs) > 1): + self.inputs.remove(self.inputs.values()[-1]) + self['property0'] = value + if (self.property0 == 'Round'): + self.inputs[1].name = 'Precision' + elif (self.property0 == 'Ping-Pong'): + self.inputs[1].name = 'Scale' + elif (len(self.inputs) > 1): self.inputs[1].name = 'Value 1' + + property0: HaxeEnumProperty( + 'property0', + items = [('Add', 'Add', 'Add'), + ('Multiply', 'Multiply', 'Multiply'), + ('Sine', 'Sine', 'Sine'), + ('Cosine', 'Cosine', 'Cosine'), + ('Max', 'Maximum', 'Max'), + ('Min', 'Minimum', 'Min'), + ('Abs', 'Absolute', 'Abs'), + ('Subtract', 'Subtract', 'Subtract'), + ('Divide', 'Divide', 'Divide'), + ('Tangent', 'Tangent', 'Tangent'), + ('Arcsine', 'Arcsine', 'Arcsine'), + ('Arccosine', 'Arccosine', 'Arccosine'), + ('Arctangent', 'Arctangent', 'Arctangent'), + ('Power', 'Power', 'Power'), + ('Logarithm', 'Logarithm', 'Logarithm'), + ('Round', 'Round', 'Round (Value 1 precision of decimal places)'), + ('Less Than', 'Less Than', 'Less Than'), + ('Greater Than', 'Greater Than', 'Greater Than'), + ('Modulo', 'Modulo', 'Modulo'), + ('Arctan2', 'Arctan2', 'Arctan2'), + ('Floor', 'Floor', 'Floor'), + ('Ceil', 'Ceil', 'Ceil'), + ('Fract', 'Fract', 'Fract'), + ('Square Root', 'Square Root', 'Square Root'), + ('Exponent', 'Exponent', 'Exponent'), + ('Ping-Pong', 'Ping-Pong', 'The output value is moved between 0.0 and the Scale based on the input value')], + name='', default='Add', set=set_enum, get=get_enum) + + property1: HaxeBoolProperty('property1', name='Clamp', default=False) + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Value 0', default_value=0.0) + self.add_input('ArmFloatSocket', 'Value 1', default_value=0.0) + + self.add_output('ArmFloatSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property1') + layout.prop(self, 'property0') + # Many arguments: Add, Subtract, Multiply, Divide + if (self.get_count_in(self.property0) == 0): + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmFloatSocket' + op.name_format = 'Value {0}' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == 2: + column.enabled = False + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/math/LN_math_expression.py b/blender/arm/logicnode/math/LN_math_expression.py new file mode 100644 index 0000000000..0795d6a810 --- /dev/null +++ b/blender/arm/logicnode/math/LN_math_expression.py @@ -0,0 +1,189 @@ +from arm.logicnode.arm_nodes import * +import re + +class MathExpressionNode(ArmLogicTreeNode): + """Mathematical operations on values.""" + bl_idname = 'LNMathExpressionNode' + bl_label = 'Math Expression' + arm_version = 1 + min_inputs = 2 + max_inputs = 10 + + @staticmethod + def get_variable_name(index): + return { + 0: 'a', + 1: 'b', + 2: 'c', + 3: 'd', + 4: 'e', + 5: 'x', + 6: 'y', + 7: 'h', + 8: 'i', + 9: 'k' + }.get(index, 'a') + + @staticmethod + def get_clear_exp(value): + return re.sub(r'[\-\+\*\/\(\)\^\%abcdexyhik0123456789. ]', '', value).strip() + + @staticmethod + def get_invalid_characters(value): + value = value.replace(' ', '') + len_v = len(value) + arg = ['a', 'b', 'c', 'd', 'e', 'x', 'y', 'h', 'i', 'k'] + for i in range(len_v): + s = value[i] + if s == '.': + if ((i - 1) < 0) or ((i + 1) >= len_v) or (not value[i - 1].isnumeric()) or (not value[i + 1].isnumeric()): + return False + oper = ['+', '-', '*', '/', '%', '^'] + if s == '(': + if (i > 0) and ((value[i - 1] not in oper) and (value[i - 1] != '(')): + return False + if (i < (len_v - 1)) and ((value[i + 1] not in arg) and (not value[i + 1].isnumeric()) and (value[i + 1] != '(')): + return False + if s == ')': + if (i > 0) and ((value[i - 1] not in arg) and (not value[i - 1].isnumeric()) and (value[i - 1] != ')')): + return False + if (i < (len_v - 1)) and (not value[i + 1].isnumeric()) and ((value[i + 1] not in oper) and (value[i + 1] != ')')): + return False + if s in oper: + if ((i > 0) and (value[i - 1] in oper)) or ((i < (len_v - 1)) and (value[i + 1] in oper)): + return False + last_sym = value[len_v - 1] + if (not last_sym.isnumeric()) and (last_sym not in arg) and (last_sym != ')'): + return False + return True + + @staticmethod + def check_variable(self, value): + variables = re.sub(r'[\-\+\*\/\(\)\^\%0123456789. ]', '', value).strip() + for vr in variables: + check = False + for inp_key in self.inputs.keys(): + if (vr == inp_key): + check = True + break + if not check: + return False + return True + + @staticmethod + def matches(line, opendelim='(', closedelim=')'): + stack = [] + for m in re.finditer(r'[{}{}]'.format(opendelim, closedelim), line): + pos = m.start() + if line[pos-1] == '\\': + # Skip escape sequence + continue + c = line[pos] + if c == opendelim: + stack.append(pos+1) + elif c == closedelim: + if len(stack) > 0: + prevpos = stack.pop() + yield (True, prevpos, pos, len(stack)) + else: + # Error + yield (False, 0, 0, 0) + pass + if len(stack) > 0: + for pos in stack: + yield (False, 0, 0, 0) + + @staticmethod + def isPartCorrect(s): + if len(s.replace('p', '').replace(' ', '').split()) == 0: + return True + REGEX = re.compile(r"(([abcdexyhikp]|\d+)[\+\-\/\*\^\%]){1,}([abcdexyhikp]|\d+)(=([abcdexyhikp]|\d+))?") + result = False + if REGEX.match(s): + result = True + return result + + @staticmethod + def isCorrect(self, s): + result = True + if s.find("(") >=0 or s.find(")") >= 0: + for correct, openpos, closepos, level in self.matches(s): + if correct: + part = s[openpos:closepos] + if part.find("(") == -1 and part.find(")") == -1: + if not self.isPartCorrect(part): + result = False + break + part = s[openpos-1:closepos+1] + replaced = s.replace(part, "p") + if replaced.find("(") >=0 or replaced.find(")") >= 0: + if not self.isCorrect(self, replaced): + result = False + break + else: + if not self.isPartCorrect(replaced): + result = False + break + else: + result = False + break + else: + result = self.isPartCorrect(s) + return result + + def set_exp_error(self, value): + self['exp_error'] = value + + def get_exp_error(self): + return self.get('exp_error', False) + + def set_exp(self, value): + value = value.lower() + self['property0'] = value + # Check errors + val_error = False + if len(self.get_clear_exp(value)) > 0: + val_error = True + elif not self.get_invalid_characters(value): + val_error = True + elif not self.check_variable(self, value): + val_error = True + elif not self.isCorrect(self, value.replace(' ', '')): + val_error = True + self.set_exp_error(val_error) + + def get_exp(self): + return self.get('property0', 'a + b') + + property0: HaxeStringProperty('property0', name='', description='Expression (operation: +, -, *, /, ^, (, ), %)', set=set_exp, get=get_exp) + property1: HaxeBoolProperty('property1', name='Clamp', default=False) + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmFloatSocket', self.get_variable_name(0), default_value=0.0) + self.add_input('ArmFloatSocket', self.get_variable_name(1), default_value=0.0) + self.add_output('ArmFloatSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property1') + # Expression + row = layout.row(align=True) + column = row.column(align=True) + column.alert = self.get_exp_error() + column.prop(self, 'property0', icon='FORCE_HARMONIC') + # Buttons + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) + if len(self.inputs) == 10: + column.enabled = False + op.node_index = str(id(self)) + op.socket_type = 'ArmFloatSocket' + op.name_format = self.get_variable_name(len(self.inputs)) + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == 2: + column.enabled = False diff --git a/blender/arm/logicnode/math/LN_matrix_math.py b/blender/arm/logicnode/math/LN_matrix_math.py new file mode 100644 index 0000000000..6a4d200e64 --- /dev/null +++ b/blender/arm/logicnode/math/LN_matrix_math.py @@ -0,0 +1,22 @@ +from arm.logicnode.arm_nodes import * + +class MatrixMathNode(ArmLogicTreeNode): + """Multiplies matrices.""" + bl_idname = 'LNMatrixMathNode' + bl_label = 'Matrix Math' + arm_section = 'matrix' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Multiply', 'Multiply', 'Multiply')], + name='', default='Multiply') + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Matrix 1') + self.add_input('ArmDynamicSocket', 'Matrix 2') + + self.add_output('ArmDynamicSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/math/LN_mix.py b/blender/arm/logicnode/math/LN_mix.py new file mode 100644 index 0000000000..7ae648a47a --- /dev/null +++ b/blender/arm/logicnode/math/LN_mix.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + +class MixNode(ArmLogicTreeNode): + """Interpolates between the two given values.""" + bl_idname = 'LNMixNode' + bl_label = 'Mix' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('Sine', 'Sine', 'Sine'), + ('Quad', 'Quad', 'Quad'), + ('Cubic', 'Cubic', 'Cubic'), + ('Quart', 'Quart', 'Quart'), + ('Quint', 'Quint', 'Quint'), + ('Expo', 'Expo', 'Expo'), + ('Circ', 'Circ', 'Circ'), + ('Back', 'Back', 'Back'), + ('Bounce', 'Bounce', 'Bounce'), + ('Elastic', 'Elastic', 'Elastic'), + ], + name='', default='Linear') + property1: HaxeEnumProperty( + 'property1', + items = [('In', 'In', 'In'), + ('Out', 'Out', 'Out'), + ('InOut', 'InOut', 'InOut'), + ], + name='', default='Out') + + property2: HaxeBoolProperty('property2', name='Clamp', default=False) + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Factor', default_value=0.0) + self.add_input('ArmFloatSocket', 'Value 1', default_value=0.0) + self.add_input('ArmFloatSocket', 'Value 2', default_value=1.0) + + self.add_output('ArmFloatSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property2') + layout.prop(self, 'property0') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/math/LN_mix_vector.py b/blender/arm/logicnode/math/LN_mix_vector.py new file mode 100644 index 0000000000..4349de861b --- /dev/null +++ b/blender/arm/logicnode/math/LN_mix_vector.py @@ -0,0 +1,46 @@ +from arm.logicnode.arm_nodes import * + +class VectorMixNode(ArmLogicTreeNode): + """Interpolates between the two given vectors.""" + bl_idname = 'LNVectorMixNode' + bl_label = 'Mix Vector' + arm_section = 'vector' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('Sine', 'Sine', 'Sine'), + ('Quad', 'Quad', 'Quad'), + ('Cubic', 'Cubic', 'Cubic'), + ('Quart', 'Quart', 'Quart'), + ('Quint', 'Quint', 'Quint'), + ('Expo', 'Expo', 'Expo'), + ('Circ', 'Circ', 'Circ'), + ('Back', 'Back', 'Back'), + ('Bounce', 'Bounce', 'Bounce'), + ('Elastic', 'Elastic', 'Elastic'), + ], + name='', default='Linear') + property1: HaxeEnumProperty( + 'property1', + items = [('In', 'In', 'In'), + ('Out', 'Out', 'Out'), + ('InOut', 'InOut', 'InOut'), + ], + name='', default='Out') + + property2: HaxeBoolProperty('property2', name='Clamp', default=False) + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Factor', default_value=0.0) + self.add_input('ArmVectorSocket', 'Vector 1', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmVectorSocket', 'Vector 2', default_value=[1.0, 1.0, 1.0]) + + self.add_output('ArmVectorSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property2') + layout.prop(self, 'property0') + if self.property0 != 'Linear': + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/math/LN_quaternion_math.py b/blender/arm/logicnode/math/LN_quaternion_math.py new file mode 100644 index 0000000000..c0135522cd --- /dev/null +++ b/blender/arm/logicnode/math/LN_quaternion_math.py @@ -0,0 +1,369 @@ +from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation + +class QuaternionMathNode(ArmLogicTreeNode): + """Mathematical operations on quaternions.""" + bl_idname = 'LNQuaternionMathNode' + bl_label = 'Quaternion Math' + bl_description = 'Mathematical operations that can be performed on rotations, when represented as quaternions specifically' + arm_section = 'quaternions' + arm_version = 3 + + def ensure_input_socket(self, socket_number, newclass, newname, default_value=None): + while len(self.inputs) < socket_number: + self.inputs.new('ArmFloatSocket', 'BOGUS') + if len(self.inputs) > socket_number: + if len(self.inputs[socket_number].links) == 1: + source_socket = self.inputs[socket_number].links[0].from_socket + else: + source_socket = None + if ( + self.inputs[socket_number].bl_idname == newclass \ + and self.inputs[socket_number].arm_socket_type != 'NONE' + ): + default_value = self.inputs[socket_number].default_value_raw + self.inputs.remove(self.inputs[socket_number]) + else: + source_socket = None + + + self.inputs.new(newclass, newname) + if default_value != None: + self.inputs[-1].default_value_raw = default_value + self.inputs.move(len(self.inputs)-1, socket_number) + if source_socket is not None: + self.id_data.links.new(source_socket, self.inputs[socket_number]) + + def ensure_output_socket(self, socket_number, newclass, newname): + sink_sockets = [] + while len(self.outputs) < socket_number: + self.outputs.new('ArmFloatSocket', 'BOGUS') + if len(self.outputs) > socket_number: + for link in self.inputs[socket_number].links: + sink_sockets.append(link.to_socket) + self.inputs.remove(self.inputs[socket_number]) + + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + for socket in sink_sockets: + self.id_data.links.new(self.inputs[socket_number], socket) + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(operation_name): + return { + 'Add': 0, + 'Subtract': 0, + 'DotProduct': 0, + 'Multiply': 0, + 'MultiplyFloats': 0, + 'Module': 1, + 'Normalize': 1, + 'GetEuler': 1, + 'FromTo': 2, + 'FromMat': 2, + 'FromRotationMat': 2, + 'ToAxisAngle': 2, + 'Lerp': 3, + 'Slerp': 3, + 'FromAxisAngle': 3, + 'FromEuler': 3 + }.get(operation_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of another operation + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + if select_current in ('Add','Subtract','Multiply','DotProduct') \ + and select_prev in ('Add','Subtract','Multiply','DotProduct'): + pass # same as select_current==select_prev for the sockets + elif select_prev != select_current: + if select_current in ('Add','Subtract','Multiply','DotProduct'): + for i in range( max(len(self.inputs)//2 ,2) ): + self.ensure_input_socket(2*i, 'ArmVectorSocket', 'Quaternion %d XYZ'%i) + self.ensure_input_socket(2*i+1, 'ArmFloatSocket', 'Quaternion %d W'%i, default_value=1.0) + if len(self.inputs)%1: + self.inputs.remove(self.inputs[len(self.inputs)-1]) + elif select_current == 'MultiplyFloats': + self.ensure_input_socket(0, 'ArmVectorSocket', 'Quaternion XYZ') + self.ensure_input_socket(1, 'ArmFloatSocket', 'Quaternion W', default_value=1.0) + for i in range( max(len(self.inputs)-2 ,1) ): + self.ensure_input_socket(i+2, 'ArmFloatSocket', 'Value %d'%i) + elif select_current in ('Module', 'Normalize'): + self.ensure_input_socket(0, 'ArmVectorSocket', 'Quaternion XYZ') + self.ensure_input_socket(1, 'ArmFloatSocket', 'Quaternion W', default_value=1.0) + while len(self.inputs)>2: + self.inputs.remove(self.inputs[2]) + else: + raise ValueError('Internal code of LNQuaternionMathNode failed to deal correctly with math operation "%s". Please report this to the developers.' %select_current) + + if select_current in ('Add','Subtract','Multiply','MultiplyFloats','Normalize'): + self.outputs[0].name = 'XYZ Out' + self.outputs[1].name = 'W Out' + else: + self.outputs[0].name = '[unused]' + self.outputs[1].name = 'Value Out' + + self['property0'] = value + self['property0_proxy'] = value + + + # this property swaperoo is kinda janky-looking, but necessary. + # Read more on LN_rotate_object.py + property0: HaxeEnumProperty( + 'property0', + items = [('Add', 'Add', 'Add'), + ('Subtract', 'Subtract', 'Subtract'), + ('DotProduct', 'Dot Product', 'Dot Product'), + ('Multiply', 'Multiply', 'Multiply'), + ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), + ('Module', 'Module', 'Module'), + ('Normalize', 'Normalize', 'Normalize'), #], + # NOTE: the unused parts need to exist to be read from an old version from the node. + # this is so dumb… + ('Lerp', 'DO NOT USE',''), + ('Slerp', 'DO NOT USE',''), + ('FromTo', 'DO NOT USE',''), + ('FromMat', 'DO NOT USE',''), + ('FromRotationMat', 'DO NOT USE',''), + ('ToAxisAngle', 'DO NOT USE',''), + ('FromAxisAngle', 'DO NOT USE',''), + ('FromEuler', 'DO NOT USE',''), + ('GetEuler', 'DO NOT USE','')], + name='', default='Add') #, set=set_enum, get=get_enum) + property0_proxy: EnumProperty( + items = [('Add', 'Add', 'Add'), + ('Subtract', 'Subtract', 'Subtract'), + ('DotProduct', 'Dot Product', 'Dot Product'), + ('Multiply', 'Multiply', 'Multiply'), + ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), + ('Module', 'Module', 'Module'), + ('Normalize', 'Normalize', 'Normalize')], + name='', default='Add', set=set_enum, get=get_enum) + + + def __init__(self): + super(QuaternionMathNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Quaternion 0 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmFloatSocket', 'Quaternion 0 W', default_value=1) + self.add_input('ArmVectorSocket', 'Quaternion 1 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmFloatSocket', 'Quaternion 1 W', default_value=1) + self.add_output('ArmVectorSocket', 'Result XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_output('ArmFloatSocket', 'Result W', default_value=1) + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0_proxy') # Operation + # Buttons + if (self.get_count_in(self.property0) == 0): + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + if (self.property0 == 'Add') or (self.property0 == 'Subtract') or (self.property0 == 'Multiply') or (self.property0 == 'DotProduct'): + op.name_format = 'Quaternion {0} XYZ;Quaternion {0} W' + else: + op.name_format = 'Value {0}' + if (self.property0 == "MultiplyFloats"): + op.socket_type = 'ArmFloatSocket' + else: + op.socket_type = 'ArmVectorSocket;ArmFloatSocket' + + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if self.property0 != "MultiplyFloats": + op.count = 2 + op.min_inputs = 4 + else: + op.min_inputs = 2 + if len(self.inputs) == 4: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + if self.arm_version == 1 or self.arm_version == 2: + ret=[] + if self.property0 == 'GetEuler': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + elif self.property0 == 'FromEuler': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + preconv = node_tree.nodes.new('LNVectorNode') + ret.append(preconv) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + node_tree.links.new(preconv.outputs[0], newself.inputs[0]) + + preconv.inputs[0].default_value = self.inputs[0].default_value + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[0]) + preconv.inputs[1].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[1]) + preconv.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[2]) + elif self.property0 == 'ToAxisAngle': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + elif self.property0 == 'FromAxisAngle': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + newself.inputs[0].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + newself.inputs[1].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[1]) + elif self.property0 in ('FromMat','FromRotationMat'): + newself = node_tree.nodes.new('LNSeparateTransformNode') + ret.append(newself) + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + + elif self.property0 in ('Lerp','Slerp','FromTo'): + newself = node_tree.nodes.new('LNRotationMathNode') + ret.append(newself) + newself.property0 = self.property0 + + for in1, in2 in zip(self.inputs, newself.inputs): + if in2.bl_idname == 'ArmRotationSocket': + in2.default_value_raw = Rotation.convert_to_quaternion( + in1.default_value,0, + 'EulerAngles','Rad','XZY' + ) + elif in1.bl_idname in ('ArmFloatSocket', 'ArmVectorSocket'): + in2.default_value = in1.default_value + for link in in1.links: + node_tree.links.new(link.from_socket, in2) + + else: + newself = node_tree.nodes.new('LNQuaternionMathNode') + ret.append(newself) + newself.property0 = self.property0 + + # convert the inputs… this is going to be hard lmao. + i_in_1 = 0 + i_in_2 = 0 + while i_in_1 < len(self.inputs): + in1 = self.inputs[i_in_1] + if in1.bl_idname == 'ArmVectorSocket': + # quaternion input: now two sockets, not one. + convnode = node_tree.nodes.new('LNSeparateRotationNode') + convnode.property0 = 'Quaternion' + ret.append(convnode) + if i_in_2 >= len(newself.inputs): + newself.ensure_input_socket(i_in_2, 'ArmVectorSocket', 'Quaternion %d XYZ'%(i_in_1)) + newself.ensure_input_socket(i_in_2+1, 'ArmFloatSocket', 'Quaternion %d W'%(i_in_1), 1.0) + node_tree.links.new(convnode.outputs[0], newself.inputs[i_in_2]) + node_tree.links.new(convnode.outputs[1], newself.inputs[i_in_2+1]) + for link in in1.links: + node_tree.links.new(link.from_socket, convnode.inputs[0]) + i_in_2 +=2 + i_in_1 +=1 + elif in1.bl_idname == 'ArmFloatSocket': + for link in in1.links: + node_tree.links.new(link.from_socket, newself.inputs[i_in_2]) + i_in_1 +=1 + i_in_2 +=1 + else: + raise ValueError('get_replacement_node() for is not LNQuaternionMathNode V1->V2 is not prepared to deal with an input socket of type %s. This is a bug to report to the developers' %in1.bl_idname) + # #### now that the input has been dealt with, let's deal with the output. + if self.property0 in ('FromEuler','FromMat','FromRotationMat','FromAxisAngle','Lerp','Slerp','FromTo'): + # the new self returns a rotation + for link in self.outputs[0].links: + out_sock_i = int( self.property0.endswith('Mat') ) + node_tree.links.new(newself.outputs[out_sock_i], link.to_socket) + elif self.property0 in ('DotProduct','Module'): + # new self returns a float + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + elif self.property0 in ('GetEuler', 'ToAxisAngle'): + # new self returns misc. + for link in self.outputs[0].links: + node_tree.links.new(newself.outputs[0], link.to_socket) + if self.property0 == 'ToAxisAngle': + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + if self.property1: + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + else: + # new self returns a proper quaternion XYZ/W + outlinks = self.outputs[0].links + if len(outlinks)>0: + conv = node_tree.nodes.new('LNRotationNode') + conv.property0='Quaternion' + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + node_tree.links.new(newself.outputs[1], conv.inputs[1]) + for link in outlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + if self.property1: + for link in self.outputs[4].links: # for W + node_tree.links.new(newself.outputs[1], link.to_socket) + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + for node in ret: # update the labels on the node's displays + if node.bl_idname == 'LNSeparateRotationNode': + node.on_property_update(None) + elif node.bl_idname == 'LNRotationNode': + node.on_property_update(None) + elif node.bl_idname == 'LNRotationMathNode': + node.on_update_operation(None) + elif node.bl_idname == 'LNQuaternionMathNode': + node.set_enum(node.get_enum()) + return ret + + # note: keep property1, so that it is actually readable for node conversion. + property1: BoolProperty(name='DEPRECATED', default=False) \ No newline at end of file diff --git a/blender/arm/logicnode/math/LN_rad_to_deg.py b/blender/arm/logicnode/math/LN_rad_to_deg.py new file mode 100644 index 0000000000..c144ae98fd --- /dev/null +++ b/blender/arm/logicnode/math/LN_rad_to_deg.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class RadToDegNode(ArmLogicTreeNode): + """Converts radians to degrees.""" + bl_idname = 'LNRadToDegNode' + bl_label = 'Rad to Deg' + arm_version = 1 + arm_section = 'angle' + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Radians') + + self.add_output('ArmFloatSocket', 'Degrees') diff --git a/blender/arm/logicnode/math/LN_rotation_math.py b/blender/arm/logicnode/math/LN_rotation_math.py new file mode 100644 index 0000000000..fea8e35040 --- /dev/null +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -0,0 +1,121 @@ +from arm.logicnode.arm_nodes import * +from mathutils import Vector + +class RotationMathNode(ArmLogicTreeNode): + """Mathematical operations on rotations.""" + bl_idname = 'LNRotationMathNode' + bl_label = 'Rotation Math' + bl_description = 'Mathematical operations that can be performed on rotations, no matter their internal representation' + arm_section = 'quaternions' + arm_version = 1 + + + @staticmethod + def get_count_in(operation_name): + return { + 'Inverse': 1, + 'Normalize': 1, + 'Compose': 2, + 'Amplify': 2, + 'FromTo': 2, + #'FromRotationMat': 2, + 'Lerp': 3, + 'Slerp': 3, + }.get(operation_name, 0) + + def ensure_input_socket(self, socket_number, newclass, newname): + while len(self.inputs) < socket_number: + self.inputs.new('ArmFloatSocket', 'BOGUS') + if len(self.inputs) > socket_number: + if len(self.inputs[socket_number].links) == 1: + source_socket = self.inputs[socket_number].links[0].from_socket + else: + source_socket = None + self.inputs.remove(self.inputs[socket_number]) + else: + source_socket = None + + + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + if source_socket is not None: + self.id_data.links.new(source_socket, self.inputs[socket_number]) + + def ensure_output_socket(self, socket_number, newclass, newname): + sink_sockets = [] + while len(self.outputs) < socket_number: + self.outputs.new('ArmFloatSocket', 'BOGUS') + if len(self.outputs) > socket_number: + for link in self.inputs[socket_number].links: + sink_sockets.append(link.to_socket) + self.inputs.remove(self.inputs[socket_number]) + + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + for socket in sink_sockets: + self.id_data.links.new(self.inputs[socket_number], socket) + + def on_property_update(self, context): + # Checking the selection of another operation + + + # Rotation as argument 0: + if self.property0 in ('Inverse','Normalize','Amplify'): + self.ensure_input_socket(0, "ArmRotationSocket", "Rotation") + self.ensure_input_socket(1, "ArmFloatSocket", "Amplification factor") + elif self.property0 in ('Slerp','Lerp','Compose'): + self.ensure_input_socket(0, "ArmRotationSocket", "From") + self.ensure_input_socket(1, "ArmRotationSocket", "To") + + if self.property0 == 'Compose': + self.inputs[0].name = 'Outer rotation' + self.inputs[1].name = 'Inner rotation' + else: + self.ensure_input_socket(2, "ArmFloatSocket", "Interpolation factor") + + elif self.property0 == 'FromTo': + self.ensure_input_socket(0, "ArmVectorSocket", "From") + self.ensure_input_socket(1, "ArmVectorSocket", "To") + + # Rotation as argument 1: + if self.property0 in ('Compose','Lerp','Slerp'): + if self.inputs[1].bl_idname != "ArmRotationSocket": + self.replace_input_socket(1, "ArmRotationSocket", "Rotation 2") + if self.property0 == 'Compose': + self.inputs[1].name = "Inner quaternion" + # Float as argument 1: + if self.property0 == 'Amplify': + if self.inputs[1].bl_idname != 'ArmFloatSocket': + self.replace_input_socket(1, "ArmFloatSocket", "Amplification factor") + # Vector as argument 1: + #if self.property0 == 'FromRotationMat': + # # WHAT?? + # pass + + while len(self.inputs) > self.get_count_in(self.property0): + self.inputs.remove(self.inputs[len(self.inputs)-1]) + + + property0: HaxeEnumProperty( + 'property0', + items = [('Compose', 'Compose (multiply)', 'compose (multiply) two rotations. Note that order of the composition matters.'), + ('Amplify', 'Amplify (multiply by float)', 'Amplify or diminish the effect of a rotation'), + #('Normalize', 'Normalize', 'Normalize'), + ('Inverse', 'Get Inverse', 'from r, get the rotation r2 so that " r×r2=r2×r= " '), + ('Lerp', 'Lerp', 'Linearly interpolation'), + ('Slerp', 'Slerp', 'Spherical linear interpolation'), + ('FromTo', 'From To', 'From direction To direction'), + #('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat') + ], + name='', default='Compose', update=on_property_update) + + #def __init__(self): + # array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmRotationSocket', 'Outer rotation', default_value=(0.0, 0.0, 0.0, 1.0) ) + self.add_input('ArmRotationSocket', 'Inner rotation', default_value=(0.0, 0.0, 0.0, 1.0) ) + self.add_output('ArmRotationSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') # Operation diff --git a/blender/arm/logicnode/math/LN_screen_to_world_space.py b/blender/arm/logicnode/math/LN_screen_to_world_space.py new file mode 100644 index 0000000000..1556d930b9 --- /dev/null +++ b/blender/arm/logicnode/math/LN_screen_to_world_space.py @@ -0,0 +1,42 @@ +from arm.logicnode.arm_nodes import * + + +class ScreenToWorldSpaceNode(ArmLogicTreeNode): + """Transforms the given screen coordinates into world coordinates.""" + bl_idname = 'LNScreenToWorldSpaceNode' + bl_label = 'Screen to World Space' + arm_section = 'matrix' + arm_version = 1 + max_outputs = 8 + + property0: HaxeBoolProperty('property0', name='Separator Out', default=False) + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Screen X') + self.add_input('ArmIntSocket', 'Screen Y') + + self.add_output('ArmVectorSocket', 'World') + self.add_output('ArmVectorSocket', 'Direction') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') # Separator Out + if self.property0: + if len(self.outputs) < self.max_outputs: + self.outputs.remove(self.outputs.values()[-1]) # Direction vector + self.add_output('ArmFloatSocket', 'X') # World X + self.add_output('ArmFloatSocket', 'Y') # World Y + self.add_output('ArmFloatSocket', 'Z') # World Z + self.add_output('ArmVectorSocket', 'Direction') # Vector + self.add_output('ArmFloatSocket', 'X') # Direction X + self.add_output('ArmFloatSocket', 'Y') # Direction Y + self.add_output('ArmFloatSocket', 'Z') # Direction Z + else: + if len(self.outputs) == self.max_outputs: + self.outputs.remove(self.outputs.values()[-1]) # Z + self.outputs.remove(self.outputs.values()[-1]) # Y + self.outputs.remove(self.outputs.values()[-1]) # X + self.outputs.remove(self.outputs.values()[-1]) # Direction + self.outputs.remove(self.outputs.values()[-1]) # Z + self.outputs.remove(self.outputs.values()[-1]) # Y + self.outputs.remove(self.outputs.values()[-1]) # X + self.add_output('ArmVectorSocket', 'Direction') diff --git a/blender/arm/logicnode/math/LN_separate_hsv.py b/blender/arm/logicnode/math/LN_separate_hsv.py new file mode 100644 index 0000000000..11afab3b3e --- /dev/null +++ b/blender/arm/logicnode/math/LN_separate_hsv.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + + +class SeparateColorHSVNode(ArmLogicTreeNode): + """Splits the given color into its HSVA components (hue, saturation, value, and alpha). + If the input color is `null`, the outputs are each set to `0.0`. + formula from: https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + """ + bl_idname = 'LNSeparateColorHSVNode' + bl_label = 'Separate HSVA' + arm_section = 'color' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + + self.add_output('ArmFloatSocket', 'H') + self.add_output('ArmFloatSocket', 'S') + self.add_output('ArmFloatSocket', 'V') + self.add_output('ArmFloatSocket', 'A') \ No newline at end of file diff --git a/blender/arm/logicnode/math/LN_separate_rgb.py b/blender/arm/logicnode/math/LN_separate_rgb.py new file mode 100644 index 0000000000..fe5ccbbcdb --- /dev/null +++ b/blender/arm/logicnode/math/LN_separate_rgb.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + + +class SeparateColorNode(ArmLogicTreeNode): + """Splits the given color into its RGBA components (red, green, blue, and alpha). + If the input color is `null`, the outputs are each set to `0.0`. + """ + bl_idname = 'LNSeparateColorNode' + bl_label = 'Separate RGBA' + arm_section = 'color' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + + self.add_output('ArmFloatSocket', 'R') + self.add_output('ArmFloatSocket', 'G') + self.add_output('ArmFloatSocket', 'B') + self.add_output('ArmFloatSocket', 'A') + + 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/math/LN_separate_xyz.py b/blender/arm/logicnode/math/LN_separate_xyz.py new file mode 100644 index 0000000000..4f7ac73509 --- /dev/null +++ b/blender/arm/logicnode/math/LN_separate_xyz.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SeparateVectorNode(ArmLogicTreeNode): + """Splits the given vector into X, Y and Z.""" + bl_idname = 'LNSeparateVectorNode' + bl_label = 'Separate XYZ' + arm_section = 'vector' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Vector') + + self.add_output('ArmFloatSocket', 'X') + self.add_output('ArmFloatSocket', 'Y') + self.add_output('ArmFloatSocket', 'Z') diff --git a/blender/arm/logicnode/math/LN_tween_float.py b/blender/arm/logicnode/math/LN_tween_float.py new file mode 100644 index 0000000000..15570523a0 --- /dev/null +++ b/blender/arm/logicnode/math/LN_tween_float.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class TweenFloatNode(ArmLogicTreeNode): + """Tween a float value. + + @input Start: Start tweening + @input Stop: Stop a tweening. tweening can be re-started via the `Start`input + @input From: Tween start value + @input To: Tween final value + @input Duration: Duartion of the tween in seconds + + @output Out: Executed immidiately after `Start` or `Stop` is called + @output Tick: Executed at every time step in the tween duration + @output Done: Executed when tween is successfully completed. Not executed if tweening is stopped mid-way + @output Value: Current tween value + """ + bl_idname = 'LNTweenFloatNode' + bl_label = 'Tween Float' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('SineIn', 'SineIn', 'SineIn'), + ('SineOut', 'SineOut', 'SineOut'), + ('SineInOut', 'SineInOut', 'SineInOut'), + ('QuadIn', 'QuadIn', 'QuadIn'), + ('QuadOut', 'QuadOut', 'QuadOut'), + ('QuadInOut', 'QuadInOut', 'QuadInOut'), + ('CubicIn', 'CubicIn', 'CubicIn'), + ('CubicOut', 'CubicOut', 'CubicOut'), + ('CubicInOut', 'CubicInOut', 'CubicInOut'), + ('QuartIn', 'QuartIn', 'QuartIn'), + ('QuartOut', 'QuartOut', 'QuartOut'), + ('QuartInOut', 'QuartInOut', 'QuartInOut'), + ('QuintIn', 'QuintIn', 'QuintIn'), + ('QuintOut', 'QuintOut', 'QuintOut'), + ('QuintInOut', 'QuintInOut', 'QuintInOut'), + ('ExpoIn', 'ExpoIn', 'ExpoIn'), + ('ExpoOut', 'ExpoOut', 'ExpoOut'), + ('ExpoInOut', 'ExpoInOut', 'ExpoInOut'), + ('CircIn', 'CircIn', 'CircIn'), + ('CircOut', 'CircOut', 'CircOut'), + ('CircInOut', 'CircInOut', 'CircInOut'), + ('BackIn', 'BackIn', 'BackIn'), + ('BackOut', 'BackOut', 'BackOut'), + ('BackInOut', 'BackInOut', 'BackInOut')], + name='', default='Linear') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmFloatSocket', 'From', default_value=0.0) + self.add_input('ArmFloatSocket', 'To', default_value=0.0) + self.add_input('ArmFloatSocket', 'Duration', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Tick') + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmFloatSocket', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/math/LN_tween_rotation.py b/blender/arm/logicnode/math/LN_tween_rotation.py new file mode 100644 index 0000000000..59efe195ef --- /dev/null +++ b/blender/arm/logicnode/math/LN_tween_rotation.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class TweenFloatNode(ArmLogicTreeNode): + """Tween rotation. + + @input Start: Start tweening + @input Stop: Stop a tweening. tweening can be re-started via the `Start`input + @input From: Tween start value + @input To: Tween final value + @input Duration: Duartion of the tween in seconds + + @output Out: Executed immidiately after `Start` or `Stop` is called + @output Tick: Executed at every time step in the tween duration + @output Done: Executed when tween is successfully completed. Not executed if tweening is stopped mid-way + @output Value: Current tween value + """ + bl_idname = 'LNTweenRotationNode' + bl_label = 'Tween Rotation' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('SineIn', 'SineIn', 'SineIn'), + ('SineOut', 'SineOut', 'SineOut'), + ('SineInOut', 'SineInOut', 'SineInOut'), + ('QuadIn', 'QuadIn', 'QuadIn'), + ('QuadOut', 'QuadOut', 'QuadOut'), + ('QuadInOut', 'QuadInOut', 'QuadInOut'), + ('CubicIn', 'CubicIn', 'CubicIn'), + ('CubicOut', 'CubicOut', 'CubicOut'), + ('CubicInOut', 'CubicInOut', 'CubicInOut'), + ('QuartIn', 'QuartIn', 'QuartIn'), + ('QuartOut', 'QuartOut', 'QuartOut'), + ('QuartInOut', 'QuartInOut', 'QuartInOut'), + ('QuintIn', 'QuintIn', 'QuintIn'), + ('QuintOut', 'QuintOut', 'QuintOut'), + ('QuintInOut', 'QuintInOut', 'QuintInOut'), + ('ExpoIn', 'ExpoIn', 'ExpoIn'), + ('ExpoOut', 'ExpoOut', 'ExpoOut'), + ('ExpoInOut', 'ExpoInOut', 'ExpoInOut'), + ('CircIn', 'CircIn', 'CircIn'), + ('CircOut', 'CircOut', 'CircOut'), + ('CircInOut', 'CircInOut', 'CircInOut'), + ('BackIn', 'BackIn', 'BackIn'), + ('BackOut', 'BackOut', 'BackOut'), + ('BackInOut', 'BackInOut', 'BackInOut')], + name='', default='Linear') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmRotationSocket', 'From') + self.add_input('ArmRotationSocket', 'To') + self.add_input('ArmFloatSocket', 'Duration', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Tick') + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmRotationSocket', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/math/LN_tween_transform.py b/blender/arm/logicnode/math/LN_tween_transform.py new file mode 100644 index 0000000000..e16ac860cc --- /dev/null +++ b/blender/arm/logicnode/math/LN_tween_transform.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class TweenTransformNode(ArmLogicTreeNode): + """Tween Transform. + + @input Start: Start tweening + @input Stop: Stop a tweening. tweening can be re-started via the `Start`input + @input From: Tween start value + @input To: Tween final value + @input Duration: Duartion of the tween in seconds + + @output Out: Executed immidiately after `Start` or `Stop` is called + @output Tick: Executed at every time step in the tween duration + @output Done: Executed when tween is successfully completed. Not executed if tweening is stopped mid-way + @output Value: Current tween value + """ + bl_idname = 'LNTweenTransformNode' + bl_label = 'Tween Transform' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('SineIn', 'SineIn', 'SineIn'), + ('SineOut', 'SineOut', 'SineOut'), + ('SineInOut', 'SineInOut', 'SineInOut'), + ('QuadIn', 'QuadIn', 'QuadIn'), + ('QuadOut', 'QuadOut', 'QuadOut'), + ('QuadInOut', 'QuadInOut', 'QuadInOut'), + ('CubicIn', 'CubicIn', 'CubicIn'), + ('CubicOut', 'CubicOut', 'CubicOut'), + ('CubicInOut', 'CubicInOut', 'CubicInOut'), + ('QuartIn', 'QuartIn', 'QuartIn'), + ('QuartOut', 'QuartOut', 'QuartOut'), + ('QuartInOut', 'QuartInOut', 'QuartInOut'), + ('QuintIn', 'QuintIn', 'QuintIn'), + ('QuintOut', 'QuintOut', 'QuintOut'), + ('QuintInOut', 'QuintInOut', 'QuintInOut'), + ('ExpoIn', 'ExpoIn', 'ExpoIn'), + ('ExpoOut', 'ExpoOut', 'ExpoOut'), + ('ExpoInOut', 'ExpoInOut', 'ExpoInOut'), + ('CircIn', 'CircIn', 'CircIn'), + ('CircOut', 'CircOut', 'CircOut'), + ('CircInOut', 'CircInOut', 'CircInOut'), + ('BackIn', 'BackIn', 'BackIn'), + ('BackOut', 'BackOut', 'BackOut'), + ('BackInOut', 'BackInOut', 'BackInOut')], + name='', default='Linear') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmDynamicSocket', 'From') + self.add_input('ArmDynamicSocket', 'To') + self.add_input('ArmFloatSocket', 'Duration', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Tick') + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmDynamicSocket', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/math/LN_tween_vector.py b/blender/arm/logicnode/math/LN_tween_vector.py new file mode 100644 index 0000000000..2570a22c25 --- /dev/null +++ b/blender/arm/logicnode/math/LN_tween_vector.py @@ -0,0 +1,67 @@ +from arm.logicnode.arm_nodes import * + + +class TweenVectorNode(ArmLogicTreeNode): + """Tween a vector value. + + @input Start: Start tweening + @input Stop: Stop a tweening. tweening can be re-started via the `Start`input + @input From: Tween start value + @input To: Tween final value + @input Duration: Duartion of the tween in seconds + + @output Out: Executed immidiately after `Start` or `Stop` is called + @output Tick: Executed at every time step in the tween duration + @output Done: Executed when tween is successfully completed. Not executed if tweening is stopped mid-way + @output Value: Current tween value + """ + bl_idname = 'LNTweenVectorNode' + bl_label = 'Tween Vector' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Linear', 'Linear', 'Linear'), + ('SineIn', 'SineIn', 'SineIn'), + ('SineOut', 'SineOut', 'SineOut'), + ('SineInOut', 'SineInOut', 'SineInOut'), + ('QuadIn', 'QuadIn', 'QuadIn'), + ('QuadOut', 'QuadOut', 'QuadOut'), + ('QuadInOut', 'QuadInOut', 'QuadInOut'), + ('CubicIn', 'CubicIn', 'CubicIn'), + ('CubicOut', 'CubicOut', 'CubicOut'), + ('CubicInOut', 'CubicInOut', 'CubicInOut'), + ('QuartIn', 'QuartIn', 'QuartIn'), + ('QuartOut', 'QuartOut', 'QuartOut'), + ('QuartInOut', 'QuartInOut', 'QuartInOut'), + ('QuintIn', 'QuintIn', 'QuintIn'), + ('QuintOut', 'QuintOut', 'QuintOut'), + ('QuintInOut', 'QuintInOut', 'QuintInOut'), + ('ExpoIn', 'ExpoIn', 'ExpoIn'), + ('ExpoOut', 'ExpoOut', 'ExpoOut'), + ('ExpoInOut', 'ExpoInOut', 'ExpoInOut'), + ('CircIn', 'CircIn', 'CircIn'), + ('CircOut', 'CircOut', 'CircOut'), + ('CircInOut', 'CircInOut', 'CircInOut'), + ('BackIn', 'BackIn', 'BackIn'), + ('BackOut', 'BackOut', 'BackOut'), + ('BackInOut', 'BackInOut', 'BackInOut')], + name='', default='Linear') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmVectorSocket', 'From') + self.add_input('ArmVectorSocket', 'To') + self.add_input('ArmFloatSocket', 'Duration', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Tick') + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmVectorSocket', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/math/LN_vector_clamp.py b/blender/arm/logicnode/math/LN_vector_clamp.py new file mode 100644 index 0000000000..c32198ae32 --- /dev/null +++ b/blender/arm/logicnode/math/LN_vector_clamp.py @@ -0,0 +1,41 @@ +from arm.logicnode.arm_nodes import * + + +class VectorClampToSizeNode(ArmLogicTreeNode): + """Clamp the vector's value inside the given range and return the result as a new vector. + + @option Clamping Mode: Whether to clamp the length of the vector + or the value of each individual component. + """ + bl_idname = 'LNVectorClampToSizeNode' + bl_label = 'Vector Clamp' + arm_section = 'vector' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + name='Clamping Mode', default='length', + description='Whether to clamp the length of the vector or the value of each individual component', + items=[ + ('length', 'Length', 'Clamp the length (magnitude) of the vector'), + ('components', 'Components', 'Clamp the individual components of the vector'), + ] + ) + + def draw_buttons(self, context, layout): + col = layout.column() + col.label(text="Clamping Mode:") + col.prop(self, 'property0', expand=True) + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Vector In', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmFloatSocket', 'Min') + self.add_input('ArmFloatSocket', 'Max') + + self.add_output('ArmVectorSocket', 'Vector 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) diff --git a/blender/arm/logicnode/math/LN_vector_math.py b/blender/arm/logicnode/math/LN_vector_math.py new file mode 100644 index 0000000000..8a386239c7 --- /dev/null +++ b/blender/arm/logicnode/math/LN_vector_math.py @@ -0,0 +1,152 @@ +from arm.logicnode.arm_nodes import * + + +class VectorMathNode(ArmLogicTreeNode): + """Mathematical operations on vectors.""" + bl_idname = 'LNVectorMathNode' + bl_label = 'Vector Math' + arm_section = 'vector' + arm_version = 1 + + def get_bool(self): + return self.get('property1', False) + + def set_bool(self, value): + self['property1'] = value + if value: + if self.property0 in ('Length', 'Distance', 'Dot Product'): + self.outputs.remove(self.outputs.values()[-1]) # Distance/Length/Scalar + self.add_output('ArmFloatSocket', 'X') # Result X + self.add_output('ArmFloatSocket', 'Y') # Result Y + self.add_output('ArmFloatSocket', 'Z') # Result Z + if self.property0 == 'Length': + self.add_output('ArmFloatSocket', 'Length') # Length + if self.property0 == 'Distance': + self.add_output('ArmFloatSocket', 'Distance') # Distance + if self.property0 == 'Dot Product': + self.add_output('ArmFloatSocket', 'Scalar') # Scalar + else: + if self.property0 in ('Length', 'Distance', 'Dot Product') and len(self.outputs) > 1: + self.outputs.remove(self.outputs.values()[-1]) # Distance/Length/Scalar + # Remove X, Y, Z + for i in range(3): + if len(self.outputs) > 1: + self.outputs.remove(self.outputs.values()[-1]) + else: + break + if self.property0 == 'Length': + self.add_output('ArmFloatSocket', 'Length') # Length + if self.property0 == 'Distance': + self.add_output('ArmFloatSocket', 'Distance') # Distance + if self.property0 == 'Dot Product': + self.add_output('ArmFloatSocket', 'Scalar') # Scalar + + property1: HaxeBoolProperty('property1', name='Separator Out', default=False, set=set_bool, get=get_bool) + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(operation_name): + return { + 'Add': 0, + 'Subtract': 0, + 'Average': 0, + 'Dot Product': 0, + 'Cross Product': 0, + 'Multiply': 0, + 'MultiplyFloats': 0, + 'Distance': 2, + 'Reflect': 2, + 'Normalize': 1, + 'Length': 1 + }.get(operation_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of another operation + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + if select_prev != select_current: + if select_prev in ('Distance', 'Length', 'Dot Product'): + self.outputs.remove(self.outputs.values()[-1]) + # Many arguments: Add, Subtract, Average, Dot Product, Cross Product, Multiply, MultiplyFloats + if self.get_count_in(select_current) == 0: + if select_current == "MultiplyFloats" or select_prev == "MultiplyFloats": + while (len(self.inputs) > 1): + self.inputs.remove(self.inputs.values()[-1]) + if select_current == "MultiplyFloats": + self.add_input('ArmFloatSocket', 'Value ' + str(len(self.inputs))) + else: + while (len(self.inputs) < 2): + self.add_input('ArmVectorSocket', 'Value ' + str(len(self.inputs))) + if select_current == 'Dot Product': + self.add_output('ArmFloatSocket', 'Scalar') + # 2 arguments: Distance, Reflect + if self.get_count_in(select_current) == 2: + count = 2 + if select_prev == "MultiplyFloats": + count = 1 + while len(self.inputs) > count: + self.inputs.remove(self.inputs.values()[-1]) + while len(self.inputs) < 2: + self.add_input('ArmVectorSocket', 'Value ' + str(len(self.inputs))) + if select_current == 'Distance': + self.add_output('ArmFloatSocket', 'Distance') + # 1 argument: Normalize, Length + if self.get_count_in(select_current) == 1: + while len(self.inputs) > 1: + self.inputs.remove(self.inputs.values()[-1]) + if select_current == 'Length': + self.add_output('ArmFloatSocket', 'Length') + self['property0'] = value + + property0: HaxeEnumProperty( + 'property0', + items=[('Add', 'Add', 'Add'), + ('Dot Product', 'Dot Product', 'Dot Product'), + ('Multiply', 'Multiply', 'Multiply'), + ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), + ('Normalize', 'Normalize', 'Normalize'), + ('Subtract', 'Subtract', 'Subtract'), + ('Average', 'Average', 'Average'), + ('Cross Product', 'Cross Product', 'Cross Product'), + ('Length', 'Length', 'Length'), + ('Distance', 'Distance', 'Distance'), + ('Reflect', 'Reflect', 'Reflect')], + name='', default='Add', set=set_enum, get=get_enum) + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Value 0', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmVectorSocket', 'Value 1', default_value=[0.0, 0.0, 0.0]) + + self.add_output('ArmVectorSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property1') # Separator Out + layout.prop(self, 'property0') # Operation + # Buttons + if self.get_count_in(self.property0) == 0: + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.name_format = 'Value {0}' + if self.property0 == "MultiplyFloats": + op.socket_type = 'ArmFloatSocket' + else: + op.socket_type = 'ArmVectorSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == 2: + column.enabled = False + + def draw_label(self) -> str: + return f'{self.bl_label}: {self.property0}' diff --git a/blender/arm/logicnode/math/LN_vector_move_towards.py b/blender/arm/logicnode/math/LN_vector_move_towards.py new file mode 100644 index 0000000000..218b3f6950 --- /dev/null +++ b/blender/arm/logicnode/math/LN_vector_move_towards.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class VectorMoveTowardsNode(ArmLogicTreeNode): + """Add a constant value to the given vector until it reach the target vector.""" + bl_idname = 'LNVectorMoveTowardsNode' + bl_label = 'Vector Move Towards' + arm_section = 'vector' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Vector 1', default_value=[0.0, 0.0, 0.0]) + self.add_input('ArmVectorSocket', 'Vector 2', default_value=[1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'Delta', default_value=0.1) + + self.add_output('ArmVectorSocket', 'Result') \ No newline at end of file diff --git a/blender/arm/logicnode/math/LN_world_to_screen_space.py b/blender/arm/logicnode/math/LN_world_to_screen_space.py new file mode 100644 index 0000000000..b3f16b6a26 --- /dev/null +++ b/blender/arm/logicnode/math/LN_world_to_screen_space.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class WorldToScreenSpaceNode(ArmLogicTreeNode): + """Transforms the given world coordinates into screen coordinates.""" + bl_idname = 'LNWorldToScreenSpaceNode' + bl_label = 'World to Screen Space' + arm_section = 'matrix' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'World') + + self.add_output('ArmVectorSocket', 'Screen') diff --git a/blender/arm/logicnode/math/__init__.py b/blender/arm/logicnode/math/__init__.py new file mode 100644 index 0000000000..cf41425282 --- /dev/null +++ b/blender/arm/logicnode/math/__init__.py @@ -0,0 +1,7 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Math') +add_node_section(name='angle', category='Math') +add_node_section(name='matrix', category='Math') +add_node_section(name='color', category='Math') +add_node_section(name='vector', category='Math') diff --git a/blender/arm/logicnode/miscellaneous/LN_boolean_to_int.py b/blender/arm/logicnode/miscellaneous/LN_boolean_to_int.py new file mode 100644 index 0000000000..c95b855d83 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_boolean_to_int.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class IntFromBooleanNode(ArmLogicTreeNode): + """Returns an int depending on the respective boolean state.""" + bl_idname = 'LNIntFromBooleanNode' + bl_label = 'Boolean to Int' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmBoolSocket', 'Bool') + + self.outputs.new('ArmIntSocket', 'Int') diff --git a/blender/arm/logicnode/miscellaneous/LN_boolean_to_vector.py b/blender/arm/logicnode/miscellaneous/LN_boolean_to_vector.py new file mode 100644 index 0000000000..5bcb500261 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_boolean_to_vector.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class VectorFromBooleanNode(ArmLogicTreeNode): + """Returns a vector depending on the respective boolean state.""" + bl_idname = 'LNVectorFromBooleanNode' + bl_label = 'Boolean to Vector' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmBoolSocket', 'X') + self.inputs.new('ArmBoolSocket', '-X') + self.inputs.new('ArmBoolSocket', 'Y') + self.inputs.new('ArmBoolSocket', '-Y') + self.inputs.new('ArmBoolSocket', 'Z') + self.inputs.new('ArmBoolSocket', '-Z') + + self.outputs.new('ArmVectorSocket', 'Vector') diff --git a/blender/arm/logicnode/miscellaneous/LN_call_group.py b/blender/arm/logicnode/miscellaneous/LN_call_group.py new file mode 100644 index 0000000000..d41f51f2d6 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_call_group.py @@ -0,0 +1,143 @@ +import bpy + +import arm.utils +from arm.logicnode.arm_nodes import * + + +class CallGroupNode(ArmLogicTreeNode): + """Calls the given group of nodes.""" + bl_idname = 'LNCallGroupNode' + bl_label = 'Call Node Group' + arm_section = 'group' + arm_version = 3 + + def __init__(self): + self.register_id() + + def arm_init(self, context): + pass + + # Function to add input sockets and re-link sockets + def update_inputs(self, tree, node, inp_sockets, in_links): + count = 0 + for output in node.outputs: + _, c_socket = arm.node_utils.output_get_connected_node(output) + if c_socket is not None: + current_socket = self.add_input(c_socket.bl_idname, output.name) + if(count < len(in_links)): + # Preserve default values in input sockets + inp_sockets[count].copy_defaults(current_socket) + for link in in_links[count]: + tree.links.new(link, current_socket) + else: + current_socket = self.add_input('ArmAnySocket', output.name) + current_socket.display_label = output.name + if(count < len(in_links)): + for link in in_links[count]: + tree.links.new(link, current_socket) + count = count + 1 + + # Function to add output sockets and re-link sockets + def update_outputs(self, tree, node, out_links): + count = 0 + for input in node.inputs: + _, c_socket = arm.node_utils.input_get_connected_node(input) + if c_socket is not None: + current_socket = self.add_output(c_socket.bl_idname, input.name) + if(count < len(out_links)): + for link in out_links[count]: + nlink = tree.links.new(current_socket, link) + nlink.is_valid = True + nlink.is_muted = False + else: + current_socket = self.add_output('ArmAnySocket', input.name) + current_socket.display_label = input.name + if(count < len(out_links)): + for link in out_links[count]: + tree.links.new(current_socket, link) + count = count + 1 + + def remove_tree(self): + self.group_tree = None + + def update_sockets(self, context): + # List to store from and to sockets of connected nodes + from_socket_list = [] + to_socket_list = [] + inp_socket_list = [] + tree = self.get_tree() + + # Loop through each input socket + for inp in self.inputs: + link_per_socket = [] + #Loop through each link to the socket + for link in inp.links: + link_per_socket.append(link.from_socket) + from_socket_list.append(link_per_socket) + inp_socket_list.append(inp) + + # Loop through each output socket + for out in self.outputs: + link_per_socket = [] + # Loop through each link to the socket + for link in out.links: + link_per_socket.append(link.to_socket) + to_socket_list.append(link_per_socket) + + # Remove all output sockets + for output in self.outputs: + self.outputs.remove(output) + # Search for Group Input/Output + if self.group_tree is not None: + for node in self.group_tree.nodes: + if node.bl_idname == 'LNGroupInputsNode': + # Update input sockets + self.update_inputs(tree, node, inp_socket_list, from_socket_list) + break + for node in self.group_tree.nodes: + if node.bl_idname == 'LNGroupOutputsNode': + # Update output sockets + self.update_outputs(tree, node, to_socket_list) + break + #Remove all old input sockets after setting defaults + for inp in inp_socket_list: + self.inputs.remove(inp) + + # Prperty to store group tree pointer + group_tree: PointerProperty(name='Group', type=bpy.types.NodeTree, update=update_sockets) + + def draw_label(self) -> str: + if self.group_tree is not None: + return f'Group: {self.group_tree.name}' + return self.bl_label + + # Draw node UI + def draw_buttons(self, context, layout): + col = layout.column() + row_name = col.row(align=True) + row_add = col.row(align=True) + row_ops = col.row() + if self.group_tree is None: + op = row_add.operator('arm.add_group_tree', icon='PLUS', text='New Group') + op.node_index = self.get_id_str() + op = row_name.operator('arm.search_group_tree', text='', icon='VIEWZOOM') + op.node_index = self.get_id_str() + if self.group_tree: + row_name.prop(self.group_tree, 'name', text='') + row_copy = row_name.split(align=True) + row_copy.alignment = 'CENTER' + fake_user = 1 if self.group_tree.use_fake_user else 0 + op = row_copy.operator('arm.copy_group_tree', text=str(self.group_tree.users - fake_user)) + op.node_index = self.get_id_str() + row_name.prop(self.group_tree, 'use_fake_user', text='') + op = row_name.operator('arm.unlink_group_tree', icon='X', text='') + op.node_index = self.get_id_str() + row_ops.enabled = not self.group_tree is None + op = row_ops.operator('arm.edit_group_tree', icon='FULLSCREEN_ENTER', text='Edit tree') + op.node_index = self.get_id_str() + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1, 2): + raise LookupError() + + return node_tree.nodes.new('LNCallGroupNode') \ No newline at end of file diff --git a/blender/arm/logicnode/miscellaneous/LN_default_if_null.py b/blender/arm/logicnode/miscellaneous/LN_default_if_null.py new file mode 100644 index 0000000000..e1e5f2dbf5 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_default_if_null.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class DefaultIfNullNode(ArmLogicTreeNode): + """Returns the connected value only if it is not `null`, otherwise the `default` value is returned. + + @input Value: the one that will be eventually null + @input Default: will be returned in case the primary value is null + """ + bl_idname = 'LNDefaultIfNullNode' + bl_label = 'Default if Null' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmDynamicSocket', 'Value In') + self.inputs.new('ArmDynamicSocket', 'Default') + + self.outputs.new('ArmDynamicSocket', 'Value Out') diff --git a/blender/arm/logicnode/miscellaneous/LN_get_application_time.py b/blender/arm/logicnode/miscellaneous/LN_get_application_time.py new file mode 100644 index 0000000000..ae2ba85a75 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_get_application_time.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class TimeNode(ArmLogicTreeNode): + """Returns the application execution time and the delta time.""" + bl_idname = 'LNTimeNode' + bl_label = 'Get Application Time' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Time') + self.add_output('ArmFloatSocket', 'Delta') diff --git a/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py b/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py new file mode 100644 index 0000000000..e6e04394b3 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class GetDebugConsoleSettings(ArmLogicTreeNode): + """Returns the debug console settings.""" + bl_idname = 'LNGetDebugConsoleSettings' + bl_label = 'Get Debug Console Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmBoolSocket', 'Visible') + self.add_output('ArmFloatSocket', 'Scale') + self.add_output('ArmStringSocket', 'Position') diff --git a/blender/arm/logicnode/miscellaneous/LN_get_display_resolution.py b/blender/arm/logicnode/miscellaneous/LN_get_display_resolution.py new file mode 100644 index 0000000000..411e298cca --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_get_display_resolution.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class DisplayInfoNode(ArmLogicTreeNode): + """Returns the current display resolution. + + @seeNode Get Window Resolution + """ + bl_idname = 'LNDisplayInfoNode' + bl_label = 'Get Display Resolution' + arm_section = 'screen' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'Width') + self.add_output('ArmIntSocket', 'Height') diff --git a/blender/arm/logicnode/miscellaneous/LN_get_fps.py b/blender/arm/logicnode/miscellaneous/LN_get_fps.py new file mode 100644 index 0000000000..4bc298d9f4 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_get_fps.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class GetFPSNode(ArmLogicTreeNode): + """Get the frames per second count.""" + bl_idname = 'LNGetFPSNode' + bl_label = 'Get Frames Per Second' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'Count') diff --git a/blender/arm/logicnode/miscellaneous/LN_get_window_resolution.py b/blender/arm/logicnode/miscellaneous/LN_get_window_resolution.py new file mode 100644 index 0000000000..d2fe52b639 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_get_window_resolution.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class WindowInfoNode(ArmLogicTreeNode): + """Returns the current window resolution. + + @seeNode Get Display Resolution + """ + bl_idname = 'LNWindowInfoNode' + bl_label = 'Get Window Resolution' + arm_section = 'screen' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'Width') + self.add_output('ArmIntSocket', 'Height') diff --git a/blender/arm/logicnode/miscellaneous/LN_group_input.py b/blender/arm/logicnode/miscellaneous/LN_group_input.py new file mode 100644 index 0000000000..018c3b35e6 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_group_input.py @@ -0,0 +1,217 @@ +import bpy + +import arm.utils +import arm.node_utils +from arm.logicnode.arm_nodes import * +import arm.logicnode.miscellaneous.LN_call_group as LN_call_group + + +class GroupInputsNode(ArmLogicTreeNode): + """Input for a node group.""" + bl_idname = 'LNGroupInputsNode' + bl_label = 'Group Input Node' + arm_section = 'group' + arm_version = 3 + + def __init__(self): + self.register_id() + + # Active socket selected + active_output: IntProperty(name='active_output', description='', default=0) + + # Flag to store invalid links + invalid_link: BoolProperty(name='invalid_link', description='', default=False) + + # Override copy prevention in certain situations such as copying entire group + copy_override: BoolProperty(name='copy override', description='', default=False) + + def init(self, context): + tree = bpy.context.space_data.edit_tree + node_count = 0 + for node in tree.nodes: + if node.bl_idname == 'LNGroupInputsNode': + node_count += 1 + if node_count > 1: + arm.log.warn("Only one group input node per node tree is allowed") + self.mute = True + else: + super().init(context) + + # Prevent copying of group node + def copy(self, node): + if not self.copy_override: + self.mute = True + self.outputs.clear() + self.copy_override = False + + def arm_init(self, context): + if not self.mute: + self.add_socket() + + # Called when link is created + def insert_link(self, link): + from_socket = link.from_socket + to_node = link.to_node + to_socket = None + # Recursively search for other socket in case of reroutes + if to_node.type == 'REROUTE': + _, to_socket = arm.node_utils.output_get_connected_node(to_node.outputs[0]) + else: + to_socket = link.to_socket + if to_socket is not None: + index = self.get_socket_index(from_socket) + # If socket connected to ArmAnySocket, link is invalid + if to_socket.bl_idname == 'ArmAnySocket': + self.invalid_link = True + else: + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + # Change socket type according to the new link + node.change_input_socket(to_socket.bl_idname, index, link.from_socket.display_label) + + # Use update method to remove invalid links + def update(self): + super().update() + if self.invalid_link: + self.remove_invalid_links() + + # Called when name of the socket is changed + def socket_name_update(self, socket): + index = self.get_socket_index(socket) + # Update socket names of the related call group nodes + call_node_groups = self.get_call_group_nodes() + for node in call_node_groups: + inp_socket = node.inputs[index] + if inp_socket.bl_idname == 'ArmAnySocket': + inp_socket.display_label = socket.display_label + else: + inp_socket.name = socket.display_label + + # Recursively search and remove invalid links + def remove_invalid_links(self): + for output in self.outputs: + for link in output.links: + if link.to_socket.bl_idname == 'ArmAnySocket': + tree = self.get_tree() + tree.links.remove(link) + break + self.invalid_link = False + + # Function to move socket up and handle the same in related call group nodes + def move_socket_up(self): + if self.active_output > 0: + self.outputs.move(self.active_output, self.active_output - 1) + call_node_groups = self.get_call_group_nodes() + for nodes in call_node_groups: + nodes.inputs.move(self.active_output, self.active_output - 1) + self.active_output = self.active_output - 1 + + # Function to move socket down and handle the same in related call group nodes + def move_socket_down(self): + if self.active_output < len(self.outputs) - 1: + self.outputs.move(self.active_output, self.active_output + 1) + call_node_groups = self.get_call_group_nodes() + for nodes in call_node_groups: + nodes.inputs.move(self.active_output, self.active_output + 1) + self.active_output = self.active_output + 1 + + # Function to recursively get related call group nodes + def get_call_group_nodes(self): + call_group_nodes = [] + # Return empty list if node is muted + if self.mute: + return call_group_nodes + for tree in bpy.data.node_groups: + if tree.bl_idname == "ArmLogicTreeType" or tree.bl_idname == "ArmGroupTree": + for node in tree.nodes: + if node.bl_idname == 'LNCallGroupNode': + if node.group_tree == self.get_tree(): + call_group_nodes.append(node) + return call_group_nodes + + # Function to add a socket and handle the same in the related call group nodes + def add_socket(self): + self.add_output('ArmAnySocket','') + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.add_input('ArmAnySocket','') + + # Function to remove a socket and handle the same in the related call group nodes + def remove_socket(self): + self.outputs.remove(self.outputs[-1]) + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.inputs.remove(node.inputs[-1]) + if self.active_output > len(self.outputs) - 1: + self.active_output = self.active_output - 1 + + # Function to add a socket at certain index and + # handle the same in the related call group nodes + def add_socket_ext(self): + index = self.active_output + 1 + self.insert_output('ArmAnySocket', index, '') + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.insert_input('ArmAnySocket', index, '') + + # Function to remove a socket at certain index and + # handle the same in the related call group nodes + def remove_socket_ext(self): + self.outputs.remove(self.outputs[self.active_output]) + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.inputs.remove(node.inputs[self.active_output]) + if self.active_output > len(self.outputs) - 1: + self.active_output = len(self.outputs) - 1 + + # Handle deletion of group input node + def free(self): + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.inputs.clear() + + # Draw node UI + def draw_buttons(self, context, layout): + if self.mute: + layout.enabled = False + row = layout.row(align=True) + op = row.operator('arm.node_call_func', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_socket' + if len(self.outputs) > 1: + op2 = row.operator('arm.node_call_func', text='', icon='X', emboss=True) + op2.node_index = self.get_id_str() + op2.callback_name = 'remove_socket' + + # Draw side panel UI + def draw_buttons_ext(self, context, layout): + if self.mute: + layout.enabled = False + node = context.active_node + split = layout.row() + split.template_list('ARM_UL_interface_sockets', 'OUT', node, 'outputs', node, 'active_output') + ops_col = split.column() + add_remove_col = ops_col.column(align=True) + props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") + props.node_index = self.get_id_str() + props.callback_name = 'add_socket_ext' + if len(self.outputs) > 1: + props = add_remove_col.operator('arm.node_call_func', icon='REMOVE', text="") + props.node_index = self.get_id_str() + props.callback_name = 'remove_socket_ext' + + ops_col.separator() + + up_down_col = ops_col.column(align=True) + props = up_down_col.operator('arm.node_call_func', icon='TRIA_UP', text="") + props.node_index = self.get_id_str() + props.callback_name = 'move_socket_up' + props = up_down_col.operator('arm.node_call_func', icon='TRIA_DOWN', text="") + props.node_index = self.get_id_str() + props.callback_name = 'move_socket_down' + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1, 2): + raise LookupError() + + return node_tree.nodes.new('LNGroupInputsNode') \ No newline at end of file diff --git a/blender/arm/logicnode/miscellaneous/LN_group_output.py b/blender/arm/logicnode/miscellaneous/LN_group_output.py new file mode 100644 index 0000000000..a4fa9636d1 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_group_output.py @@ -0,0 +1,217 @@ +import bpy + +import arm.utils +import arm.node_utils +from arm.logicnode.arm_nodes import * +import arm.logicnode.miscellaneous.LN_call_group as LN_call_group + + +class GroupOutputsNode(ArmLogicTreeNode): + """Output for a node group.""" + bl_idname = 'LNGroupOutputsNode' + bl_label = 'Group Output Node' + arm_section = 'group' + arm_version = 3 + + def __init__(self): + self.register_id() + + # Active socket selected + active_input: IntProperty(name='active_input', description='', default=0) + + # Flag to store invalid links + invalid_link: BoolProperty(name='invalid_link', description='', default=False) + + # Override copy prevention in certain situations such as copying entire group + copy_override: BoolProperty(name='copy override', description='', default=False) + + def init(self, context): + tree = bpy.context.space_data.edit_tree + node_count = 0 + for node in tree.nodes: + if node.bl_idname == 'LNGroupOutputsNode': + node_count += 1 + if node_count > 1: + arm.log.warn("Only one group output node per node tree is allowed") + self.mute = True + else: + super().init(context) + + # Prevent copying of group node + def copy(self, node): + if not self.copy_override: + self.mute = True + self.inputs.clear() + self.copy_override = False + + def arm_init(self, context): + if not self.mute: + self.add_socket() + + # Called when link is created + def insert_link(self, link): + to_socket = link.to_socket + from_node = link.from_node + from_socket = None + # Recursively search for other socket in case of reroutes + if from_node.type == 'REROUTE': + _, from_socket = arm.node_utils.input_get_connected_node(to_socket) + else: + from_socket = link.from_socket + if from_socket is not None: + index = self.get_socket_index(to_socket) + # If socket connected to ArmAnySocket, link is invalid + if from_socket.bl_idname == 'ArmAnySocket': + self.invalid_link = True + else: + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + # Change socket type according to the new link + node.change_output_socket(from_socket.bl_idname, index, link.to_socket.display_label) + + # Use update method to remove invalid links + def update(self): + super().update() + if self.invalid_link: + self.remove_invalid_links() + + # Called when name of the socket is changed + def socket_name_update(self, socket): + index = self.get_socket_index(socket) + # Update socket names of the related call group nodes + call_node_groups = self.get_call_group_nodes() + for node in call_node_groups: + out_socket = node.outputs[index] + if out_socket.bl_idname == 'ArmAnySocket': + out_socket.display_label = socket.display_label + else: + out_socket.name = socket.display_label + + # Recursively search and remove invalid links + def remove_invalid_links(self): + for input in self.inputs: + for link in input.links: + if link.from_socket.bl_idname == 'ArmAnySocket': + tree = self.get_tree() + tree.links.remove(link) + break + self.invalid_link = False + + # Function to move socket up and handle the same in related call group nodes + def move_socket_up(self): + if self.active_input > 0: + self.inputs.move(self.active_input, self.active_input - 1) + call_node_groups = self.get_call_group_nodes() + for nodes in call_node_groups: + nodes.outputs.move(self.active_input, self.active_input - 1) + self.active_input = self.active_input - 1 + + # Function to move socket down and handle the same in related call group nodes + def move_socket_down(self): + if self.active_input < len(self.inputs) - 1: + self.inputs.move(self.active_input, self.active_input + 1) + call_node_groups = self.get_call_group_nodes() + for nodes in call_node_groups: + nodes.outputs.move(self.active_input, self.active_input + 1) + self.active_input = self.active_input + 1 + + # Function to recursively get related call group nodes + def get_call_group_nodes(self): + call_group_nodes = [] + # Return empty list if node is muted + if self.mute: + return call_group_nodes + for tree in bpy.data.node_groups: + if tree.bl_idname == "ArmLogicTreeType" or tree.bl_idname == "ArmGroupTree": + for node in tree.nodes: + if node.bl_idname == 'LNCallGroupNode': + if node.group_tree == self.get_tree(): + call_group_nodes.append(node) + return call_group_nodes + + # Function to add a socket and handle the same in the related call group nodes + def add_socket(self): + self.add_input('ArmAnySocket','',) + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.add_output('ArmAnySocket','') + + # Function to remove a socket and handle the same in the related call group nodes + def remove_socket(self): + self.inputs.remove(self.inputs[-1]) + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.outputs.remove(node.outputs[-1]) + if self.active_input > len(self.inputs) - 1: + self.active_input = self.active_input - 1 + + # Function to add a socket at certain index and + # handle the same in the related call group nodes + def add_socket_ext(self): + index = self.active_input + 1 + self.insert_input('ArmAnySocket', index, '') + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.insert_output('ArmAnySocket', index, '') + + # Function to remove a socket at certain index and + # handle the same in the related call group nodes + def remove_socket_ext(self): + self.inputs.remove(self.inputs[self.active_input]) + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.outputs.remove(node.outputs[self.active_input]) + if self.active_input > len(self.inputs) - 1: + self.active_input = len(self.inputs) - 1 + + # Handle deletion of group input node + def free(self): + call_group_nodes = self.get_call_group_nodes() + for node in call_group_nodes: + node.outputs.clear() + + # Draw node UI + def draw_buttons(self, context, layout): + if self.mute: + layout.enabled = False + row = layout.row(align=True) + op = row.operator('arm.node_call_func', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_socket' + if len(self.inputs) > 1: + op2 = row.operator('arm.node_call_func', text='', icon='X', emboss=True) + op2.node_index = self.get_id_str() + op2.callback_name = 'remove_socket' + + # Draw side panel UI + def draw_buttons_ext(self, context, layout): + if self.mute: + layout.enabled = False + node = context.active_node + split = layout.row() + split.template_list('ARM_UL_interface_sockets', 'IN', node, 'inputs', node, 'active_input') + ops_col = split.column() + add_remove_col = ops_col.column(align=True) + props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") + props.node_index = self.get_id_str() + props.callback_name = 'add_socket_ext' + if len(self.inputs) > 1: + props = add_remove_col.operator('arm.node_call_func', icon='REMOVE', text="") + props.node_index = self.get_id_str() + props.callback_name = 'remove_socket_ext' + + ops_col.separator() + + up_down_col = ops_col.column(align=True) + props = up_down_col.operator('arm.node_call_func', icon='TRIA_UP', text="") + props.node_index = self.get_id_str() + props.callback_name = 'move_socket_up' + props = up_down_col.operator('arm.node_call_func', icon='TRIA_DOWN', text="") + props.node_index = self.get_id_str() + props.callback_name = 'move_socket_down' + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1, 2): + raise LookupError() + + return node_tree.nodes.new('LNGroupOutputsNode') \ No newline at end of file diff --git a/blender/arm/logicnode/miscellaneous/LN_set_debug_console_settings.py b/blender/arm/logicnode/miscellaneous/LN_set_debug_console_settings.py new file mode 100644 index 0000000000..b9c4d6a57b --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_set_debug_console_settings.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + +class SetDebugConsoleSettings(ArmLogicTreeNode): + """Sets the debug console settings.""" + bl_idname = 'LNSetDebugConsoleSettings' + bl_label = 'Set Debug Console Settings' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('left', 'Anchor Left', 'Anchor debug console in the top left'), + ('center', 'Anchor Center', 'Anchor debug console in the top center'), + ('right', 'Anchor Right', 'Anchor the debug console in the top right')], + name='', default='right') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Visible') + self.add_input('ArmFloatSocket', 'Scale', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/miscellaneous/LN_set_time_scale.py b/blender/arm/logicnode/miscellaneous/LN_set_time_scale.py new file mode 100644 index 0000000000..77bc3f5932 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_set_time_scale.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class SetTimeScaleNode(ArmLogicTreeNode): + """Sets the global time scale.""" + bl_idname = 'LNSetTimeScaleNode' + bl_label = 'Set Time Scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Scale', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/miscellaneous/LN_sleep.py b/blender/arm/logicnode/miscellaneous/LN_sleep.py new file mode 100644 index 0000000000..884720e7ca --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_sleep.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SleepNode(ArmLogicTreeNode): + """Waits a specified amount of seconds until passing + through the incoming signal.""" + bl_idname = 'LNSleepNode' + bl_label = 'Sleep' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Time') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/miscellaneous/LN_timer.py b/blender/arm/logicnode/miscellaneous/LN_timer.py new file mode 100644 index 0000000000..2e615fd8a6 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_timer.py @@ -0,0 +1,50 @@ +from arm.logicnode.arm_nodes import * + + +class TimerNode(ArmLogicTreeNode): + """Runs a timer with a specified amount of repetitions. + + @input Start: Start the timer or continue if paused. In both cases, + the values of `Duration` and `Repeat` are (re-)evaluated. + @input Pause: Pause the timer. + @input Stop: Stop and reset the timer. This does not activate any outputs. + @input Duration: The time in seconds that the timer runs. + @input Repeat: The number of times the timer will repeat, or 0 for infinite repetition. + + @output Out: Activated after each repetition. + @output Done: Activated after the last repetition (never activated if `Repeat` is 0). + @output Running: Whether the timer is currently running. + @output Time Passed: The time in seconds that has passed since the + current repetition started, excluding pauses. + @output Time Left: The time left in seconds until the timer is done + or the next repetition starts. + @output Progress: Percentage of the timer's progress of the current + repetition (`Time Passed/Duration`). + @output Repetitions: The index of the current repetition, starting at 0. + """ + bl_idname = 'LNTimerNode' + bl_label = 'Timer' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Start') + self.add_input('ArmNodeSocketAction', 'Pause') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmFloatSocket', 'Duration', default_value=1.0) + self.add_input('ArmIntSocket', 'Repeat') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Done') + self.add_output('ArmBoolSocket', 'Running') + self.add_output('ArmIntSocket', 'Time Passed') + self.add_output('ArmIntSocket', 'Time Left') + self.add_output('ArmFloatSocket', 'Progress') + self.add_output('ArmFloatSocket', 'Repetitions') + + def draw_label(self) -> str: + inp_duration = self.inputs['Duration'] + inp_repeat = self.inputs['Repeat'] + if inp_duration.is_linked or inp_repeat.is_linked: + return self.bl_label + + return f'{self.bl_label}: {round(inp_duration.default_value_raw, 3)}s ({inp_repeat.default_value_raw} R.)' diff --git a/blender/arm/logicnode/miscellaneous/__init__.py b/blender/arm/logicnode/miscellaneous/__init__.py new file mode 100644 index 0000000000..18a9e23119 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/__init__.py @@ -0,0 +1,5 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='group', category='Miscellaneous') +add_node_section(name='screen', category='Miscellaneous') +add_node_section(name='default', category='Miscellaneous') diff --git a/blender/arm/logicnode/native/LN_call_haxe_static.py b/blender/arm/logicnode/native/LN_call_haxe_static.py new file mode 100644 index 0000000000..2a3e382dee --- /dev/null +++ b/blender/arm/logicnode/native/LN_call_haxe_static.py @@ -0,0 +1,47 @@ +from arm.logicnode.arm_nodes import * + +class CallHaxeStaticNode(ArmLogicTreeNode): + """Calls the given static Haxe function and optionally passes arguments to it. + + **Compatibility info**: prior versions of this node didn't accept arguments and instead implicitly passed the current logic tree object as the first argument. In newer versions you need to pass that argument explicitly if the called function expects it. + + @input Function: the full module path to the function. + @output Result: the result of the function.""" + bl_idname = 'LNCallHaxeStaticNode' + bl_label = 'Call Haxe Static' + arm_section = 'haxe' + arm_version = 3 + min_inputs = 2 + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Function') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Result') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('arm.node_add_input', text='Add Arg', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmDynamicSocket' + op.name_format = "Arg {0}" + op.index_name_offset = -2 + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 2): + raise LookupError() + + if self.arm_version == 1 or self.arm_version == 2: + return NodeReplacement( + 'LNCallHaxeStaticNode', self.arm_version, 'LNCallHaxeStaticNode', 2, + in_socket_mapping={0:0, 1:1}, out_socket_mapping={0:0, 1:1} + ) \ No newline at end of file diff --git a/blender/arm/logicnode/native/LN_clear_console.py b/blender/arm/logicnode/native/LN_clear_console.py new file mode 100644 index 0000000000..9646447742 --- /dev/null +++ b/blender/arm/logicnode/native/LN_clear_console.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class PrintNode(ArmLogicTreeNode): + """Clears the system console.""" + bl_idname = 'LNClearConsoleNode' + bl_label = 'Clear Console' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/native/LN_detect_mobile_browser.py b/blender/arm/logicnode/native/LN_detect_mobile_browser.py new file mode 100644 index 0000000000..4268ab0df5 --- /dev/null +++ b/blender/arm/logicnode/native/LN_detect_mobile_browser.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class DetectMobileBrowserNode(ArmLogicTreeNode): + """Determines the mobile browser or not (works only for web browsers).""" + bl_idname = 'LNDetectMobileBrowserNode' + bl_label = 'Detect Mobile Browser' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmBoolSocket', 'Mobile') diff --git a/blender/arm/logicnode/native/LN_expression.py b/blender/arm/logicnode/native/LN_expression.py new file mode 100644 index 0000000000..843869fd83 --- /dev/null +++ b/blender/arm/logicnode/native/LN_expression.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class ExpressionNode(ArmLogicTreeNode): + """Evaluates a Haxe expression and returns its output. + + @output Result: the result of the expression.""" + bl_idname = 'LNExpressionNode' + bl_label = 'Expression' + arm_version = 1 + arm_section = 'haxe' + + property0: HaxeStringProperty('property0', name='', default='') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/native/LN_get_date_time.py b/blender/arm/logicnode/native/LN_get_date_time.py new file mode 100644 index 0000000000..d9ccf20f42 --- /dev/null +++ b/blender/arm/logicnode/native/LN_get_date_time.py @@ -0,0 +1,113 @@ +from arm.logicnode.arm_nodes import * + +class GetDateTimeNode(ArmLogicTreeNode): + """Returns the values of the current date and time.""" + bl_idname = 'LNGetDateTimeNode' + bl_label = 'Get Date and Time' + arm_section = 'Native' + arm_version = 1 + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(type_name): + return { + 'now': 0, + 'timestamp': 1, + 'timeZoneOffSet': 2, + 'weekday': 3, + 'day': 4, + 'month': 5, + 'year': 6, + 'hours': 7, + 'minutes': 8, + 'seconds': 9, + 'all': 10, + 'formatted': 11, + }.get(type_name, 10) + + def get_enum(self): + return self.get('property0', 10) + + def set_enum(self, value): + # Checking the selection of each type + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + #Check if type changed + if select_prev != select_current: + + for i in self.inputs: + self.inputs.remove(i) + for i in self.outputs: + self.outputs.remove(i) + + if (self.get_count_in(select_current) == 0): + self.add_output('ArmStringSocket', 'Date') + elif (self.get_count_in(select_current) == 10): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Timestamp') + self.add_output('ArmIntSocket', 'Timezone Offset') + self.add_output('ArmIntSocket', 'Weekday') + self.add_output('ArmIntSocket', 'Day') + self.add_output('ArmIntSocket', 'Month') + self.add_output('ArmIntSocket', 'Year') + self.add_output('ArmIntSocket', 'Hours') + self.add_output('ArmIntSocket', 'Minutes') + self.add_output('ArmIntSocket', 'Seconds') + elif (self.get_count_in(select_current) == 11): + self.add_input('ArmStringSocket', 'Format', default_value="%Y/%m/%d - %H:%M:%S") + self.add_output('ArmStringSocket', 'Date') + else: + self.add_output('ArmIntSocket', 'Value') + + self['property0'] = value + + + property0: HaxeEnumProperty( + 'property0', + items = [('now', 'Now', 'Returns a Date representing the current local time.'), + ('timestamp', 'Timestamp', 'Returns the timestamp (in milliseconds) of this date'), + ('timeZoneOffSet', 'Timezone Offset', 'Returns the time zone difference of this Date in the current locale to UTC, in minutes'), + ('weekday', 'Weekday', 'Returns the day of the week of this Date (0-6 range, where 0 is Sunday) in the local timezone.'), + ('day', 'Day', 'Returns the day of this Date (1-31 range) in the local timezone.'), + ('month', 'Month', 'Returns the month of this Date (0-11 range) in the local timezone. Note that the month number is zero-based.'), + ('year', 'Year', 'Returns the full year of this Date (4 digits) in the local timezone.'), + ('hours', 'Hours', 'Returns the hours of this Date (0-23 range) in the local timezone.'), + ('minutes', 'Minutes', 'Returns the minutes of this Date (0-59 range) in the local timezone.'), + ('seconds', 'Seconds', 'Returns the seconds of this Date (0-59 range) in the local timezone.'), + ('all', 'All', 'Get all of the individual separated date and time properties'), + ('formatted', 'Formatted', 'Format the current system date and time')], + name='', + default='all', + set=set_enum, + get=get_enum) + + + + + def __init__(self): + array_nodes[str(id(self))] = self + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Timestamp') + self.add_output('ArmIntSocket', 'Timezone Offset') + self.add_output('ArmIntSocket', 'Weekday') + self.add_output('ArmIntSocket', 'Day') + self.add_output('ArmIntSocket', 'Month') + self.add_output('ArmIntSocket', 'Year') + self.add_output('ArmIntSocket', 'Hours') + self.add_output('ArmIntSocket', 'Minutes') + self.add_output('ArmIntSocket', 'Seconds') + + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + diff --git a/blender/arm/logicnode/native/LN_get_haxe_property.py b/blender/arm/logicnode/native/LN_get_haxe_property.py new file mode 100644 index 0000000000..2748a873aa --- /dev/null +++ b/blender/arm/logicnode/native/LN_get_haxe_property.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class GetHaxePropertyNode(ArmLogicTreeNode): + """Returns a property of an Haxe object (via the Reflection API). + + @seeNode Set Haxe Property""" + bl_idname = 'LNGetHaxePropertyNode' + bl_label = 'Get Haxe Property' + arm_version = 1 + arm_section = 'haxe' + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Dynamic') + self.add_input('ArmStringSocket', 'Property') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/native/LN_get_system_language.py b/blender/arm/logicnode/native/LN_get_system_language.py new file mode 100644 index 0000000000..4dcb6cc5d0 --- /dev/null +++ b/blender/arm/logicnode/native/LN_get_system_language.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class GetSystemLanguage(ArmLogicTreeNode): + """Returns the language of the current system.""" + bl_idname = 'LNGetSystemLanguage' + bl_label = 'Get System Language' + arm_section = 'Native' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmStringSocket', 'Language') diff --git a/blender/arm/logicnode/native/LN_get_system_name.py b/blender/arm/logicnode/native/LN_get_system_name.py new file mode 100644 index 0000000000..6e08ac3e9b --- /dev/null +++ b/blender/arm/logicnode/native/LN_get_system_name.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +# Class GetSystemName +class GetSystemName(ArmLogicTreeNode): + """Returns the name of the current system.""" + bl_idname = 'LNGetSystemName' + bl_label = 'Get System Name' + arm_section = 'Native' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmStringSocket', 'System Name') + self.add_output('ArmBoolSocket', 'Windows') + self.add_output('ArmBoolSocket', 'Linux') + self.add_output('ArmBoolSocket', 'Mac') + self.add_output('ArmBoolSocket', 'HTML5') + self.add_output('ArmBoolSocket', 'Android') diff --git a/blender/arm/logicnode/native/LN_loadUrl.py b/blender/arm/logicnode/native/LN_loadUrl.py new file mode 100644 index 0000000000..fbbed12bce --- /dev/null +++ b/blender/arm/logicnode/native/LN_loadUrl.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class LoadUrlNode(ArmLogicTreeNode): + """Load the given URL in a new tab (works only for web browsers).""" + bl_idname = 'LNLoadUrlNode' + bl_label = 'Load URL' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'URL') diff --git a/blender/arm/logicnode/native/LN_print.py b/blender/arm/logicnode/native/LN_print.py new file mode 100644 index 0000000000..8ba303d727 --- /dev/null +++ b/blender/arm/logicnode/native/LN_print.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class PrintNode(ArmLogicTreeNode): + """Print the given value to the console.""" + bl_idname = 'LNPrintNode' + bl_label = 'Print' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'String') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/native/LN_read_file.py b/blender/arm/logicnode/native/LN_read_file.py new file mode 100644 index 0000000000..a0fa3091c4 --- /dev/null +++ b/blender/arm/logicnode/native/LN_read_file.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + + +class ReadFileNode(ArmLogicTreeNode): + """Reads the given file and returns its content. + + @input File: the asset name of the file as used by Kha. + @input Use cache: if unchecked, re-read the file from disk every + time the node is executed. Otherwise, cache the file after the + first read and return the cached content. + + @output Loaded: activated after the file has been read. If the file + doesn't exist, the output is not activated. + @output Content: the content of the file. + + @seeNode Write File + """ + bl_idname = 'LNReadFileNode' + bl_label = 'Read File' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'File') + self.add_input('ArmBoolSocket', 'Use cache', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Loaded') + self.add_output('ArmStringSocket', 'Content') diff --git a/blender/arm/logicnode/native/LN_read_json.py b/blender/arm/logicnode/native/LN_read_json.py new file mode 100644 index 0000000000..79a7ad87e2 --- /dev/null +++ b/blender/arm/logicnode/native/LN_read_json.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + + +class ReadJsonNode(ArmLogicTreeNode): + """Reads the given JSON file and returns its content. + + @input File: the asset name of the file as used by Kha. + @input Use cache: if unchecked, re-read the file from disk every + time the node is executed. Otherwise, cache the file after the + first read and return the cached content. + + @output Loaded: activated after the file has been read. If the file + doesn't exist, the output is not activated. + @output Dynamic: the content of the file. + + @seeNode Write JSON + """ + bl_idname = 'LNReadJsonNode' + bl_label = 'Read JSON' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'File') + self.add_input('ArmBoolSocket', 'Use cache', default_value=1) + + self.add_output('ArmNodeSocketAction', 'Loaded') + self.add_output('ArmDynamicSocket', 'Dynamic') diff --git a/blender/arm/logicnode/native/LN_read_storage.py b/blender/arm/logicnode/native/LN_read_storage.py new file mode 100644 index 0000000000..287410cc92 --- /dev/null +++ b/blender/arm/logicnode/native/LN_read_storage.py @@ -0,0 +1,22 @@ +from arm.logicnode.arm_nodes import * + + +class ReadStorageNode(ArmLogicTreeNode): + """Reads a value from the application's default storage file. Each + value is uniquely identified by a key. + + For a detailed explanation of the storage system, please read the + documentation for the [`Write Storage`](#write-storage) node. + + @seeNode Write Storage + """ + bl_idname = 'LNReadStorageNode' + bl_label = 'Read Storage' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Key') + self.add_input('ArmStringSocket', 'Default') + + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/native/LN_script.py b/blender/arm/logicnode/native/LN_script.py new file mode 100644 index 0000000000..da0fb05d87 --- /dev/null +++ b/blender/arm/logicnode/native/LN_script.py @@ -0,0 +1,28 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class ScriptNode(ArmLogicTreeNode): + """Executes the given script.""" + bl_idname = 'LNScriptNode' + bl_label = 'Script' + arm_section = 'haxe' + arm_version = 1 + + @property + def property0(self): + return bpy.data.texts[self.property0_].as_string() if self.property0_ in bpy.data.texts else '' + + + property0_: HaxeStringProperty('property0', name='Text', default='') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Array') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Result') + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property0_', bpy.data, 'texts', icon='NONE', text='') diff --git a/blender/arm/logicnode/native/LN_set_haxe_property.py b/blender/arm/logicnode/native/LN_set_haxe_property.py new file mode 100644 index 0000000000..bba925b36a --- /dev/null +++ b/blender/arm/logicnode/native/LN_set_haxe_property.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class SetHaxePropertyNode(ArmLogicTreeNode): + """Sets a property of an Haxe object (via the Reflection API). + + @seeNode Get Haxe Property""" + bl_idname = 'LNSetHaxePropertyNode' + bl_label = 'Set Haxe Property' + arm_section = 'haxe' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Dynamic') + self.add_input('ArmStringSocket', 'Property') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/native/LN_set_vibrate.py b/blender/arm/logicnode/native/LN_set_vibrate.py new file mode 100644 index 0000000000..e9294bf677 --- /dev/null +++ b/blender/arm/logicnode/native/LN_set_vibrate.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * +import arm.utils + +class SetVibrateNode(ArmLogicTreeNode): + """Pulses the vibration hardware on the device for time in milliseconds, if such hardware exists.""" + bl_idname = 'LNSetVibrateNode' + bl_label = 'Set Vibrate' + arm_section = 'Native' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmIntSocket', 'Milliseconds', default_value=100) + + self.add_output('ArmNodeSocketAction', 'Out') + # Add permission for target android + arm.utils.add_permission_target_android(arm.utils.PermissionName.VIBRATE) diff --git a/blender/arm/logicnode/native/LN_shutdown.py b/blender/arm/logicnode/native/LN_shutdown.py new file mode 100644 index 0000000000..6292eafa41 --- /dev/null +++ b/blender/arm/logicnode/native/LN_shutdown.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class ShutdownNode(ArmLogicTreeNode): + """Closes the application.""" + bl_idname = 'LNShutdownNode' + bl_label = 'Shutdown' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/native/LN_write_file.py b/blender/arm/logicnode/native/LN_write_file.py new file mode 100644 index 0000000000..d6bbcd9008 --- /dev/null +++ b/blender/arm/logicnode/native/LN_write_file.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + + +class WriteFileNode(ArmLogicTreeNode): + """Writes the given string content to the given file. If the file + already exists, the existing content of the file is overwritten. + + > **This node is currently only implemented on Krom** + + @input File: the name of the file, relative to `Krom.getFilesLocation()` + @input Content: the content to write to the file. + + @seeNode Read File + """ + bl_idname = 'LNWriteFileNode' + bl_label = 'Write File' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'File') + self.add_input('ArmStringSocket', 'Content') diff --git a/blender/arm/logicnode/native/LN_write_json.py b/blender/arm/logicnode/native/LN_write_json.py new file mode 100644 index 0000000000..8468b97aac --- /dev/null +++ b/blender/arm/logicnode/native/LN_write_json.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + + +class WriteJsonNode(ArmLogicTreeNode): + """Writes the given content to the given JSON file. If the file + already exists, the existing content of the file is overwritten. + + > **This node is currently only implemented on Krom** + + @input File: the name of the file, relative to `Krom.getFilesLocation()`, + including the file extension. + @input Dynamic: the content to write to the file. Can be any type that can + be serialized to JSON. + + @seeNode Read JSON + """ + bl_idname = 'LNWriteJsonNode' + bl_label = 'Write JSON' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'File') + self.add_input('ArmDynamicSocket', 'Dynamic') diff --git a/blender/arm/logicnode/native/LN_write_storage.py b/blender/arm/logicnode/native/LN_write_storage.py new file mode 100644 index 0000000000..098ab435df --- /dev/null +++ b/blender/arm/logicnode/native/LN_write_storage.py @@ -0,0 +1,37 @@ +from arm.logicnode.arm_nodes import * + + +class WriteStorageNode(ArmLogicTreeNode): + """Writes a given value to the application's default storage file. + Each value is uniquely identified by a key, which can be used to + later read the value from the storage file. + + Each key can only refer to one value, so writing a second value with + a key that is already used overwrites the already stored value with + the second value. + + The location of the default storage file varies on different + platforms, as implemented by the Kha storage API: + - *Windows*: `%USERPROFILE%/Saved Games//default.kha` + - *Linux*: one of `$HOME/./default.kha`, `$XDG_DATA_HOME/./default.kha` or `$HOME/.local/share/default.kha` + - *MacOS*: `~/Library/Application Support//default.kha` + - *iOS*: `~/Library/Application Support//default.kha` + - *Android*: `//files/default.kha` + - *HTML 5*: saved in the local storage (web storage API) for the project's [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin). + + `` refers to the name set at `Armory Exporter > Name`, + `` is the generated package name on Android. + + @seeNode Read Storage + """ + bl_idname = 'LNWriteStorageNode' + bl_label = 'Write Storage' + arm_section = 'file' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Key') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/native/__init__.py b/blender/arm/logicnode/native/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/navmesh/LN_get_agent_data.py b/blender/arm/logicnode/navmesh/LN_get_agent_data.py new file mode 100644 index 0000000000..b26d4f8c9c --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_get_agent_data.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetAgentDataNode(ArmLogicTreeNode): + """Gets the speed and turn duration of the agent""" + bl_idname = 'LNGetAgentDataNode' + bl_label = 'Get Agent Data' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmFloatSocket', 'Speed') + self.add_output('ArmFloatSocket', 'Turn Duration') \ No newline at end of file diff --git a/blender/arm/logicnode/navmesh/LN_go_to_location.py b/blender/arm/logicnode/navmesh/LN_go_to_location.py new file mode 100644 index 0000000000..5db1bf96c4 --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_go_to_location.py @@ -0,0 +1,44 @@ +from arm.logicnode.arm_nodes import * + +class GoToLocationNode(ArmLogicTreeNode): + """Makes a NavMesh agent go to location. + + @input In: Start navigation. + @input Object: The object to navigate. Object must have `NavAgent` trait applied. + @input Location: Closest point on the navmesh to navigate to. + @input Speed: Rate of movement. + @input Turn Duration: Rate of turn. + @input Height Offset: Height of the object from the navmesh. + @input Use Raycast: Use physics ray cast to get more precise z positioning. + @input Ray Cast Depth: Depth of ray cast from the object origin. + @input Ray Cast Mask: Ray cast mask for collision detection. + + @output Out: Executed immidiately after start of the navigation. + @output Tick Position: Executed at every step of navigation translation. + @output Tick Rotation: Executed at every step of navigation rotation. + """ + bl_idname = 'LNGoToLocationNode' + bl_label = 'Go to Location' + 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('ArmFloatSocket', 'Speed', 5.0) + self.add_input('ArmFloatSocket', 'Turn Duration', 0.4) + self.add_input('ArmFloatSocket', 'Height Offset', 0.0) + self.add_input('ArmBoolSocket','Use Raycast') + self.add_input('ArmFloatSocket', 'Ray Cast Depth', -5.0) + self.add_input('ArmIntSocket', 'Ray Cast Mask', 1) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Tick Position') + self.add_output('ArmNodeSocketAction', 'Tick Rotation') + + 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/navmesh/LN_navigable_location.py b/blender/arm/logicnode/navmesh/LN_navigable_location.py new file mode 100644 index 0000000000..3e8b8f8b97 --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_navigable_location.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class NavigableLocationNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNNavigableLocationNode' + bl_label = 'Navigable Location' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Location') diff --git a/blender/arm/logicnode/navmesh/LN_pick_navmesh_location.py b/blender/arm/logicnode/navmesh/LN_pick_navmesh_location.py new file mode 100644 index 0000000000..2ae40119f8 --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_pick_navmesh_location.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class PickLocationNode(ArmLogicTreeNode): + """Pick a location coordinates in the given NavMesh.""" + bl_idname = 'LNPickLocationNode' + bl_label = 'Pick NavMesh Location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'NavMesh') + self.add_input('ArmVectorSocket', 'Screen Coords') + + self.add_output('ArmVectorSocket', 'Location') diff --git a/blender/arm/logicnode/navmesh/LN_stop_agent.py b/blender/arm/logicnode/navmesh/LN_stop_agent.py new file mode 100644 index 0000000000..ff49fc9f51 --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_stop_agent.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class StopAgentNode(ArmLogicTreeNode): + """Stops the given NavMesh agent.""" + bl_idname = 'LNStopAgentNode' + bl_label = 'Stop Agent' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/navmesh/__init__.py b/blender/arm/logicnode/navmesh/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/network/LN_network_client.py b/blender/arm/logicnode/network/LN_network_client.py new file mode 100644 index 0000000000..733524e993 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_client.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkClientNode(ArmLogicTreeNode): + """Network client to connect to an existing host""" + bl_idname = 'LNNetworkClientNode' + bl_label = 'Create Client' + arm_version = 1 + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Url', default_value="ws://127.0.0.1:8001") + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Connection') diff --git a/blender/arm/logicnode/network/LN_network_close_connection.py b/blender/arm/logicnode/network/LN_network_close_connection.py new file mode 100644 index 0000000000..296c3d39d8 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_close_connection.py @@ -0,0 +1,34 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkCloseConnectionNode(ArmLogicTreeNode): + """Close connection on the network""" + bl_idname = 'LNNetworkCloseConnectionNode' + bl_label = 'Close Connection' + arm_version = 1 + + + property0: HaxeBoolProperty( + 'property0', + name="To Null", + description="Close the connection and set to null", + default=False) + + + property1: HaxeEnumProperty( + 'property1', + items = [('client', 'Client', 'Close client connection on network'), + ('host', 'Host', 'Close host connection on the network'), + ('securehost', 'Secure Host', 'Close secure host connection on the network')], + name='', default='client') + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + + self.add_output('ArmNodeSocketAction', 'Out') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_event.py b/blender/arm/logicnode/network/LN_network_event.py new file mode 100644 index 0000000000..944a14d2bc --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_event.py @@ -0,0 +1,92 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkEventNode(ArmLogicTreeNode): + """Triggers an event from the network getting both message data and sender ID""" + bl_idname = 'LNNetworkEventNode' + bl_label = 'Network Event' + arm_version = 1 + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(type_name): + return { + 'client': 0, + 'host': 1, + 'securehost': 2 + }.get(type_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of each type + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + #Check if type changed + if select_prev != select_current: + + for i in self.inputs: + self.inputs.remove(i) + + # Arguements for type Client + if (self.get_count_in(select_current) == 0): + self.add_input('ArmStringSocket', 'Url', default_value="ws://127.0.0.1:8001") + + + # Arguements for type Host + if (self.get_count_in(select_current) == 1): + self.add_input('ArmStringSocket', 'Domain', default_value="127.0.0.1") + self.add_input('ArmIntSocket', 'Port', default_value=8001) + + + # Arguements for type Secure Host + if (self.get_count_in(select_current) == 2): + self.add_input('ArmStringSocket', 'Domain', default_value="127.0.0.1") + self.add_input('ArmIntSocket', 'Port', default_value=8001) + + + self['property0'] = value + + + property0: HaxeEnumProperty( + 'property0', + items = [('client', 'Client', 'Network client Event listener'), + ('host', 'Host', 'Network host Event listener'), + ('securehost', 'Secure Host', 'Network secure host Event listener')], + name='', + default='client', + set=set_enum, + get=get_enum) + + + property1: HaxeEnumProperty( + 'property1', + items = [('onopen', 'OnOpen', 'Listens to onOpen event'), + ('onmessage', 'OnMessage', 'Listens to onMessage event'), + ('onerror', 'OnError', 'Listens to onError event'), + ('onclose', 'OnClose', 'Listens to onClose event')], + name='', + default='onopen') + + + def __init__(self): + array_nodes[str(id(self))] = self + + + def arm_init(self, context): + #self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Url', default_value="ws://127.0.0.1:8001") + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'ID') + self.add_output('ArmDynamicSocket', 'Data') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/network/LN_network_host.py b/blender/arm/logicnode/network/LN_network_host.py new file mode 100644 index 0000000000..833f82c0bc --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_host.py @@ -0,0 +1,70 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkHostNode(ArmLogicTreeNode): + """Network host for other clients to connect""" + bl_idname = 'LNNetworkHostNode' + bl_label = 'Create Host' + arm_version = 1 + ssl_ind = 4 + + def update_ssl(self, context): + """This is a helper method to allow declaring the `secure` + property before the update_sockets() method. It's not required + but then you would need to move the declaration of `secure` + further down.""" + self.update_sockets(context) + + property0: HaxeBoolProperty( + 'property0', + name="Secure", + description="Enable SSL encryption", + default=False, + update=update_ssl + ) + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Domain', default_value="127.0.0.1") + self.add_input('ArmIntSocket', 'Port', default_value=8001) + self.add_input('ArmIntSocket', 'Max Conn.', default_value=25) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Connection') + + self.update_sockets(context) + + def update_sockets(self, context): + # It's bad to remove from a list during iteration so we use + # this helper list here + remove_list = [] + + # Remove dynamically placed input sockets + for i in range(NetworkHostNode.ssl_ind, len(self.inputs)): + remove_list.append(self.inputs[i]) + for i in remove_list: + self.inputs.remove(i) + + # Add dynamic input sockets + if self.property0: + self.add_input('ArmStringSocket', 'Certificate') + self.add_input('ArmStringSocket', 'Private Key') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + in_socket_mapping={0:0, 1:1, 2:2, 3:3} + if self.property0: + in_socket_mapping.update({4:4, 5:5}) + + return NodeReplacement( + 'LNNetworkHostNode', self.arm_version, 'LNNetworkHostNode', 4, + in_socket_mapping=in_socket_mapping, + out_socket_mapping={0:0, 1:1}, + property_mapping={'property0':'property0'}) diff --git a/blender/arm/logicnode/network/LN_network_host_close_client.py b/blender/arm/logicnode/network/LN_network_host_close_client.py new file mode 100644 index 0000000000..fde7c89e84 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_host_close_client.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkHostCloseClientNode(ArmLogicTreeNode): + """Close a client from a host connection by ID""" + bl_idname = 'LNNetworkHostCloseClientNode' + bl_label = 'Host Close Client' + arm_version = 1 + + + property0: HaxeBoolProperty( + 'property0', + name="Secure", + description="Secure host connection", + default=False, + ) + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'ID') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_host_get_ip.py b/blender/arm/logicnode/network/LN_network_host_get_ip.py new file mode 100644 index 0000000000..5a7e4416eb --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_host_get_ip.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkHostGetIpNode(ArmLogicTreeNode): + """Return an IP from the ID of a connection""" + bl_idname = 'LNNetworkHostGetIpNode' + bl_label = 'Host Get IP' + arm_version = 1 + + + property0: HaxeBoolProperty( + 'property0', + name="Secure", + description="Secure host connection", + default=False, + ) + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'ID') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmStringSocket', 'IP') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_http_request.py b/blender/arm/logicnode/network/LN_network_http_request.py new file mode 100644 index 0000000000..76130bb8b1 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_http_request.py @@ -0,0 +1,78 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkHttpRequestNode(ArmLogicTreeNode): + """Network Http Request""" + bl_idname = 'LNNetworkHttpRequestNode' + bl_label = 'Http Request' + arm_version = 1 + + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(type_name): + return { + 'get': 0, + 'post': 1 + }.get(type_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + if select_prev != select_current: + + for i in self.inputs: + self.inputs.remove(i) + for i in self.outputs: + self.outputs.remove(i) + + if (self.get_count_in(select_current) == 0): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Url') + self.add_input('ArmDynamicSocket', 'Headers') + self.add_input('ArmDynamicSocket', 'Parameters') + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Status') + self.add_output('ArmDynamicSocket', 'Response') + self['property0'] = value + else: + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Url') + self.add_input('ArmDynamicSocket', 'Data') + self.add_input('ArmBoolSocket', 'Bytes') + self.add_input('ArmDynamicSocket', 'Headers') + self.add_input('ArmDynamicSocket', 'Parameters') + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Status') + self.add_output('ArmDynamicSocket', 'Response') + self['property0'] = value + + + property0: HaxeEnumProperty( + 'property0', + items = [('get', 'Get', 'Http get request'), + ('post', 'Post', 'Http post request')], + name='', default='get', + set=set_enum, + get=get_enum) + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Url') + self.add_input('ArmDynamicSocket', 'Headers') + self.add_input('ArmDynamicSocket', 'Parameters') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Status') + self.add_output('ArmDynamicSocket', 'Response') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_message_parser.py b/blender/arm/logicnode/network/LN_network_message_parser.py new file mode 100644 index 0000000000..aea3ebe709 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_message_parser.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkMessageParserNode(ArmLogicTreeNode): + """Parses message type from data packet""" + bl_idname = 'LNNetworkMessageParserNode' + bl_label = 'Message Parser' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('string', 'String', 'Event for a string over the network'), + ('vector', 'Vector', 'Event for a vector over the network'), + ('float', 'Float', 'Event for a float over the network'), + ('integer', 'Integer', 'Event for an integer over the network'), + ('boolean', 'Boolean', 'Event for a boolean over the network'), + ('transform', 'Transform', 'Event for a transform over the network'), + ('rotation', 'Rotation', 'Event for a rotation over the network')], + name='', default='string') + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'API') + self.add_input('ArmDynamicSocket', 'Data') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'API') + self.add_output('ArmDynamicSocket', 'Data') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_open.py b/blender/arm/logicnode/network/LN_network_open.py new file mode 100644 index 0000000000..9ebbc52e3f --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_open.py @@ -0,0 +1,28 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkOpenConnectionNode(ArmLogicTreeNode): + """Open connection on the network""" + bl_idname = 'LNNetworkOpenConnectionNode' + bl_label = 'Open Connection' + arm_version = 1 + + + property0: HaxeEnumProperty( + 'property0', + items = [('client', 'Client', 'Open client connection on network'), + ('host', 'Host', 'Open host connection on the network'), + ('securehost', 'Secure Host', 'Open secure host connection on the network')], + name='', default='client') + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Connection') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/network/LN_network_send_message.py b/blender/arm/logicnode/network/LN_network_send_message.py new file mode 100644 index 0000000000..09971fd4f1 --- /dev/null +++ b/blender/arm/logicnode/network/LN_network_send_message.py @@ -0,0 +1,108 @@ +from arm.logicnode.arm_nodes import * + + +class NetworkSendMessageNode(ArmLogicTreeNode): + """Send messages directly to a host as a client or to all of the + network clients connected to a host send messages directly""" + bl_idname = 'LNNetworkSendMessageNode' + bl_label = 'Send Message' + arm_version = 1 + ind = 4 + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(type_name): + return { + 'client': 0, + 'host': 1, + 'securehost': 2 + }.get(type_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of each type + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + #Check if type changed + if select_prev != select_current: + + for i in self.inputs: + self.inputs.remove(i) + + # Arguements for type Client + if (self.get_count_in(select_current) == 0): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'API') + self.add_input('ArmDynamicSocket', 'Data') + + # Arguements for type Host + if (self.get_count_in(select_current) == 1): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'API') + self.add_input('ArmDynamicSocket', 'Data') + self.add_input('ArmStringSocket', 'ID') + self.add_input('ArmBoolSocket', 'Send All') + + + # Arguements for type Secure Host + if (self.get_count_in(select_current) == 2): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'API') + self.add_input('ArmDynamicSocket', 'Data') + self.add_input('ArmStringSocket', 'ID') + self.add_input('ArmBoolSocket', 'Send All') + + + self['property0'] = value + + + property0: HaxeEnumProperty( + 'property0', + items = [('client', 'Client', 'Network client Event listener'), + ('host', 'Host', 'Network host Event listener'), + ('securehost', 'Secure Host', 'Network secure host Event listener')], + name='', + default='client', + set=set_enum, + get=get_enum) + + + property1: HaxeEnumProperty( + 'property1', + items = [('string', 'String', 'Send a string over the network to one or all of the clients'), + ('vector', 'Vector', 'Send a vector over the network to one or all of the clients'), + ('float', 'Float', 'Send a float over the network to one or all of the clients'), + ('integer', 'Integer', 'Send an integer over the network to one or all of the clients'), + ('boolean', 'Boolean', 'Send a boolean over the network to one or all of the clients'), + ('transform', 'Transform', 'Send a transform over the network to one or all of the clients'), + ('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')], + name='', + default='string') + + + def __init__(self): + array_nodes[str(id(self))] = self + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Connection') + self.add_input('ArmStringSocket', 'API') + self.add_input('ArmDynamicSocket', 'Data') + + self.add_output('ArmNodeSocketAction', 'Out') + + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/network/__init__.py b/blender/arm/logicnode/network/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/blender/arm/logicnode/network/__init__.py @@ -0,0 +1 @@ + diff --git a/blender/arm/logicnode/object/LN_get_distance.py b/blender/arm/logicnode/object/LN_get_distance.py new file mode 100644 index 0000000000..5885e890e9 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_distance.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class GetDistanceNode(ArmLogicTreeNode): + """Returns the euclidian distance between the two given objects. + + @see For distance between two locations, use the `Distance` operator + in the *[`Vector Math`](#vector-math)* node.""" + bl_idname = 'LNGetDistanceNode' + bl_label = 'Get Distance' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmFloatSocket', 'Distance') diff --git a/blender/arm/logicnode/object/LN_get_object_by_name.py b/blender/arm/logicnode/object/LN_get_object_by_name.py new file mode 100644 index 0000000000..a19bdf06d0 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_by_name.py @@ -0,0 +1,16 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class GetObjectNode(ArmLogicTreeNode): + """Searches for a object that uses the given name in the current active scene and returns it.""" + + bl_idname = 'LNGetObjectNode' + bl_label = 'Get Object by Name' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Name') + + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/object/LN_get_object_by_uid.py b/blender/arm/logicnode/object/LN_get_object_by_uid.py new file mode 100644 index 0000000000..dae950576a --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_by_uid.py @@ -0,0 +1,16 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class GetObjectByUidNode(ArmLogicTreeNode): + """Searches for a object with this uid in the current active scene and returns it.""" + + bl_idname = 'LNGetObjectByUidNode' + bl_label = 'Get Object By Uid' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Uid') + + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/object/LN_get_object_child.py b/blender/arm/logicnode/object/LN_get_object_child.py new file mode 100644 index 0000000000..76409006f3 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_child.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class GetChildNode(ArmLogicTreeNode): + """Returns the child of the given object by the child object's name.""" + bl_idname = 'LNGetChildNode' + bl_label = 'Get Object Child' + arm_section = 'relations' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('By Name', 'By Name', 'By Name'), + ('Contains', 'Contains', 'Contains'), + ('Starts With', 'Starts With', 'Starts With'), + ('Ends With', 'Ends With', 'Ends With'), + ], + name='', default='By Name') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Parent') + self.add_input('ArmStringSocket', 'Child Name') + + self.add_output('ArmNodeSocketObject', 'Child') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/object/LN_get_object_children.py b/blender/arm/logicnode/object/LN_get_object_children.py new file mode 100644 index 0000000000..a23bf33ffe --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_children.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetChildrenNode(ArmLogicTreeNode): + """Returns the children of the given object.""" + bl_idname = 'LNGetChildrenNode' + bl_label = 'Get Object Children' + arm_section = 'relations' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Parent') + + self.add_output('ArmNodeSocketArray', 'Children') diff --git a/blender/arm/logicnode/object/LN_get_object_mesh.py b/blender/arm/logicnode/object/LN_get_object_mesh.py new file mode 100644 index 0000000000..9339134e0c --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_mesh.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetMeshNode(ArmLogicTreeNode): + """Returns the mesh of the given object.""" + bl_idname = 'LNGetMeshNode' + bl_label = 'Get Object Mesh' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmDynamicSocket', 'Mesh') diff --git a/blender/arm/logicnode/object/LN_get_object_name.py b/blender/arm/logicnode/object/LN_get_object_name.py new file mode 100644 index 0000000000..bb242d3234 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_name.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetNameNode(ArmLogicTreeNode): + """Returns the name of the given object.""" + bl_idname = 'LNGetNameNode' + bl_label = 'Get Object Name' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmStringSocket', 'Name') diff --git a/blender/arm/logicnode/object/LN_get_object_offscreen.py b/blender/arm/logicnode/object/LN_get_object_offscreen.py new file mode 100644 index 0000000000..6bd85f8bc8 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_offscreen.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetObjectOffscreenNode(ArmLogicTreeNode): + """Returns if the given object is offscreen. Don't works if culling is disabled.""" + bl_idname = 'LNGetObjectOffscreenNode' + bl_label = 'Get Object Offscreen' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmNodeSocketObject', 'Object') + + self.outputs.new('ArmBoolSocket', 'Is Object Offscreen') + self.outputs.new('ArmBoolSocket', 'Is Mesh Offscreen') + self.outputs.new('ArmBoolSocket', 'Is Shadow Offscreen') diff --git a/blender/arm/logicnode/object/LN_get_object_parent.py b/blender/arm/logicnode/object/LN_get_object_parent.py new file mode 100644 index 0000000000..5ca239a62f --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_parent.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetParentNode(ArmLogicTreeNode): + """Returns the direct parent (nearest in the hierarchy) of the given object. + + @seeNode Set Object Parent""" + bl_idname = 'LNGetParentNode' + bl_label = 'Get Object Parent' + arm_section = 'relations' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Child') + + self.add_output('ArmNodeSocketObject', 'Parent') diff --git a/blender/arm/logicnode/object/LN_get_object_property.py b/blender/arm/logicnode/object/LN_get_object_property.py new file mode 100644 index 0000000000..3332677b09 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_property.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class GetPropertyNode(ArmLogicTreeNode): + """Returns the value of the given object property. + + @seeNode Set Object Property""" + bl_idname = 'LNGetPropertyNode' + bl_label = 'Get Object Property' + arm_version = 1 + arm_section = 'props' + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Property') + + self.add_output('ArmDynamicSocket', 'Value') + self.add_output('ArmStringSocket', 'Property') diff --git a/blender/arm/logicnode/object/LN_get_object_uid.py b/blender/arm/logicnode/object/LN_get_object_uid.py new file mode 100644 index 0000000000..ba54651c21 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_uid.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetUidNode(ArmLogicTreeNode): + """Returns the uid of the given object.""" + bl_idname = 'LNGetUidNode' + bl_label = 'Get Object Uid' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmIntSocket', 'Uid') diff --git a/blender/arm/logicnode/object/LN_get_object_visible.py b/blender/arm/logicnode/object/LN_get_object_visible.py new file mode 100644 index 0000000000..e01cbf5742 --- /dev/null +++ b/blender/arm/logicnode/object/LN_get_object_visible.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class GetVisibleNode(ArmLogicTreeNode): + """Returns whether the given object or its visual components are + visible. + + @seeNode Set Object Visible""" + bl_idname = 'LNGetVisibleNode' + bl_label = 'Get Object Visible' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmBoolSocket', 'Is Object Visible') + self.add_output('ArmBoolSocket', 'Is Mesh Visible') + self.add_output('ArmBoolSocket', 'Is Shadow Visible') diff --git a/blender/arm/logicnode/object/LN_mesh.py b/blender/arm/logicnode/object/LN_mesh.py new file mode 100644 index 0000000000..3f05dda286 --- /dev/null +++ b/blender/arm/logicnode/object/LN_mesh.py @@ -0,0 +1,18 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class MeshNode(ArmLogicTreeNode): + """Stores the given mesh as a variable.""" + bl_idname = 'LNMeshNode' + bl_label = 'Mesh' + arm_version = 1 + + property0_get: HaxePointerProperty('property0_get', name='', type=bpy.types.Mesh) + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Mesh', is_var=True) + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property0_get', bpy.data, 'meshes', icon='NONE', text='') diff --git a/blender/arm/logicnode/object/LN_object.py b/blender/arm/logicnode/object/LN_object.py new file mode 100644 index 0000000000..c9e937bb26 --- /dev/null +++ b/blender/arm/logicnode/object/LN_object.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + + +class ObjectNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given object as a variable.""" + bl_idname = 'LNObjectNode' + bl_label = 'Object' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object In') + + self.add_output('ArmNodeSocketObject', 'Object Out', is_var=True) + + def draw_label(self) -> str: + inp_object = self.inputs['Object In'] + if inp_object.is_linked: + return super().draw_label() + + obj_name = inp_object.get_default_value() + if obj_name == '': + obj_name = '_self' + + return f'{super().draw_label()}: {obj_name}' + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].default_value_raw diff --git a/blender/arm/logicnode/object/LN_raycast_closest_object.py b/blender/arm/logicnode/object/LN_raycast_closest_object.py new file mode 100644 index 0000000000..5c12b888bf --- /dev/null +++ b/blender/arm/logicnode/object/LN_raycast_closest_object.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class RaycastClosestObjectNode(ArmLogicTreeNode): + """it takes an objects array and returns true of false if at least one of those objects is touched at screen (x, y), the object that is touched and the (x,y, z) position of that touch if returned""" + bl_idname = 'LNRaycastClosestObjectNode' + bl_label = 'Raycast Closest Object' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketArray', 'Obj Array') + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'True') + self.add_output('ArmNodeSocketAction', 'False') + self.add_output('ArmNodeSocketObject', 'Object') + self.add_output('ArmVectorSocket', 'Location') diff --git a/blender/arm/logicnode/object/LN_raycast_object.py b/blender/arm/logicnode/object/LN_raycast_object.py new file mode 100644 index 0000000000..10a5a338a9 --- /dev/null +++ b/blender/arm/logicnode/object/LN_raycast_object.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class RaycastObjectNode(ArmLogicTreeNode): + """it takes an object and returns true or false if the object is touched at screen (x, y) and the (x,y, z) position of that touch if returned""" + bl_idname = 'LNRaycastObjectNode' + bl_label = 'Raycast Object' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmNodeSocketObject', 'Camera') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'True') + self.add_output('ArmNodeSocketAction', 'False') + self.add_output('ArmVectorSocket', 'Location') + diff --git a/blender/arm/logicnode/object/LN_remove_object.py b/blender/arm/logicnode/object/LN_remove_object.py new file mode 100644 index 0000000000..5770342a20 --- /dev/null +++ b/blender/arm/logicnode/object/LN_remove_object.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class RemoveObjectNode(ArmLogicTreeNode): + """This node will remove a scene object or a scene object and its children. + + @input Object: Scene object to remove. + @input Remove Children: Remove scene object's children too. + @input Keep Children Transforms: Scene object's children will maintain current transforms when the scene object is removed, else children transforms revert to scene origin transforms. + """ + bl_idname = 'LNRemoveObjectNode' + bl_label = 'Remove Object' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Remove Children', default_value=True) + self.add_input('ArmBoolSocket', 'Keep Children Transforms', default_value=True) + + 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/object/LN_remove_object_parent.py b/blender/arm/logicnode/object/LN_remove_object_parent.py new file mode 100644 index 0000000000..80439051d0 --- /dev/null +++ b/blender/arm/logicnode/object/LN_remove_object_parent.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ClearParentNode(ArmLogicTreeNode): + """Removes the parent of the given object.""" + bl_idname = 'LNClearParentNode' + bl_label = 'Remove Object Parent' + arm_section = 'relations' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Keep Transform', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/object/LN_self_object.py b/blender/arm/logicnode/object/LN_self_object.py new file mode 100644 index 0000000000..0dcbd0d09b --- /dev/null +++ b/blender/arm/logicnode/object/LN_self_object.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class SelfObjectNode(ArmLogicTreeNode): + """Returns the object that owns the trait.""" + bl_idname = 'LNSelfNode' + bl_label = 'Self Object' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/object/LN_set_object_mesh.py b/blender/arm/logicnode/object/LN_set_object_mesh.py new file mode 100644 index 0000000000..114e007880 --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_mesh.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetMeshNode(ArmLogicTreeNode): + """Sets the mesh of the given object.""" + bl_idname = 'LNSetMeshNode' + bl_label = 'Set Object Mesh' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Mesh') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/object/LN_set_object_name.py b/blender/arm/logicnode/object/LN_set_object_name.py new file mode 100644 index 0000000000..236e25b6d6 --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_name.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetNameNode(ArmLogicTreeNode): + """Sets the name of the given object.""" + bl_idname = 'LNSetNameNode' + bl_label = 'Set Object Name' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Name') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/object/LN_set_object_parent.py b/blender/arm/logicnode/object/LN_set_object_parent.py new file mode 100644 index 0000000000..b664044817 --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_parent.py @@ -0,0 +1,32 @@ +from arm.logicnode.arm_nodes import * + +class SetParentNode(ArmLogicTreeNode): + """Sets the direct parent (nearest in the hierarchy) of the given object. + @input Object: Object to be parented. + @input Parent: New parent object. Use `Get Scene Root` node to unparent object. + @input Keep Transform: Keep transform after unparenting from old parent + @input Parent Inverse: Preserve object transform after parenting to new object. + + @seeNode Get Object Parent""" + bl_idname = 'LNSetParentNode' + bl_label = 'Set Object Parent' + arm_section = 'relations' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmNodeSocketObject', 'Parent') + self.add_input('ArmBoolSocket', 'Keep Transform', default_value = True) + self.add_input('ArmBoolSocket', 'Parent Inverse') + + 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( + 'LNSetParentNode', self.arm_version, 'LNSetParentNode', 2, + in_socket_mapping={0:0, 1:1, 2:2}, out_socket_mapping={0:0} + ) diff --git a/blender/arm/logicnode/object/LN_set_object_property.py b/blender/arm/logicnode/object/LN_set_object_property.py new file mode 100644 index 0000000000..326beb2f9a --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_property.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + +class SetPropertyNode(ArmLogicTreeNode): + """Sets the value of the given object property. + + This node can be used to share variables between different traits. + If the trait(s) you want to access the variable with are on + different objects, use the *[`Global Object`](#global-object)* + node to store the data. Every trait can access this one. + + @seeNode Get Object Property""" + bl_idname = 'LNSetPropertyNode' + bl_label = 'Set Object Property' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Property') + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/object/LN_set_object_shape_key.py b/blender/arm/logicnode/object/LN_set_object_shape_key.py new file mode 100644 index 0000000000..87888cdf05 --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_shape_key.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class SetObjectShapeKeyNode(ArmLogicTreeNode): + """Sets shape key value of the object""" + bl_idname = 'LNSetObjectShapeKeyNode' + bl_label = 'Set Object Shape Key' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Shape Key') + self.add_input('ArmFloatSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/object/LN_set_object_visible.py b/blender/arm/logicnode/object/LN_set_object_visible.py new file mode 100644 index 0000000000..09107e9ec9 --- /dev/null +++ b/blender/arm/logicnode/object/LN_set_object_visible.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + +class SetVisibleNode(ArmLogicTreeNode): + """Sets whether the given object is visible. + + @seeNode Get Object Visible""" + bl_idname = 'LNSetVisibleNode' + bl_label = 'Set Object Visible' + arm_section = 'props' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('object', 'Object', 'All object componenets visibility'), + ('mesh', 'Mesh', 'Mesh visibility only'), + ('shadow', 'Shadow', 'Shadow visibility only'), + ], + name='', default='object') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', 'Visible') + self.add_input('ArmBoolSocket', 'Children', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/object/LN_spawn_object.py b/blender/arm/logicnode/object/LN_spawn_object.py new file mode 100644 index 0000000000..de4ff22f57 --- /dev/null +++ b/blender/arm/logicnode/object/LN_spawn_object.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class SpawnObjectNode(ArmLogicTreeNode): + """Spawns the given object if present in the current active scene. The spawned object has the same name of its instance, but they are treated as different objects.""" + + bl_idname = 'LNSpawnObjectNode' + bl_label = 'Spawn Object' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Transform') + self.add_input('ArmBoolSocket', 'Children', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/object/LN_spawn_object_by_name.py b/blender/arm/logicnode/object/LN_spawn_object_by_name.py new file mode 100644 index 0000000000..3fed2d7609 --- /dev/null +++ b/blender/arm/logicnode/object/LN_spawn_object_by_name.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + +class SpawnObjectByNameNode(ArmLogicTreeNode): + """Spawns an object bearing the given name, even if not present in the active scene""" + bl_idname = 'LNSpawnObjectByNameNode' + bl_label = 'Spawn Object By Name' + arm_version = 1 + + property0: HaxePointerProperty( + 'property0', + type=bpy.types.Scene, name='Scene', + description='The scene from which to take the object') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Name') + self.add_input('ArmDynamicSocket', 'Transform') + self.add_input('ArmBoolSocket', 'Children', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'Object') + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property0', bpy.data, "scenes") diff --git a/blender/arm/logicnode/object/__init__.py b/blender/arm/logicnode/object/__init__.py new file mode 100644 index 0000000000..92a396311b --- /dev/null +++ b/blender/arm/logicnode/object/__init__.py @@ -0,0 +1,5 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Object') +add_node_section(name='props', category='Object') +add_node_section(name='relations', category='Object') diff --git a/blender/arm/logicnode/physics/LN_Add_rigid_body.py b/blender/arm/logicnode/physics/LN_Add_rigid_body.py new file mode 100644 index 0000000000..ab7dc3ffb7 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_Add_rigid_body.py @@ -0,0 +1,140 @@ +from arm.logicnode.arm_nodes import * + +class AddRigidBodyNode(ArmLogicTreeNode): + """Adds a rigid body to an object if not already present. + + @option Advanced: Shows optional advanced options for rigid body. + + @option Shape: Shape of the rigid body including Box, Sphere, Capsule, Cone, Cylinder, Convex Hull and Mesh + + @input Object: Object to which rigid body is added. + + @input Mass: Mass of the rigid body. Must be > 0. + + @input Active: Rigid body actively participates in the physics world and will be affected by collisions + + @input Animated: Rigid body follows animation and will affect other active non-animated rigid bodies. + + @input Trigger: Rigid body behaves as a trigger and detects collision. However, rigd body does not contribute to or receive collissions. + + @input Friction: Surface friction of the rigid body. Minimum value = 0, Preferred max value = 1. + + @input Bounciness: How elastic is the surface of the rigid body. Minimum value = 0, Preferred max value = 1. + + @input Continuous Collision Detection (CCD): Detects for collisions in between frames. Use only for very fast moving objects. + + @input Collision Margin: Enable an external margin for collision detection + + @input Margin: Length of the collision margin. Must be > 0. + + @input Linear Damping: Damping for linear translation. Recommended range 0 to 1. + + @input Angular Damping: Damping for angular translation. Recommended range 0 to 1. + + @input Angular Friction: Rolling or angular friction. Recommended range >= 0 + + @input Use Deactivation: Deactive this rigid body when below the Linear and Angular velocity threshold. Enable to improve performance. + + @input Linear Velocity Threshold: Velocity below which decativation occurs if enabled. + + @input Angular Velocity Threshold: Velocity below which decativation occurs if enabled. + + @input Collision Group: A set of rigid bodies that can interact with each other + + @input Collision Mask: Bitmask to filter collisions. Collision can occur between two rigid bodies if they have atleast one bit in common. + + @output Rigid body: Object to which rigid body was added. + + @output Out: activated after rigid body is added. + """ + + bl_idname = 'LNAddRigidBodyNode' + bl_label = 'Add Rigid Body' + arm_version = 2 + + NUM_STATIC_INS = 9 + + def update_advanced(self, context): + """This is a helper method to allow declaring the `advanced` + property before the update_sockets() method. It's not required + but then you would need to move the declaration of `advanced` + further down.""" + self.update_sockets(context) + + property1: HaxeBoolProperty( + 'property1', + name="Advanced", + description="Show advanced options", + default=False, + update=update_advanced + ) + + property0: HaxeEnumProperty( + 'property0', + items = [('Box', 'Box', 'Box'), + ('Sphere', 'Sphere', 'Sphere'), + ('Capsule', 'Capsule', 'Capsule'), + ('Cone', 'Cone', 'Cone'), + ('Cylinder', 'Cylinder', 'Cylinder'), + ('Convex Hull', 'Convex Hull', 'Convex Hull'), + ('Mesh', 'Mesh', 'Mesh')], + name='Shape', default='Box') + + def arm_init(self, context): + + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmFloatSocket', 'Mass', 1.0) + self.add_input('ArmBoolSocket', 'Active', True) + self.add_input('ArmBoolSocket', 'Animated', False) + self.add_input('ArmBoolSocket', 'Trigger', False) + self.add_input('ArmFloatSocket', 'Friction', 0.5) + self.add_input('ArmFloatSocket', 'Bounciness', 0.0) + self.add_input('ArmBoolSocket', 'Continuous Collision Detection', False) + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'Rigid body') + + self.update_sockets(context) + + def update_sockets(self, context): + # It's bad to remove from a list during iteration so we use + # this helper list here + remove_list = [] + + # Remove dynamically placed input sockets + for i in range(AddRigidBodyNode.NUM_STATIC_INS, len(self.inputs)): + remove_list.append(self.inputs[i]) + for i in remove_list: + self.inputs.remove(i) + + # Add dynamic input sockets + if self.property1: + self.add_input('ArmBoolSocket', 'Collision Margin', False) + self.add_input('ArmFloatSocket', 'Margin', 0.04) + self.add_input('ArmFloatSocket', 'Linear Damping', 0.04) + self.add_input('ArmFloatSocket', 'Angular Damping', 0.1) + self.add_input('ArmFloatSocket', 'Angular Friction', 0.1) + self.add_input('ArmBoolSocket', 'Use Deacivation') + self.add_input('ArmFloatSocket', 'Linear Velocity Threshold', 0.4) + self.add_input('ArmFloatSocket', 'Angular Velocity Threshold', 0.5) + self.add_input('ArmIntSocket', 'Collision Group', 1) + self.add_input('ArmIntSocket', 'Collision Mask', 1) + + + def draw_buttons(self, context, layout): + layout.prop(self, "property1") + layout.prop(self, 'property0') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + in_socket_mapping={0:0, 1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8} + if self.property1: + in_socket_mapping.update({9:9, 10:10, 11:11, 12:12, 13:14, 14:15, 15:16, 16:17, 17:18}) + + return NodeReplacement( + 'LNAddRigidBodyNode', self.arm_version, 'LNAddRigidBodyNode', 2, + in_socket_mapping=in_socket_mapping, + out_socket_mapping={0:0, 1:1}, + property_mapping={'property0':'property0', 'property1':'property1'}) diff --git a/blender/arm/logicnode/physics/LN_add_physics_constraint.py b/blender/arm/logicnode/physics/LN_add_physics_constraint.py new file mode 100644 index 0000000000..1e59a422d7 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_add_physics_constraint.py @@ -0,0 +1,176 @@ +from arm.logicnode.arm_nodes import * + +class AddPhysicsConstraintNode(ArmLogicTreeNode): + """ + Add a physics constraint to constrain two rigid bodies if not already present. + + @option Fixed: No fredom of movement. Relative positions and rotations of rigid bodies are fixed + + @option Point: Both rigid bodies are constrained at the pivot object. + + @option Hinge: Constrained objects can move only along angular Z axis of the pivot object. + + @option Slider: Constrained objects can move only along linear X axis of the pivot object. + + @option Piston: Constrained objects can move only and rotate along X axis of the pivot object. + + @option GenericSpring: Fully custimizable generic 6 degree of freedom constraint with optional springs. All liner and angular axes can be constrained + along with spring options. Use `Physics Constraint Node` to set a combination of constraints and springs. + + @seeNode Physics Constraint + + @input Pivot object: The object to which the physics constraint traint is applied. This object will not be affected by the constraint + but is necessary to specify the constraint axes and location. Hence, the pivot object need not be a rigid body. Typically an `Empty` + object may be used. Each pivot object can have only one constraint trait applied. Moving/rotating/parenting the pivot object after the constraint + is applied has no effect. However, removig the pivot object removes the constraint and `RB 1` and `RB 2` are no longer constrained. + + @input RB 1: The first rigid body to be constrained. Must be a rigid body. This object can be constrained by more than one constraint. + + @input RB 2: The second rigid body to be constrained. Must be a rigid body. This object can be constrained by more than one constraint. + + @input Disable Collisions: Disable collisions between `RB 1` and `RB 2` + + @input Breakable: Constraint can break if stress on the constraint is more than the set threshold. Disable this option to disable breaking. + + @input Breaking threshold: Stress on the constraint above which the constraint breaks. Depends on the mass, velocity of rigid bodies and type of constraint. + + @input Limit Lower: Lower limit of the consraint in that particular axis + + @input Limit Upper: Upper limit of the constraint in that particular axis. (`lower limit` = `upper limit`) --> Fully constrained. (`lower limit` < `upper limit`) --> Partially constrained + (`lower limit` > `upper limit`) --> Full freedom. + + @input Angular limits: Limits to constarin rotation. Specified in degrees. Range (-360 to +360) + + @input Add Constarint: Option to add custom constraint to `Generic Spring` type. + """ + + + bl_idname = 'LNAddPhysicsConstraintNode' + bl_label = 'Add Physics Constraint' + arm_section = 'add' + arm_version = 1 + + @staticmethod + def get_enum_id_value(obj, prop_name, value): + return obj.bl_rna.properties[prop_name].enum_items[value].identifier + + @staticmethod + def get_count_in(type_name): + return { + 'Fixed': 0, + 'Point': 1, + 'Hinge': 2, + 'Slider': 3, + 'Piston': 4, + 'Generic Spring': 5 + }.get(type_name, 0) + + def get_enum(self): + return self.get('property0', 0) + + def set_enum(self, value): + # Checking the selection of another type + select_current = self.get_enum_id_value(self, 'property0', value) + select_prev = self.property0 + + #Check if a different type is selected + if select_prev != select_current: + print('New value selected') + # Arguements for type Fixed + if (self.get_count_in(select_current) == 0): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + + # Arguements for type Point + if (self.get_count_in(select_current) == 1): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + + #Arguements for type Hinge + if (self.get_count_in(select_current) == 2): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + #Z ang limits + self.add_input('ArmBoolSocket', 'Z angle') + self.add_input('ArmFloatSocket', 'Z ang lower', -45.0) + self.add_input('ArmFloatSocket', 'Z ang upper', 45.0) + + #Arguements for type Slider + if (self.get_count_in(select_current) == 3): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + #X lin limits + self.add_input('ArmBoolSocket', 'X linear') + self.add_input('ArmFloatSocket', 'X lin lower') + self.add_input('ArmFloatSocket', 'X lin upper') + + #Arguements for type Piston + if (self.get_count_in(select_current) == 4): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + #X lin limits + self.add_input('ArmBoolSocket', 'X linear') + self.add_input('ArmFloatSocket', 'X lin lower') + self.add_input('ArmFloatSocket', 'X lin upper') + #X ang limits + self.add_input('ArmBoolSocket', 'X angle') + self.add_input('ArmFloatSocket', 'X ang lower', -45.0) + self.add_input('ArmFloatSocket', 'X ang upper', 45.0) + + #Arguements for type GenericSpring + if (self.get_count_in(select_current) == 5): + while (len(self.inputs) > 7): + self.inputs.remove(self.inputs.values()[-1]) + + self['property0'] = value + + property0: HaxeEnumProperty( + 'property0', + items = [('Fixed', 'Fixed', 'Fixed'), + ('Point', 'Point', 'Point'), + ('Hinge', 'Hinge', 'Hinge'), + ('Slider', 'Slider', 'Slider'), + ('Piston', 'Piston', 'Piston'), + ('Generic Spring', 'Generic Spring', 'Generic Spring')], + name='Type', default='Fixed', set=set_enum, get=get_enum) + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Pivot Object') + self.add_input('ArmNodeSocketObject', 'RB 1') + self.add_input('ArmNodeSocketObject', 'RB 2') + self.add_input('ArmBoolSocket', 'Disable Collissions') + self.add_input('ArmBoolSocket', 'Breakable') + self.add_input('ArmFloatSocket', 'Breaking Threshold') + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + #GenericSpring: + if (self.get_count_in(self.property0) == 5): + grid0 = layout.grid_flow(row_major=True, columns=1, align=True) + grid0.label(text="Possible Constraints:") + grid0.label(text="Linear [X, Y, Z]") + grid0.label(text="Angular [X, Y, Z]") + grid0.label(text="Spring Linear [X, Y, Z]") + grid0.label(text="Spring Angular [X, Y, Z]") + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_add_input', text='Add Constraint', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmDynamicSocket' + op.name_format = 'Constraint {0}'.format(len(self.inputs) - 6) + column1 = row.column(align=True) + op = column1.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + #Static inputs + if len(self.inputs) < 8: + column1.enabled = False + #Max Possible inputs + if len(self.inputs) > 18: + column.enabled = False + diff --git a/blender/arm/logicnode/physics/LN_apply_force.py b/blender/arm/logicnode/physics/LN_apply_force.py new file mode 100644 index 0000000000..20aba1b164 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_force.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + +class ApplyForceNode(ArmLogicTreeNode): + """Applies force in the given rigid body. + + @seeNode Apply Force At Location + @seeNode Apply Impulse + @seeNode Apply Impulse At Location + + @input Force: the force vector + @input On Local Axis: if `true`, interpret the force vector as in + object space + """ + bl_idname = 'LNApplyForceNode' + bl_label = 'Apply Force' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Force') + self.add_input('ArmBoolSocket', 'On Local Axis') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_apply_force_at_location.py b/blender/arm/logicnode/physics/LN_apply_force_at_location.py new file mode 100644 index 0000000000..8a308a27f1 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_force_at_location.py @@ -0,0 +1,30 @@ +from arm.logicnode.arm_nodes import * + +class ApplyForceAtLocationNode(ArmLogicTreeNode): + """Applies force in the given rigid body at the given position. + + @seeNode Apply Force + @seeNode Apply Impulse + @seeNode Apply Impulse At Location + + @input Force: the force vector + @input Force On Local Axis: if `true`, interpret the force vector as in + object space + @input Location: the location where to apply the force + @input Relative Location: if `true`, use the location relative + to the objects location, otherwise use world coordinates + """ + bl_idname = 'LNApplyForceAtLocationNode' + bl_label = 'Apply Force At Location' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Force') + self.add_input('ArmBoolSocket', 'Force On Local Axis') + self.add_input('ArmVectorSocket', 'Location') + self.add_input('ArmBoolSocket', 'Relative Location') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_apply_impulse.py b/blender/arm/logicnode/physics/LN_apply_impulse.py new file mode 100644 index 0000000000..68c2006dd9 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_impulse.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + +class ApplyImpulseNode(ArmLogicTreeNode): + """Applies impulse in the given rigid body. + + @seeNode Apply Impulse At Location + @seeNode Apply Force + @seeNode Apply Force At Location + + @input Impulse: the impulse vector + @input On Local Axis: if `true`, interpret the impulse vector as in + object space + """ + bl_idname = 'LNApplyImpulseNode' + bl_label = 'Apply Impulse' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Impulse') + self.add_input('ArmBoolSocket', 'On Local Axis') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_apply_impulse_at_location.py b/blender/arm/logicnode/physics/LN_apply_impulse_at_location.py new file mode 100644 index 0000000000..5345cda7bf --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_impulse_at_location.py @@ -0,0 +1,30 @@ +from arm.logicnode.arm_nodes import * + +class ApplyImpulseAtLocationNode(ArmLogicTreeNode): + """Applies impulse in the given rigid body at the given position. + + @seeNode Apply Impulse + @seeNode Apply Force + @seeNode Apply Force At Location + + @input Impulse: the impulse vector + @input Impulse On Local Axis: if `true`, interpret the impulse vector as in + object space + @input Location: the location where to apply the impulse + @input Relative Location: if `true`, use the location relative + to the objects location, otherwise use world coordinates + """ + bl_idname = 'LNApplyImpulseAtLocationNode' + bl_label = 'Apply Impulse At Location' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Impulse') + self.add_input('ArmBoolSocket', 'Impulse On Local Axis') + self.add_input('ArmVectorSocket', 'Location') + self.add_input('ArmBoolSocket', 'Relative Location') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_apply_torque.py b/blender/arm/logicnode/physics/LN_apply_torque.py new file mode 100644 index 0000000000..70d2633e7d --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_torque.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class ApplyTorqueNode(ArmLogicTreeNode): + """Applies torque to the given rigid body.""" + bl_idname = 'LNApplyTorqueNode' + bl_label = 'Apply Torque' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Torque') + self.add_input('ArmBoolSocket', 'On Local Axis') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_apply_torque_impulse.py b/blender/arm/logicnode/physics/LN_apply_torque_impulse.py new file mode 100644 index 0000000000..2bf208c27a --- /dev/null +++ b/blender/arm/logicnode/physics/LN_apply_torque_impulse.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class ApplyTorqueImpulseNode(ArmLogicTreeNode): + """Applies torque impulse in the given rigid body.""" + bl_idname = 'LNApplyTorqueImpulseNode' + bl_label = 'Apply Torque Impulse' + arm_section = 'force' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Torque') + self.add_input('ArmBoolSocket', 'On Local Axis') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_convex_cast.py b/blender/arm/logicnode/physics/LN_convex_cast.py new file mode 100644 index 0000000000..fb3ff4a9e1 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_convex_cast.py @@ -0,0 +1,34 @@ +from arm.logicnode.arm_nodes import * + +class ConvexCastNode(ArmLogicTreeNode): + """Casts a convex rigid body and get the closest hit point. Also called Convex Sweep Test. + + @seeNode Mask + + @input Convex RB: A convex Rigid Body object to be used for the sweep test. + @input From: The initial location of the convex object. + @input To: The final location of the convex object. + @input Rotation: Rotation of the Convex RB during sweep test. + @input Mask: A bit mask value to specify which + objects are considered + + @output Hit Position: The hit position in world coordinates + @output Convex Position: Position of the convex RB at the time of collision. + @output Normal: The surface normal of the hit position relative to + the world. + """ + bl_idname = 'LNPhysicsConvexCastNode' + bl_label = 'Convex Cast' + arm_section = 'ray' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Convex RB') + self.add_input('ArmVectorSocket', 'From') + self.add_input('ArmVectorSocket', 'To') + self.add_input('ArmRotationSocket', 'Rotation') + self.add_input('ArmIntSocket', 'Mask', default_value=1) + + self.add_output('ArmVectorSocket', 'Hit Position') + self.add_output('ArmVectorSocket', 'Convex Position') + self.add_output('ArmVectorSocket', 'Normal') diff --git a/blender/arm/logicnode/physics/LN_convex_cast_on.py b/blender/arm/logicnode/physics/LN_convex_cast_on.py new file mode 100644 index 0000000000..ffcbdfd9e8 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_convex_cast_on.py @@ -0,0 +1,38 @@ +from arm.logicnode.arm_nodes import * + +class ConvexCastOnNode(ArmLogicTreeNode): + """Casts a convex rigid body and get the closest hit point. Also called Convex Sweep Test. + + @seeNode Mask + + @input In: Input trigger + @input Convex RB: A convex Rigid Body object to be used for the sweep test. + @input From: The initial location of the convex object. + @input To: The final location of the convex object. + @input Rotation: Rotation of the Convex RB during sweep test. + @input Mask: A bit mask value to specify which + objects are considered + + @output Out: Output after hit + @output Hit Position: The hit position in world coordinates + @output Convex Position: Position of the convex RB at the time of collision. + @output Normal: The surface normal of the hit position relative to + the world. + """ + bl_idname = 'LNPhysicsConvexCastOnNode' + bl_label = 'Convex Cast On' + arm_section = 'ray' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Convex RB') + self.add_input('ArmVectorSocket', 'From') + self.add_input('ArmVectorSocket', 'To') + self.add_input('ArmRotationSocket', 'Rotation') + self.add_input('ArmIntSocket', 'Mask', default_value=1) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmVectorSocket', 'Hit Position') + self.add_output('ArmVectorSocket', 'Convex Position') + self.add_output('ArmVectorSocket', 'Normal') diff --git a/blender/arm/logicnode/physics/LN_get_rb_contacts.py b/blender/arm/logicnode/physics/LN_get_rb_contacts.py new file mode 100644 index 0000000000..9f63fae5ce --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_rb_contacts.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class GetContactsNode(ArmLogicTreeNode): + """Returns an array with all objects that are colliding with the + given object. + + @seeNode Get First Contact + """ + bl_idname = 'LNGetContactsNode' + bl_label = 'Get RB Contacts' + arm_section = 'contact' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + + self.add_output('ArmNodeSocketArray', 'Contacts') diff --git a/blender/arm/logicnode/physics/LN_get_rb_data.py b/blender/arm/logicnode/physics/LN_get_rb_data.py new file mode 100644 index 0000000000..76ecb2e2b6 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_rb_data.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class GetRigidBodyDataNode(ArmLogicTreeNode): + """Returns the data of the given rigid body.""" + bl_idname = 'LNGetRigidBodyDataNode' + bl_label = 'Get RB Data' + arm_section = 'props' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmNodeSocketObject', 'Object') + + self.outputs.new('ArmBoolSocket', 'Is RB') + self.outputs.new('ArmIntSocket', 'Collision Group') + self.outputs.new('ArmIntSocket', 'Collision Mask') + self.outputs.new('ArmBoolSocket', 'Is Animated') + self.outputs.new('ArmBoolSocket', 'Is Static') + self.outputs.new('ArmFloatSocket', 'Angular Damping') + self.outputs.new('ArmFloatSocket', 'Linear Damping') + self.outputs.new('ArmFloatSocket', 'Friction') + self.outputs.new('ArmFloatSocket', 'Mass') + #self.outputs.new('ArmStringSocket', 'Collision Shape') + #self.outputs.new('ArmIntSocket', 'Activation State') + #self.outputs.new('ArmBoolSocket', 'Is Gravity Enabled') + #self.outputs.new(ArmVectorSocket', Angular Factor') + #self.outputs.new('ArmVectorSocket', Linear Factor') diff --git a/blender/arm/logicnode/physics/LN_get_rb_first_contact.py b/blender/arm/logicnode/physics/LN_get_rb_first_contact.py new file mode 100644 index 0000000000..ea8ea7af84 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_rb_first_contact.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class GetFirstContactNode(ArmLogicTreeNode): + """Returns the first object that is colliding with the given object. + + @seeNode Get Contacts + """ + bl_idname = 'LNGetFirstContactNode' + bl_label = 'Get RB First Contact' + arm_section = 'contact' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + + self.add_output('ArmNodeSocketObject', 'First Contact') diff --git a/blender/arm/logicnode/physics/LN_get_rb_point_velocity.py b/blender/arm/logicnode/physics/LN_get_rb_point_velocity.py new file mode 100644 index 0000000000..0be723b8d9 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_rb_point_velocity.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetPointVelocityNode(ArmLogicTreeNode): + """Returns the world velocity of the given point along the rigid body.""" + bl_idname = 'LNGetPointVelocityNode' + bl_label = 'Get RB Point Velocity' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Point') + + self.add_output('ArmVectorSocket', 'Velocity') diff --git a/blender/arm/logicnode/physics/LN_get_rb_velocity.py b/blender/arm/logicnode/physics/LN_get_rb_velocity.py new file mode 100644 index 0000000000..18dcadebc4 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_rb_velocity.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class GetVelocityNode(ArmLogicTreeNode): + """Returns the world velocity of the given rigid body.""" + bl_idname = 'LNGetVelocityNode' + bl_label = 'Get RB Velocity' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmBoolSocket', 'Linear On Local Axis') + self.add_input('ArmBoolSocket', 'Angular On Local Axis') + + self.add_output('ArmVectorSocket', 'Linear') + self.add_output('ArmVectorSocket', 'Angular') diff --git a/blender/arm/logicnode/physics/LN_get_world_gravity.py b/blender/arm/logicnode/physics/LN_get_world_gravity.py new file mode 100644 index 0000000000..0fb4aec81b --- /dev/null +++ b/blender/arm/logicnode/physics/LN_get_world_gravity.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetGravityNode(ArmLogicTreeNode): + """Returns the world gravity. + + @seeNode Set Gravity + """ + bl_idname = 'LNGetGravityNode' + bl_label = 'Get World Gravity' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'World Gravity') diff --git a/blender/arm/logicnode/physics/LN_has_contact.py b/blender/arm/logicnode/physics/LN_has_contact.py new file mode 100644 index 0000000000..627180e333 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_has_contact.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class HasContactNode(ArmLogicTreeNode): + """Returns whether the given rigid body has contact with another given rigid body.""" + bl_idname = 'LNHasContactNode' + bl_label = 'Has Contact' + arm_section = 'contact' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB 1') + self.add_input('ArmNodeSocketObject', 'RB 2') + + self.add_output('ArmBoolSocket', 'Has Contact') diff --git a/blender/arm/logicnode/physics/LN_has_contact_array.py b/blender/arm/logicnode/physics/LN_has_contact_array.py new file mode 100644 index 0000000000..a7a6b759ca --- /dev/null +++ b/blender/arm/logicnode/physics/LN_has_contact_array.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class HasContactArrayNode(ArmLogicTreeNode): + """Returns whether the given rigid body has contact with other given rigid bodies.""" + bl_idname = 'LNHasContactArrayNode' + bl_label = 'Has Contact Array' + arm_section = 'contact' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmNodeSocketArray', 'RBs') + + self.add_output('ArmBoolSocket', 'Has Contact') diff --git a/blender/arm/logicnode/physics/LN_is_rb_active.py b/blender/arm/logicnode/physics/LN_is_rb_active.py new file mode 100644 index 0000000000..ad1e03ab1e --- /dev/null +++ b/blender/arm/logicnode/physics/LN_is_rb_active.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class IsRigidBodyActiveNode(ArmLogicTreeNode): + """Returns whether the given rigid body is active or sleeping.""" + bl_idname = 'LNIsRigidBodyActiveNode' + bl_label = 'RB Is Active' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + self.add_output('ArmBoolSocket', 'Is Active') diff --git a/blender/arm/logicnode/physics/LN_on_contact.py b/blender/arm/logicnode/physics/LN_on_contact.py new file mode 100644 index 0000000000..8ca26aab22 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_on_contact.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + +class OnContactNode(ArmLogicTreeNode): + """Activates the output when the rigid body make contact with + another rigid body. + + @option Begin: the output is activated on the first frame when the + two objects have contact + @option End: the output is activated on the frame after the last + frame when the two objects had contact + @option Overlap: the output is activated on each frame the object + have contact + """ + bl_idname = 'LNOnContactNode' + bl_label = 'On Contact' + arm_section = 'contact' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('begin', 'Begin', 'The contact between the rigid bodies begins'), + ('overlap', 'Overlap', 'The contact between the rigid bodies is happening'), + ('end', 'End', 'The contact between the rigid bodies ends')], + name='', default='begin') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB 1') + self.add_input('ArmNodeSocketObject', 'RB 2') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/physics/LN_on_contact_array.py b/blender/arm/logicnode/physics/LN_on_contact_array.py new file mode 100644 index 0000000000..c39a6ad242 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_on_contact_array.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + +class OnContactArrayNode(ArmLogicTreeNode): + """Activates the output when the given rigid body make contact with other given rigid bodies.""" + bl_idname = 'LNOnContactArrayNode' + bl_label = 'On Contact Array' + arm_section = 'contact' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('begin', 'Begin', 'The contact between the rigid bodies begins'), + ('overlap', 'Overlap', 'The contact between the rigid bodies is happening'), + ('end', 'End', 'The contact between the rigid bodies ends')], + name='', default='begin') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmNodeSocketArray', 'RBs') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/physics/LN_on_volume_trigger.py b/blender/arm/logicnode/physics/LN_on_volume_trigger.py new file mode 100644 index 0000000000..0c4cf2d529 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_on_volume_trigger.py @@ -0,0 +1,27 @@ +from arm.logicnode.arm_nodes import * + +class OnVolumeTriggerNode(ArmLogicTreeNode): + """Activates the output when the given object enters, overlaps or leaves the bounding box of the given trigger object. (Note: Works even if objects are not Rigid Bodies). + + @input RB: this object is taken as the entering object + @input Trigger: this object is used as the volume trigger + """ + bl_idname = 'LNOnVolumeTriggerNode' + bl_label = 'On Volume Trigger' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('begin', 'Begin', 'The contact between the rigid bodies begins'), + ('overlap', 'Overlap', 'The contact between the rigid bodies is happening'), + ('end', 'End', 'The contact between the rigid bodies ends')], + name='', default='begin') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object 1') + self.add_input('ArmNodeSocketObject', 'Object 2') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/physics/LN_physics_constraint.py b/blender/arm/logicnode/physics/LN_physics_constraint.py new file mode 100644 index 0000000000..efbda22c63 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_physics_constraint.py @@ -0,0 +1,74 @@ +from arm.logicnode.arm_nodes import * + + +class PhysicsConstraintNode(ArmLogicTreeNode): + """ + Custom physics constraint to add to `Add Physics Constarint` node. + + @option Linear/Angualr: Select if constrint is applied along linear or angular axis. + + @option Axis: Local axis of the pivot object along which the constraint is applied. + + @option Spring: Constraint is a Spring along the selected axis. + + @input Limit Lower: Lower limit of the consraint in that particular axis + + @input Limit Upper: Upper limit of the constraint in that particular axis. (`lower limit` = `upper limit`) --> Fully constrained. (`lower limit` < `upper limit`) --> Partially constrained + (`lower limit` > `upper limit`) --> Full freedom. + + @seeNode Add Physics Constraint + """ + + bl_idname = 'LNPhysicsConstraintNode' + bl_label = 'Physics Constraint' + arm_section = 'add' + arm_version = 1 + + def update_spring(self, context): + self.update_sockets(context) + + property0: HaxeEnumProperty( + 'property0', + items=[('Linear', 'Linear', 'Linear'), + ('Angular', 'Angular', 'Angular')], + name='Type', default='Linear') + + property1: HaxeEnumProperty( + 'property1', + items=[('X', 'X', 'X'), + ('Y', 'Y', 'Y'), + ('Z', 'Z', 'Z')], + name='Axis', default='X') + + property2: HaxeBoolProperty( + 'property2', + name="Spring", + description="Is a spring constraint", + default=False, + update=update_spring + ) + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Lower limit') + self.add_input('ArmFloatSocket', 'Upper limit') + self.add_output('ArmDynamicSocket', 'Constraint') + + def update_sockets(self, context): + while len(self.inputs) > 0: + self.inputs.remove(self.inputs.values()[-1]) + + # Add dynamic input sockets + if self.property2: + self.add_input('ArmFloatSocket', 'Stiffness', 10.0) + self.add_input('ArmFloatSocket', 'Damping', 0.5) + else: + self.add_input('ArmFloatSocket', 'Lower limit') + self.add_input('ArmFloatSocket', 'Upper limit') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + layout.prop(self, 'property2') diff --git a/blender/arm/logicnode/physics/LN_pick_rb.py b/blender/arm/logicnode/physics/LN_pick_rb.py new file mode 100644 index 0000000000..075da9c9e2 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_pick_rb.py @@ -0,0 +1,33 @@ +from arm.logicnode.arm_nodes import * + + +class PickObjectNode(ArmLogicTreeNode): + """Picks the rigid body in the given location using the screen + coordinates (2D). + + @seeNode Mask + + @input Screen Coords: the location at which to pick, in screen + coordinates + @input Mask: a bit mask value to specify which + objects are considered + + @output RB: the object that was hit + @output Hit: the hit position in world coordinates + @output Normal: the hit normal in world coordinates + """ + bl_idname = 'LNPickObjectNode' + bl_label = 'Pick RB' + arm_section = 'ray' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Screen Coords') + self.add_input('ArmIntSocket', 'Mask', default_value=1) + + self.add_output('ArmNodeSocketObject', 'RB') + self.add_output('ArmVectorSocket', 'Hit') + self.add_output('ArmVectorSocket', 'Normal') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/physics/LN_ray_cast.py b/blender/arm/logicnode/physics/LN_ray_cast.py new file mode 100644 index 0000000000..1fa4401a0e --- /dev/null +++ b/blender/arm/logicnode/physics/LN_ray_cast.py @@ -0,0 +1,32 @@ +from arm.logicnode.arm_nodes import * + +class RayCastNode(ArmLogicTreeNode): + """Casts a physics ray and returns the first object that is hit by + this ray. + + @seeNode Mask + + @input From: the location from where to start the ray, in world + coordinates + @input To: the target location of the ray, in world coordinates + @input Mask: a bit mask value to specify which + objects are considered + + @output RB: the object that was hit + @output Hit: the hit position in world coordinates + @output Normal: the surface normal of the hit position relative to + the world + """ + bl_idname = 'LNCastPhysicsRayNode' + bl_label = 'Ray Cast' + arm_section = 'ray' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'From') + self.add_input('ArmVectorSocket', 'To') + self.add_input('ArmIntSocket', 'Mask', default_value=1) + + self.add_output('ArmNodeSocketObject', 'RB') + self.add_output('ArmVectorSocket', 'Hit') + self.add_output('ArmVectorSocket', 'Normal') diff --git a/blender/arm/logicnode/physics/LN_ray_cast_on.py b/blender/arm/logicnode/physics/LN_ray_cast_on.py new file mode 100644 index 0000000000..4f62c9aa84 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_ray_cast_on.py @@ -0,0 +1,36 @@ +from arm.logicnode.arm_nodes import * + +class RayCastOnNode(ArmLogicTreeNode): + """Casts a physics ray and returns the first object that is hit by + this ray. + + @seeNode Mask + + @input In: Input trigger + @input From: the location from where to start the ray, in world + coordinates + @input To: the target location of the ray, in world coordinates + @input Mask: a bit mask value to specify which + objects are considered + + @output Out: Output after hit + @output RB: the object that was hit + @output Hit: the hit position in world coordinates + @output Normal: the surface normal of the hit position relative to + the world + """ + bl_idname = 'LNCastPhysicsRayOnNode' + bl_label = 'Ray Cast On' + arm_section = 'ray' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmVectorSocket', 'From') + self.add_input('ArmVectorSocket', 'To') + self.add_input('ArmIntSocket', 'Mask', default_value=1) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'RB') + self.add_output('ArmVectorSocket', 'Hit') + self.add_output('ArmVectorSocket', 'Normal') diff --git a/blender/arm/logicnode/physics/LN_remove_rb.py b/blender/arm/logicnode/physics/LN_remove_rb.py new file mode 100644 index 0000000000..7c05c68e03 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_remove_rb.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class RemovePhysicsNode (ArmLogicTreeNode): + """Removes the rigid body from the given object.""" + bl_idname = 'LNRemovePhysicsNode' + bl_label = 'Remove RB' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('ArmNodeSocketObject', 'RB') + + self.outputs.new('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_set_rb_activation_state.py b/blender/arm/logicnode/physics/LN_set_rb_activation_state.py new file mode 100644 index 0000000000..63eaec4cdf --- /dev/null +++ b/blender/arm/logicnode/physics/LN_set_rb_activation_state.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class SetActivationStateNode(ArmLogicTreeNode): + """Sets the rigid body simulation state of the given object.""" + bl_idname = 'LNSetActivationStateNode' + bl_label = 'Set RB Activation State' + bl_icon = 'NONE' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('inactive', 'Inactive', 'The rigid body simulation is deactivated'), + ('active', 'Active', 'The rigid body simulation is activated'), + ('always active', 'Always Active', 'The rigid body simulation is never deactivated'), + ('always inactive', 'Always Inactive', 'The rigid body simulation is never activated'), + ], + name='', default='inactive') + + def arm_init(self, context): + self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('ArmNodeSocketObject', 'RB') + + self.outputs.new('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/physics/LN_set_rb_friction.py b/blender/arm/logicnode/physics/LN_set_rb_friction.py new file mode 100644 index 0000000000..7efdcd792f --- /dev/null +++ b/blender/arm/logicnode/physics/LN_set_rb_friction.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetFrictionNode (ArmLogicTreeNode): + """Sets the friction of the given rigid body.""" + bl_idname = 'LNSetFrictionNode' + bl_label = 'Set RB Friction' + bl_icon = 'NONE' + arm_version = 1 + + def arm_init(self, context): + self.inputs.new('ArmNodeSocketAction', 'In') + self.inputs.new('ArmNodeSocketObject', 'RB') + self.inputs.new('ArmFloatSocket', 'Friction') + + self.outputs.new('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_set_rb_gravity_enabled.py b/blender/arm/logicnode/physics/LN_set_rb_gravity_enabled.py new file mode 100644 index 0000000000..27a576a117 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_set_rb_gravity_enabled.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetGravityEnabledNode(ArmLogicTreeNode): + """Sets whether the gravity is enabled for the given rigid body.""" + bl_idname = 'LNSetGravityEnabledNode' + bl_label = 'Set RB Gravity Enabled' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmBoolSocket', 'Enabled') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_set_rb_velocity.py b/blender/arm/logicnode/physics/LN_set_rb_velocity.py new file mode 100644 index 0000000000..21980ae809 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_set_rb_velocity.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class SetVelocityNode(ArmLogicTreeNode): + """Sets the velocity of the given rigid body.""" + bl_idname = 'LNSetVelocityNode' + bl_label = 'Set RB Velocity' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'RB') + self.add_input('ArmVectorSocket', 'Linear') + self.add_input('ArmVectorSocket', 'Linear Factor', default_value=[1.0, 1.0, 1.0]) + self.add_input('ArmVectorSocket', 'Angular') + self.add_input('ArmVectorSocket', 'Angular Factor', default_value=[1.0, 1.0, 1.0]) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_set_world_gravity.py b/blender/arm/logicnode/physics/LN_set_world_gravity.py new file mode 100644 index 0000000000..f8923b29c3 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_set_world_gravity.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class SetGravityNode(ArmLogicTreeNode): + """Sets the world gravity. + + @seeNode Get World Gravity + """ + bl_idname = 'LNSetGravityNode' + bl_label = 'Set World Gravity' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmVectorSocket', 'Gravity') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/physics/LN_volume_trigger.py b/blender/arm/logicnode/physics/LN_volume_trigger.py new file mode 100644 index 0000000000..af438c32a2 --- /dev/null +++ b/blender/arm/logicnode/physics/LN_volume_trigger.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + +class VolumeTriggerNode(ArmLogicTreeNode): + """Returns `true` if the given rigid body enters, overlaps or leaves the + given volume trigger. + + @input RB: this object is taken as the entering object + @input Trigger: this object is used as the volume trigger + """ + bl_idname = 'LNVolumeTriggerNode' + bl_label = 'Volume Trigger' + arm_section = 'misc' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('begin', 'Begin', 'The contact between the rigid bodies begins'), + ('overlap', 'Overlap', 'The contact between the rigid bodies is happening'), + ('end', 'End', 'The contact between the rigid bodies ends')], + name='', default='begin') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object 1') + self.add_input('ArmNodeSocketObject', 'Object 2') + + self.add_output('ArmBoolSocket', 'Bool') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/physics/__init__.py b/blender/arm/logicnode/physics/__init__.py new file mode 100644 index 0000000000..bc24ae8538 --- /dev/null +++ b/blender/arm/logicnode/physics/__init__.py @@ -0,0 +1,6 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Physics') +add_node_section(name='force', category='Physics') +add_node_section(name='contact', category='Physics') +add_node_section(name='ray', category='Physics') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_get_global_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_get_global_node.py new file mode 100644 index 0000000000..f5f61d7319 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_get_global_node.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class ColorgradingGetGlobalNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingGetGlobalNode' + bl_label = 'Colorgrading Get Global' + arm_section = 'colorgrading' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Whitebalance') + self.add_output('ArmVectorSocket', 'Tint') + self.add_output('ArmVectorSocket', 'Saturation') + self.add_output('ArmVectorSocket', 'Contrast') + self.add_output('ArmVectorSocket', 'Gamma') + self.add_output('ArmVectorSocket', 'Gain') + self.add_output('ArmVectorSocket', 'Offset') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_get_highlight_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_get_highlight_node.py new file mode 100644 index 0000000000..c4f16f9f09 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_get_highlight_node.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class ColorgradingGetHighlightNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingGetHighlightNode' + bl_label = 'Colorgrading Get Highlight' + arm_section = 'colorgrading' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'HightlightMin') + self.add_output('ArmVectorSocket', 'Saturation') + self.add_output('ArmVectorSocket', 'Contrast') + self.add_output('ArmVectorSocket', 'Gamma') + self.add_output('ArmVectorSocket', 'Gain') + self.add_output('ArmVectorSocket', 'Offset') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_get_midtone_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_get_midtone_node.py new file mode 100644 index 0000000000..29bab2c214 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_get_midtone_node.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class ColorgradingGetMidtoneNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingGetMidtoneNode' + bl_label = 'Colorgrading Get Midtone' + arm_section = 'colorgrading' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmVectorSocket', 'Saturation') + self.add_output('ArmVectorSocket', 'Contrast') + self.add_output('ArmVectorSocket', 'Gamma') + self.add_output('ArmVectorSocket', 'Gain') + self.add_output('ArmVectorSocket', 'Offset') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_get_shadow_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_get_shadow_node.py new file mode 100644 index 0000000000..06130071a3 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_get_shadow_node.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class ColorgradingGetShadowNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingGetShadowNode' + bl_label = 'Colorgrading Get Shadow' + arm_section = 'colorgrading' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'ShadowMax') + self.add_output('ArmVectorSocket', 'Saturation') + self.add_output('ArmVectorSocket', 'Contrast') + self.add_output('ArmVectorSocket', 'Gamma') + self.add_output('ArmVectorSocket', 'Gain') + self.add_output('ArmVectorSocket', 'Offset') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_set_global_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_set_global_node.py new file mode 100644 index 0000000000..a72ac52ac4 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_set_global_node.py @@ -0,0 +1,75 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +def update_node(self, context): + #Clean all nodes + + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + + if (self.property0 == 'Uniform'): + self.draw_nodes_uniform(context) + elif (self.property0 == 'RGB'): + self.draw_nodes_rgb(context) + else: + self.draw_nodes_colorwheel(context) + +def set_data(self, context): + + abspath = bpy.path.abspath(self.filepath) + abspath = abspath.replace("\\","\\\\") + with open(abspath, 'r') as myfile: + data = myfile.read().replace('\n', '').replace('"','') + self.property1 = data + +class ColorgradingSetGlobalNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingSetGlobalNode' + bl_label = 'Colorgrading Set Global' + arm_section = 'colorgrading' + arm_version = 1 + + # TODO: RRESET FILE OPTION FOR THE BELOW + property0 : HaxeEnumProperty( + 'property0', + items = [('RGB', 'RGB', 'RGB'), + ('Uniform', 'Uniform', 'Uniform')], + name='Mode', default='Uniform', update=update_node) + property1 : HaxeStringProperty('property1', name="Loaded Data", description="Loaded data - Just ignore", default="") + filepath : StringProperty(name="Preset File", description="Postprocess colorgrading preset file", default="", subtype="FILE_PATH", update=set_data) + + + def draw_nodes_uniform(self, context): + self.add_input('ArmFloatSocket', 'Whitebalance', default_value=6500.0) + self.add_input('ArmColorSocket', 'Tint', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('ArmFloatSocket', 'Saturation', default_value=1) + self.add_input('ArmFloatSocket', 'Contrast', default_value=1) + self.add_input('ArmFloatSocket', 'Gamma', default_value=1) + self.add_input('ArmFloatSocket', 'Gain', default_value=1) + self.add_input('ArmFloatSocket', 'Offset', default_value=1) + + def draw_nodes_rgb(self, context): + self.add_input('ArmFloatSocket', 'Whitebalance', default_value=6500.0) + self.add_input('ArmVectorSocket', 'Tint', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Saturation', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Contrast', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gamma', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gain', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Offset', default_value=[1,1,1]) + + def draw_nodes_colorwheel(self, context): + pass + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.draw_nodes_uniform(context) + + def draw_buttons(self, context, layout): + layout.label(text="Select value mode") + layout.prop(self, 'property0') + if (self.property0 == 'Preset File'): + layout.prop(self, 'filepath') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_set_highlight_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_set_highlight_node.py new file mode 100644 index 0000000000..01b23cd112 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_set_highlight_node.py @@ -0,0 +1,73 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +def update_node(self, context): + #Clean all nodes + + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + + if (self.property0 == 'Uniform'): + self.draw_nodes_uniform(context) + elif (self.property0 == 'RGB'): + self.draw_nodes_rgb(context) + else: + self.draw_nodes_colorwheel(context) + +def set_data(self, context): + + abspath = bpy.path.abspath(self.filepath) + abspath = abspath.replace("\\","\\\\") + with open(abspath, 'r') as myfile: + data = myfile.read().replace('\n', '').replace('"','') + self.property1 = data + +class ColorgradingSetHighlightNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingSetHighlightNode' + bl_label = 'Colorgrading Set Highlight' + arm_section = 'colorgrading' + arm_version = 1 + + # TODO: RRESET FILE OPTION FOR THE BELOW + property0: HaxeEnumProperty( + 'property0', + items = [('RGB', 'RGB', 'RGB'), + ('Uniform', 'Uniform', 'Uniform')], + name='Mode', default='Uniform', update=update_node) + property1 : HaxeStringProperty('property1', name="Loaded Data", description="Loaded data - Just ignore", default="") + filepath : StringProperty(name="Preset File", description="Postprocess colorgrading preset file", default="", subtype="FILE_PATH", update=set_data) + + + def draw_nodes_uniform(self, context): + self.add_input('ArmFloatSocket', 'HighlightMin', default_value=0) + self.add_input('ArmFloatSocket', 'Saturation', default_value=1) + self.add_input('ArmFloatSocket', 'Contrast', default_value=1) + self.add_input('ArmFloatSocket', 'Gamma', default_value=1) + self.add_input('ArmFloatSocket', 'Gain', default_value=1) + self.add_input('ArmFloatSocket', 'Offset', default_value=1) + + def draw_nodes_rgb(self, context): + self.add_input('ArmFloatSocket', 'HighlightMin', default_value=0) + self.add_input('ArmVectorSocket', 'Saturation', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Contrast', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gamma', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gain', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Offset', default_value=[1,1,1]) + + def draw_nodes_colorwheel(self, context): + pass + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.draw_nodes_uniform(context) + + def draw_buttons(self, context, layout): + layout.label(text="Select value mode") + layout.prop(self, 'property0') + if (self.property0 == 'Preset File'): + layout.prop(self, 'filepath') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_set_midtone_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_set_midtone_node.py new file mode 100644 index 0000000000..8a5bd19c27 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_set_midtone_node.py @@ -0,0 +1,72 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +def update_node(self, context): + #Clean all nodes + + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + + if (self.property0 == 'Uniform'): + self.draw_nodes_uniform(context) + elif (self.property0 == 'RGB'): + self.draw_nodes_rgb(context) + else: + self.draw_nodes_colorwheel(context) + +def set_data(self, context): + + abspath = bpy.path.abspath(self.filepath) + abspath = abspath.replace("\\","\\\\") + with open(abspath, 'r') as myfile: + data = myfile.read().replace('\n', '').replace('"','') + self.property1 = data + +class ColorgradingSetMidtoneNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingSetMidtoneNode' + bl_label = 'Colorgrading Set Midtone' + arm_section = 'colorgrading' + arm_version = 1 + + # TODO: RRESET FILE OPTION FOR THE BELOW + property0: HaxeEnumProperty( + 'property0', + items = [('RGB', 'RGB', 'RGB'), + ('Uniform', 'Uniform', 'Uniform')], + name='Mode', default='Uniform', update=update_node) + property1 : HaxeStringProperty('property1', name="Loaded Data", description="Loaded data - Just ignore", default="") + filepath : StringProperty(name="Preset File", description="Postprocess colorgrading preset file", default="", subtype="FILE_PATH", update=set_data) + + + def draw_nodes_uniform(self, context): + self.add_input('ArmFloatSocket', 'Saturation', default_value=1) + self.add_input('ArmFloatSocket', 'Contrast', default_value=1) + self.add_input('ArmFloatSocket', 'Gamma', default_value=1) + self.add_input('ArmFloatSocket', 'Gain', default_value=1) + self.add_input('ArmFloatSocket', 'Offset', default_value=1) + + def draw_nodes_rgb(self, context): + self.add_input('ArmVectorSocket', 'Tint', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Saturation', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Contrast', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gamma', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gain', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Offset', default_value=[1,1,1]) + + def draw_nodes_colorwheel(self, context): + pass + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.draw_nodes_uniform(context) + + def draw_buttons(self, context, layout): + layout.label(text="Select value mode") + layout.prop(self, 'property0') + if (self.property0 == 'Preset File'): + layout.prop(self, 'filepath') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/postprocess/LN_colorgrading_set_shadow_node.py b/blender/arm/logicnode/postprocess/LN_colorgrading_set_shadow_node.py new file mode 100644 index 0000000000..bc05651d22 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_colorgrading_set_shadow_node.py @@ -0,0 +1,73 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +def update_node(self, context): + #Clean all nodes + + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + + if (self.property0 == 'Uniform'): + self.draw_nodes_uniform(context) + elif (self.property0 == 'RGB'): + self.draw_nodes_rgb(context) + else: + self.draw_nodes_colorwheel(context) + +def set_data(self, context): + + abspath = bpy.path.abspath(self.filepath) + abspath = abspath.replace("\\","\\\\") + with open(abspath, 'r') as myfile: + data = myfile.read().replace('\n', '').replace('"','') + self.property1 = data + +class ColorgradingSetShadowNode(ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNColorgradingSetShadowNode' + bl_label = 'Colorgrading Set Shadow' + arm_section = 'colorgrading' + arm_version = 1 + + # TODO: RRESET FILE OPTION FOR THE BELOW + property0: HaxeEnumProperty( + 'property0', + items = [('RGB', 'RGB', 'RGB'), + ('Uniform', 'Uniform', 'Uniform')], + name='Mode', default='Uniform', update=update_node) + property1 : HaxeStringProperty('property1', name="Loaded Data", description="Loaded data - Just ignore", default="") + filepath : StringProperty(name="Preset File", description="Postprocess colorgrading preset file", default="", subtype="FILE_PATH", update=set_data) + + + def draw_nodes_uniform(self, context): + self.add_input('ArmFloatSocket', 'ShadowMax', default_value=1) + self.add_input('ArmFloatSocket', 'Saturation', default_value=1) + self.add_input('ArmFloatSocket', 'Contrast', default_value=1) + self.add_input('ArmFloatSocket', 'Gamma', default_value=1) + self.add_input('ArmFloatSocket', 'Gain', default_value=1) + self.add_input('ArmFloatSocket', 'Offset', default_value=1) + + def draw_nodes_rgb(self, context): + self.add_input('ArmFloatSocket', 'ShadowMax', default_value=1) + self.add_input('ArmVectorSocket', 'Saturation', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Contrast', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gamma', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Gain', default_value=[1,1,1]) + self.add_input('ArmVectorSocket', 'Offset', default_value=[1,1,1]) + + def draw_nodes_colorwheel(self, context): + pass + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.draw_nodes_uniform(context) + + def draw_buttons(self, context, layout): + layout.label(text="Select value mode") + layout.prop(self, 'property0') + if (self.property0 == 'Preset File'): + layout.prop(self, 'filepath') + layout.prop(self, 'property1') diff --git a/blender/arm/logicnode/postprocess/LN_get_bloom_settings.py b/blender/arm/logicnode/postprocess/LN_get_bloom_settings.py new file mode 100644 index 0000000000..bf1e30c41a --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_bloom_settings.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + + +class BloomGetNode(ArmLogicTreeNode): + """Return the current bloom post-processing settings. This node + requires `Armory Render Path > Renderer > Realtime postprocess` + to be enabled in order to work. + """ + bl_idname = 'LNBloomGetNode' + bl_label = 'Get Bloom Settings' + arm_version = 2 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Threshold') + self.add_output('ArmFloatSocket', 'Knee') + self.add_output('ArmFloatSocket', 'Strength') + self.add_output('ArmFloatSocket', 'Radius') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + return NodeReplacement( + 'LNBloomGetNode', 1, + 'LNBloomGetNode', 2, + {}, {0: 0, 1: 2, 2: 3} + ) diff --git a/blender/arm/logicnode/postprocess/LN_get_ca_settings.py b/blender/arm/logicnode/postprocess/LN_get_ca_settings.py new file mode 100644 index 0000000000..f4064fb47e --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_ca_settings.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class ChromaticAberrationGetNode(ArmLogicTreeNode): + """Returns the chromatic aberration post-processing settings.""" + bl_idname = 'LNChromaticAberrationGetNode' + bl_label = 'Get CA Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Strength') + self.add_output('ArmFloatSocket', 'Samples') diff --git a/blender/arm/logicnode/postprocess/LN_get_camera_post_process.py b/blender/arm/logicnode/postprocess/LN_get_camera_post_process.py new file mode 100644 index 0000000000..c04cb8b6ff --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_camera_post_process.py @@ -0,0 +1,29 @@ +from arm.logicnode.arm_nodes import * + +class CameraGetNode(ArmLogicTreeNode): + """Returns the post-processing effects of a camera.""" + bl_idname = 'LNCameraGetNode' + bl_label = 'Get Camera Post Process' + arm_version = 4 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'F-Stop')#0 + self.add_output('ArmFloatSocket', 'Shutter Time')#1 + self.add_output('ArmFloatSocket', 'ISO')#2 + self.add_output('ArmFloatSocket', 'Exposure Compensation')#3 + self.add_output('ArmFloatSocket', 'Fisheye Distortion')#4 + self.add_output('ArmBoolSocket', 'Auto Focus')#5 + self.add_output('ArmFloatSocket', 'DOF Distance')#6 + self.add_output('ArmFloatSocket', 'DOF Length')#7 + self.add_output('ArmFloatSocket', 'DOF F-Stop')#8 + self.add_output('ArmBoolSocket', 'Tonemapping')#9 + self.add_output('ArmFloatSocket', 'Distort')#10 + self.add_output('ArmFloatSocket', 'Film Grain')#11 + self.add_output('ArmFloatSocket', 'Sharpen')#12 + self.add_output('ArmFloatSocket', 'Vignette')#13 + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 3): + raise LookupError() + + return NodeReplacement.Identity(self) diff --git a/blender/arm/logicnode/postprocess/LN_get_lenstexture_settings.py b/blender/arm/logicnode/postprocess/LN_get_lenstexture_settings.py new file mode 100644 index 0000000000..8ffa9a3d13 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_lenstexture_settings.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class LenstextureGetNode(ArmLogicTreeNode): + """Returns the lens texture settings.""" + bl_idname = 'LNLenstextureGetNode' + bl_label = 'Get Lenstexture Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Center Min Clip') + self.add_output('ArmFloatSocket', 'Center Max Clip') + self.add_output('ArmFloatSocket', 'Luminance Min') + self.add_output('ArmFloatSocket', 'Luminance Max') + self.add_output('ArmFloatSocket', 'Brightness Exponent') diff --git a/blender/arm/logicnode/postprocess/LN_get_letterbox_settings.py b/blender/arm/logicnode/postprocess/LN_get_letterbox_settings.py new file mode 100644 index 0000000000..7d561b0e97 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_letterbox_settings.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class LetterboxGetNode(ArmLogicTreeNode): + """Returns the letterbox post-processing settings.""" + bl_idname = 'LNLetterboxGetNode' + bl_label = 'Get Letterbox Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmColorSocket', 'Color') + self.add_output('ArmFloatSocket', 'Size') diff --git a/blender/arm/logicnode/postprocess/LN_get_ssao_settings.py b/blender/arm/logicnode/postprocess/LN_get_ssao_settings.py new file mode 100644 index 0000000000..d263ae6045 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_ssao_settings.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class SSAOGetNode(ArmLogicTreeNode): + """Returns the SSAO post-processing settings.""" + bl_idname = 'LNSSAOGetNode' + bl_label = 'Get SSAO Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Radius') + self.add_output('ArmFloatSocket', 'Strength') + self.add_output('ArmFloatSocket', 'Max Steps') diff --git a/blender/arm/logicnode/postprocess/LN_get_ssr_settings.py b/blender/arm/logicnode/postprocess/LN_get_ssr_settings.py new file mode 100644 index 0000000000..684685c9e5 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_get_ssr_settings.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SSRGetNode(ArmLogicTreeNode): + """Returns the SSR post-processing settings.""" + bl_idname = 'LNSSRGetNode' + bl_label = 'Get SSR Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'SSR Step') + self.add_output('ArmFloatSocket', 'SSR Step Min') + self.add_output('ArmFloatSocket', 'SSR Search') + self.add_output('ArmFloatSocket', 'SSR Falloff') + self.add_output('ArmFloatSocket', 'SSR Jitter') diff --git a/blender/arm/logicnode/postprocess/LN_lenstexture_set.py b/blender/arm/logicnode/postprocess/LN_lenstexture_set.py new file mode 100644 index 0000000000..53698ccdb4 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_lenstexture_set.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class LenstextureSetNode(ArmLogicTreeNode): + """Set the lens texture settings.""" + bl_idname = 'LNLenstextureSetNode' + bl_label = 'Set Lenstexture' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Center Min Clip', default_value=0.1) + self.add_input('ArmFloatSocket', 'Center Max Clip', default_value=0.5) + self.add_input('ArmFloatSocket', 'Luminance Min', default_value=0.10) + self.add_input('ArmFloatSocket', 'Luminance Max', default_value=2.50) + self.add_input('ArmFloatSocket', 'Brightness Exponent', default_value=2.0) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/postprocess/LN_set_bloom_settings.py b/blender/arm/logicnode/postprocess/LN_set_bloom_settings.py new file mode 100644 index 0000000000..5ba5f133f7 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_bloom_settings.py @@ -0,0 +1,34 @@ +from arm.logicnode.arm_nodes import * +import arm.node_utils + + +class BloomSetNode(ArmLogicTreeNode): + """Set the bloom post-processing settings. This node + requires `Armory Render Path > Renderer > Realtime postprocess` + to be enabled in order to work. + """ + bl_idname = 'LNBloomSetNode' + bl_label = 'Set Bloom Settings' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Threshold', default_value=0.8) + self.add_input('ArmFloatSocket', 'Knee', default_value=0.5) + self.add_input('ArmFloatSocket', 'Strength', default_value=0.05) + self.add_input('ArmFloatSocket', 'Radius', default_value=6.5) + + self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + return NodeReplacement( + 'LNBloomSetNode', 1, + 'LNBloomSetNode', 2, + {0: 0, 1: 1, 2: 3, 3: 4}, {0: 0}, + None, + { + 1: arm.node_utils.get_socket_default(self.inputs[1]), + 3: arm.node_utils.get_socket_default(self.inputs[2]), + 4: arm.node_utils.get_socket_default(self.inputs[3]), + } + ) diff --git a/blender/arm/logicnode/postprocess/LN_set_ca_settings.py b/blender/arm/logicnode/postprocess/LN_set_ca_settings.py new file mode 100644 index 0000000000..5031cd0a72 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_ca_settings.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class ChromaticAberrationSetNode(ArmLogicTreeNode): + """Set the chromatic aberration post-processing settings.""" + bl_idname = 'LNChromaticAberrationSetNode' + bl_label = 'Set CA Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Strength', default_value=2.0) + self.add_input('ArmIntSocket', 'Samples', default_value=32) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/postprocess/LN_set_camera_post_process.py b/blender/arm/logicnode/postprocess/LN_set_camera_post_process.py new file mode 100644 index 0000000000..d365eb5f63 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_camera_post_process.py @@ -0,0 +1,39 @@ +from arm.logicnode.arm_nodes import * + +class CameraSetNode(ArmLogicTreeNode): + """Set the post-processing effects of a camera.""" + bl_idname = 'LNCameraSetNode' + bl_label = 'Set Camera Post Process' + arm_version = 4 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'F-stop', default_value=1.0)#0 + self.add_input('ArmFloatSocket', 'Shutter Time', default_value=2.8333)#1 + self.add_input('ArmFloatSocket', 'ISO', default_value=100.0)#2 + self.add_input('ArmFloatSocket', 'Exposure Compensation', default_value=0.0)#3 + self.add_input('ArmFloatSocket', 'Fisheye Distortion', default_value=0.01)#4 + self.add_input('ArmBoolSocket', 'Auto Focus', default_value=True)#5 + self.add_input('ArmFloatSocket', 'DoF Distance', default_value=10.0)#6 + self.add_input('ArmFloatSocket', 'DoF Length', default_value=160.0)#7 + self.add_input('ArmFloatSocket', 'DoF F-Stop', default_value=128.0)#8 + self.add_input('ArmBoolSocket', 'Tonemapping', default_value=False)#9 + self.add_input('ArmFloatSocket', 'Distort', default_value=2.0)#10 + self.add_input('ArmFloatSocket', 'Film Grain', default_value=2.0)#11 + self.add_input('ArmFloatSocket', 'Sharpen', default_value=0.25)#12 + self.add_input('ArmFloatSocket', 'Vignette', default_value=0.7)#13 + + self.add_output('ArmNodeSocketAction', 'Out') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in range(0, 4): + raise LookupError() + elif self.arm_version == 1: + newnode = node_tree.nodes.new('LNCameraSetNode') + + for link in self.inputs[10].links: + node_tree.links.new(newnode.inputs[10], link.to_socket) + + return newnode + else: + return NodeReplacement.Identity(self) \ No newline at end of file diff --git a/blender/arm/logicnode/postprocess/LN_set_letterbox_settings.py b/blender/arm/logicnode/postprocess/LN_set_letterbox_settings.py new file mode 100644 index 0000000000..4e71e9557e --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_letterbox_settings.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + +class LetterboxSetNode(ArmLogicTreeNode): + """Set the letterbox post-processing settings.""" + bl_idname = 'LNLetterboxSetNode' + bl_label = 'Set Letterbox Settings' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmColorSocket', 'Color', default_value=[0.0, 0.0, 0.0, 1.0]) + self.add_input('ArmFloatSocket', 'Size', default_value=0.1) + + 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) diff --git a/blender/arm/logicnode/postprocess/LN_set_ssao_settings.py b/blender/arm/logicnode/postprocess/LN_set_ssao_settings.py new file mode 100644 index 0000000000..e401b95559 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_ssao_settings.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SSAOSetNode(ArmLogicTreeNode): + """Set the SSAO post-processing settings.""" + bl_idname = 'LNSSAOSetNode' + bl_label = 'Set SSAO Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Radius', default_value=1.0) + self.add_input('ArmFloatSocket', 'Strength', default_value=5.0) + self.add_input('ArmIntSocket', 'Max Steps', default_value=8) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/postprocess/LN_set_ssr_settings.py b/blender/arm/logicnode/postprocess/LN_set_ssr_settings.py new file mode 100644 index 0000000000..9e3da739c2 --- /dev/null +++ b/blender/arm/logicnode/postprocess/LN_set_ssr_settings.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class SSRSetNode(ArmLogicTreeNode): + """Set the SSR post-processing settings.""" + bl_idname = 'LNSSRSetNode' + bl_label = 'Set SSR Settings' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'SSR Step', default_value=0.04) + self.add_input('ArmFloatSocket', 'SSR Step Min', default_value=0.05) + self.add_input('ArmFloatSocket', 'SSR Search', default_value=5.0) + self.add_input('ArmFloatSocket', 'SSR Falloff', default_value=5.0) + self.add_input('ArmFloatSocket', 'SSR Jitter', default_value=0.6) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/postprocess/__init__.py b/blender/arm/logicnode/postprocess/__init__.py new file mode 100644 index 0000000000..1835830279 --- /dev/null +++ b/blender/arm/logicnode/postprocess/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Postprocess') +add_node_section(name='colorgrading', category='Postprocess') diff --git a/blender/arm/logicnode/random/LN_random_boolean.py b/blender/arm/logicnode/random/LN_random_boolean.py new file mode 100644 index 0000000000..1a2f46b3ca --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_boolean.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + + +class RandomBooleanNode(ArmLogicTreeNode): + """Generates a random boolean.""" + bl_idname = 'LNRandomBooleanNode' + bl_label = 'Random Boolean' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmBoolSocket', 'Bool') diff --git a/blender/arm/logicnode/random/LN_random_choice.py b/blender/arm/logicnode/random/LN_random_choice.py new file mode 100644 index 0000000000..7ba4749ac6 --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_choice.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + + +class RandomChoiceNode(ArmLogicTreeNode): + """Choose a random value from a given array.""" + bl_idname = 'LNRandomChoiceNode' + bl_label = 'Random Choice' + arm_version = 1 + + def arm_init(self, context): + + self.add_input('ArmNodeSocketArray', 'Array') + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/random/LN_random_color.py b/blender/arm/logicnode/random/LN_random_color.py new file mode 100644 index 0000000000..9d142b153f --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_color.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + + +class RandomColorNode(ArmLogicTreeNode): + """Generates a random color.""" + bl_idname = 'LNRandomColorNode' + bl_label = 'Random Color' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmColorSocket', 'Color') diff --git a/blender/arm/logicnode/random/LN_random_float.py b/blender/arm/logicnode/random/LN_random_float.py new file mode 100644 index 0000000000..7bb08dde76 --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_float.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + + +class RandomFloatNode(ArmLogicTreeNode): + """Generates a random float.""" + bl_idname = 'LNRandomFloatNode' + bl_label = 'Random Float' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Min') + self.add_input('ArmFloatSocket', 'Max', default_value=1.0) + # self.add_input('ArmIntSocket', 'Seed') + self.add_output('ArmFloatSocket', 'Float') diff --git a/blender/arm/logicnode/random/LN_random_integer.py b/blender/arm/logicnode/random/LN_random_integer.py new file mode 100644 index 0000000000..166a2f0a99 --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_integer.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + + +class RandomIntegerNode(ArmLogicTreeNode): + """Generates a random integer.""" + bl_idname = 'LNRandomIntegerNode' + bl_label = 'Random Integer' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Min') + self.add_input('ArmIntSocket', 'Max', default_value=2) + self.add_output('ArmIntSocket', 'Int') diff --git a/blender/arm/logicnode/random/LN_random_output.py b/blender/arm/logicnode/random/LN_random_output.py new file mode 100644 index 0000000000..72f249f375 --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_output.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + + +class RandomOutputNode(ArmLogicTreeNode): + """Activate a random output when the input is activated.""" + bl_idname = 'LNRandomOutputNode' + bl_label = 'Random Output' + arm_section = 'logic' + arm_version = 1 + + def __init__(self): + array_nodes[str(id(self))] = self + + def arm_init(self, context): + + self.add_input('ArmNodeSocketAction', 'In') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_add_output', text='New', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'ArmNodeSocketAction' + op2 = row.operator('arm.node_remove_output', text='', icon='X', emboss=True) + op2.node_index = str(id(self)) diff --git a/blender/arm/logicnode/random/LN_random_string.py b/blender/arm/logicnode/random/LN_random_string.py new file mode 100644 index 0000000000..652a4836d6 --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_string.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + + +class RandomStringNode(ArmLogicTreeNode): + """Generate a random string based on a provided characters list. + + @input Length: The length of the string to generate. If the length is + 0 or negative, an empty string is returned. + @input Characters: A string containing the characters from which the + random generator can choose. For each letter in the output, the + generator randomly samples a character in the input string, so + the more often a character occurs in the input, the higher is + its chance of appearance in each letter of the result. + For example, if you provide `aaab` as a character string, + approximately 75% percent of the characters in all generated + strings are `a`, the remaining 25% are `b`. + """ + bl_idname = 'LNRandomStringNode' + bl_label = 'Random String' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Length') + self.add_input('ArmStringSocket', 'Characters') + self.add_output('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/random/LN_random_vector.py b/blender/arm/logicnode/random/LN_random_vector.py new file mode 100644 index 0000000000..b49beabece --- /dev/null +++ b/blender/arm/logicnode/random/LN_random_vector.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + + +class RandomVectorNode(ArmLogicTreeNode): + """Generates a random vector.""" + bl_idname = 'LNRandomVectorNode' + bl_label = 'Random Vector' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Min', default_value=[-1.0, -1.0, -1.0]) + self.add_input('ArmVectorSocket', 'Max', default_value=[1.0, 1.0, 1.0]) + self.add_output('ArmVectorSocket', 'Vector') diff --git a/blender/arm/logicnode/random/__init__.py b/blender/arm/logicnode/random/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/renderpath/LN_create_render_target.py b/blender/arm/logicnode/renderpath/LN_create_render_target.py new file mode 100644 index 0000000000..26953c01b1 --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_create_render_target.py @@ -0,0 +1,40 @@ +from arm.logicnode.arm_nodes import * + +class CreateRenderTargetNode(ArmLogicTreeNode): + """Create a render target and set it as parameter to the specified object material. + This image can be then drawn to using `Draw To Material Image Node`. In most cases, the render target needs to be created just once. + + @seeNode Get Scene Root + + @seeNode Draw To Material Image Node + + @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 Width: Width of the render target image created. + + @input Height: Height of the render target image created. + """ + + bl_idname = 'LNCreateRenderTargetNode' + bl_label = 'Create Render Target' + arm_section = 'draw' + arm_version = 1 + + 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('ArmIntSocket', 'Width') + self.add_input('ArmIntSocket', 'Height') + + self.add_output('ArmNodeSocketAction', 'Out') \ No newline at end of file diff --git a/blender/arm/logicnode/renderpath/LN_pause_active_camera_render.py b/blender/arm/logicnode/renderpath/LN_pause_active_camera_render.py new file mode 100644 index 0000000000..bba9971251 --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_pause_active_camera_render.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + +class PauseActiveCameraRenderNode(ArmLogicTreeNode): + """Pause only the rendering of active camera. The logic behaviour remains active. + + @input In: Activate to set property. + @input Pause: Pause the rendering when enabled. + + @output Out: Activated after property is set. + """ + bl_idname = 'LNPauseActiveCameraRenderNode' + bl_label = 'Pause Active Camera Render' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Pause', default_value=False) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/renderpath/LN_rotate_render_target.py b/blender/arm/logicnode/renderpath/LN_rotate_render_target.py new file mode 100644 index 0000000000..05539d4baf --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_rotate_render_target.py @@ -0,0 +1,31 @@ +from arm.logicnode.arm_nodes import * + + +class RotateRenderTargetNode(ArmLogicTreeNode): + """Rotates the render target. + + @input In: Activate to rotate render target. The input must + be (indirectly) called from an `On Render2D` node. + @input Angle: Angle in radians to rotate. + @input Center X: X coordinate to rotate around. + @input Center Y: Y coordinate to rotate around. + @input Revert After: Revert rotation after all the draw calls + are activated from this node. + + @output Out: Activated after the render target is rotated. + + @see [`kha.graphics2.Graphics.rotate()`](http://kha.tech/api/kha/graphics2/Graphics.html#rotate). + """ + bl_idname = 'LNRotateRenderTargetNode' + bl_label = 'Rotate Render Target' + arm_section = 'draw' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Angle') + self.add_input('ArmFloatSocket', 'Center X') + self.add_input('ArmFloatSocket', 'Center Y') + self.add_input('ArmBoolSocket', 'Revert after', default_value=True) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/renderpath/LN_set_msaa_quality.py b/blender/arm/logicnode/renderpath/LN_set_msaa_quality.py new file mode 100644 index 0000000000..81511a03cf --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_set_msaa_quality.py @@ -0,0 +1,24 @@ +from arm.logicnode.arm_nodes import * + +class RpMSAANode(ArmLogicTreeNode): + """Sets the MSAA quality.""" + bl_idname = 'LNRpMSAANode' + bl_label = 'Set MSAA Quality' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('1', '1', '1'), + ('2', '2', '2'), + ('4', '4', '4'), + ('8', '8', '8'), + ('16', '16', '16') + ], + name='', default='1') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/renderpath/LN_set_post_process_quality.py b/blender/arm/logicnode/renderpath/LN_set_post_process_quality.py new file mode 100644 index 0000000000..faa883bb26 --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_set_post_process_quality.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + +class RpConfigNode(ArmLogicTreeNode): + """Sets the post process quality.""" + bl_idname = 'LNRpConfigNode' + bl_label = 'Set Post Process Quality' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('SSGI', 'SSGI', 'SSGI'), + ('SSR', 'SSR', 'SSR'), + ('Bloom', 'Bloom', 'Bloom'), + ('GI', 'GI', 'GI'), + ('Motion Blur', 'Motion Blur', 'Motion Blur') + ], + name='', default='SSGI') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmBoolSocket', 'Enable') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/renderpath/LN_set_shader_uniform.py b/blender/arm/logicnode/renderpath/LN_set_shader_uniform.py new file mode 100644 index 0000000000..8afbcd7591 --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_set_shader_uniform.py @@ -0,0 +1,46 @@ +from bpy.props import EnumProperty + +from arm.logicnode.arm_nodes import * + + +class SetShaderUniformNode(ArmLogicTreeNode): + """Set a global shader uniform value.""" + bl_idname = 'LNSetShaderUniformNode' + bl_label = 'Set Shader Uniform' + bl_width_default = 200 + arm_section = 'shaders' + arm_version = 1 + + def on_update_uniform_type(self, _): + self.inputs.remove(self.inputs[2]) + + if self.property0 == 'int': + self.add_input('ArmIntSocket', 'Int') + elif self.property0 == 'float': + self.add_input('ArmFloatSocket', 'Float') + elif self.property0 in ('vec2', 'vec3', 'vec4'): + self.add_input('ArmVectorSocket', 'Vector') + + property0: HaxeEnumProperty( + 'property0', + items = [('int', 'int', 'int'), + ('float', 'float', 'float'), + ('vec2', 'vec2', 'vec2'), + ('vec3', 'vec3', 'vec3'), + ('vec4', 'vec4', 'vec4')], + name='Uniform Type', + default='float', + description="The type of the uniform", + update=on_update_uniform_type) + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Uniform Name') + self.add_input('ArmFloatSocket', 'Float') + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + split = layout.split(factor=0.5, align=True) + + split.label(text="Type") + split.prop(self, "property0", text="") diff --git a/blender/arm/logicnode/renderpath/LN_set_shadows_quality.py b/blender/arm/logicnode/renderpath/LN_set_shadows_quality.py new file mode 100644 index 0000000000..be16096e9f --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_set_shadows_quality.py @@ -0,0 +1,22 @@ +from arm.logicnode.arm_nodes import * + +class RpShadowQualityNode(ArmLogicTreeNode): + """Sets the shadows quality.""" + bl_idname = 'LNRpShadowQualityNode' + bl_label = 'Set Shadows Quality' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('High', 'High', 'High'), + ('Medium', 'Medium', 'Medium'), + ('Low', 'Low', 'Low') + ], + name='', default='Medium') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/renderpath/LN_set_ssaa_quality.py b/blender/arm/logicnode/renderpath/LN_set_ssaa_quality.py new file mode 100644 index 0000000000..7608749a67 --- /dev/null +++ b/blender/arm/logicnode/renderpath/LN_set_ssaa_quality.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + +class RpSuperSampleNode(ArmLogicTreeNode): + """Sets the supersampling quality.""" + bl_idname = 'LNRpSuperSampleNode' + bl_label = 'Set SSAA Quality' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('1', '1', '1'), + ('1.5', '1.5', '1.5'), + ('2', '2', '2'), + ('4', '4', '4') + ], + name='', default='1') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/renderpath/__init__.py b/blender/arm/logicnode/renderpath/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/replacement.py b/blender/arm/logicnode/replacement.py new file mode 100644 index 0000000000..0cf1396af3 --- /dev/null +++ b/blender/arm/logicnode/replacement.py @@ -0,0 +1,346 @@ +""" +This module contains the functionality to replace nodes by other nodes +in order to keep files from older Armory versions compatible with newer versions. + +Nodes can define custom update procedures which describe how the replacement +should look like. + +Original author: @niacdoial +""" +import os.path +import time +import traceback +import typing +from typing import Dict, List, Optional, Tuple + +import bpy.props + +import arm.log as log +import arm.logicnode.arm_nodes as arm_nodes +import arm.logicnode.arm_sockets +import arm.node_utils as node_utils + +if arm.is_reload(__name__): + log = arm.reload_module(log) + arm_nodes = arm.reload_module(arm_nodes) + arm.logicnode.arm_sockets = arm.reload_module(arm.logicnode.arm_sockets) + node_utils = arm.reload_module(node_utils) +else: + arm.enable_reload(__name__) + +# List of errors that occurred during the replacement +# Format: (error identifier, node.bl_idname (or None), tree name, exception traceback (optional)) +replacement_errors: List[Tuple[str, Optional[str], str, Optional[str]]] = [] + + +class NodeReplacement: + """ + Represents a simple replacement rule, this can replace nodes of one type to nodes of a second type. + However, it is fairly limited. For instance, it assumes there are no changes in the type of the inputs or outputs + Second, it also assumes that node properties (especially EnumProperties) keep the same possible values. + + - from_node, from_node_version: the type of node to be removed, and its version number + - to_node, to_node_version: the type of node which takes from_node's place, and its version number + - *_socket_mapping: a map which defines how the sockets of the old node shall be connected to the new node + {1: 2} means that anything connected to the socket with index 1 on the original node will be connected to the socket with index 2 on the new node + - property_mapping: the mapping used to transfer the values of the old node's properties to the new node's properties. + {"property0": "property1"} mean that the value of the new node's property1 should be the old node's property0's value. + - input_defaults: a mapping used to give default values to the inputs which aren't overridden otherwise. + - property_defaults: a mapping used to define the value of the new node's properties, when they aren't overridden otherwise. + """ + + def __init__(self, from_node: str, from_node_version: int, to_node: str, to_node_version: int, + in_socket_mapping: Dict[int, int], out_socket_mapping: Dict[int, int], property_mapping: Optional[Dict[str, str]] = None, + input_defaults: Optional[Dict[int, any]] = None, property_defaults: Optional[Dict[str, any]] = None): + self.from_node = from_node + self.to_node = to_node + self.from_node_version = from_node_version + self.to_node_version = to_node_version + + self.in_socket_mapping = in_socket_mapping + self.out_socket_mapping = out_socket_mapping + self.property_mapping = {} if property_mapping is None else property_mapping + + self.input_defaults = {} if input_defaults is None else input_defaults + self.property_defaults = {} if property_defaults is None else property_defaults + + @classmethod + def Identity(cls, node: 'ArmLogicTreeNode'): + """Returns a NodeReplacement that does nothing, while operating on a given node. + WARNING: it assumes that all node properties have names that start with "property" + """ + in_socks = {i: i for i in range(len(node.inputs))} + out_socks = {i: i for i in range(len(node.outputs))} + + # Find all properties for this node + props = {} + possible_properties = [] + for attrname in dir(node): + # We assume that property names start with 'property' + if attrname.startswith('property'): + possible_properties.append(attrname) + + for attrname in possible_properties: + # Search in type annotations + if attrname not in node.__annotations__: + continue + + # Properties must be annotated with '_PropertyDeferred', see + # https://developer.blender.org/rB37e6a1995ac7eeabd5b6a56621ad5a850dae4149 + # and https://developer.blender.org/rBc44c611c6d8c6ae071b48efb5fc07168f18cd17e + if not isinstance(node.__annotations__[attrname], bpy.props._PropertyDeferred): + continue + + props[attrname] = attrname + + return NodeReplacement( + node.bl_idname, node.arm_version, node.bl_idname, type(node).arm_version, + in_socket_mapping=in_socks, out_socket_mapping=out_socks, + property_mapping=props + ) + + def chain_with(self, other): + """Modify the current NodeReplacement by "adding" a second replacement after it""" + if self.to_node != other.from_node or self.to_node_version != other.from_node_version: + raise TypeError('the given NodeReplacement-s could not be chained') + self.to_node = other.to_node + self.to_node_version = other.to_node_version + + for i1, i2 in self.in_socket_mapping.items(): + i3 = other.in_socket_mapping[i2] + self.in_socket_mapping[i1] = i3 + for i1, i2 in self.out_socket_mapping.items(): + i3 = other.out_socket_mapping[i2] + self.out_socket_mapping[i1] = i3 + for p1, p2 in self.property_mapping.items(): + p3 = other.property_mapping[p2] + self.property_mapping[p1] = p3 + + old_input_defaults = self.input_defaults + self.input_defaults = other.input_defaults.copy() + for i, x in old_input_defaults.items(): + self.input_defaults[ other.in_socket_mapping[i] ] = x + + old_property_defaults = self.property_defaults + self.property_defaults = other.property_defaults.copy() + for p, x in old_property_defaults.items(): + self.property_defaults[ other.property_mapping[p] ] = x + + @staticmethod + def replace_input_socket(tree: bpy.types.NodeTree, socket_src: bpy.types.NodeSocket, socket_dst: bpy.types.NodeSocket): + if socket_src.is_linked: + for link in socket_src.links: + tree.links.new(link.from_socket, socket_dst) + else: + node_utils.set_socket_default(socket_dst, node_utils.get_socket_default(socket_src)) + + @staticmethod + def replace_output_socket(tree: bpy.types.NodeTree, socket_src: bpy.types.NodeSocket, socket_dst: bpy.types.NodeSocket): + if socket_src.is_linked: + for link in socket_src.links: + tree.links.new(socket_dst, link.to_socket) + else: + node_utils.set_socket_default(socket_dst, node_utils.get_socket_default(socket_src)) + + +def replace(tree: bpy.types.NodeTree, node: 'ArmLogicTreeNode'): + """Replaces the given node with its replacement.""" + + # the node can either return a NodeReplacement object (for simple replacements) + # or a brand new node, for more complex stuff. + response = node.get_replacement_node(tree) + + if isinstance(response, arm_nodes.ArmLogicTreeNode): + newnode = response + # some misc. properties + node_utils.copy_basic_node_props(from_node=node, to_node=newnode) + + elif isinstance(response, list): # a list of nodes: + for newnode in response: + node_utils.copy_basic_node_props(from_node=node, to_node=newnode) + + elif isinstance(response, NodeReplacement): + replacement = response + # if the returned object is a NodeReplacement, check that it corresponds to the node (also, create the new node) + if node.bl_idname != replacement.from_node or node.arm_version != replacement.from_node_version: + raise LookupError("The provided NodeReplacement doesn't seem to correspond to the node needing replacement") + + # Create the replacement node + newnode = tree.nodes.new(response.to_node) + if newnode.arm_version != replacement.to_node_version: + tree.nodes.remove(newnode) + raise LookupError("The provided NodeReplacement doesn't seem to correspond to the node needing replacement") + + # some misc. properties + node_utils.copy_basic_node_props(from_node=node, to_node=newnode) + + # now, use the `replacement` to hook up the new node correctly + # start by applying defaults + for prop_name, prop_value in replacement.property_defaults.items(): + setattr(newnode, prop_name, prop_value) + for input_id, input_value in replacement.input_defaults.items(): + input_socket = newnode.inputs[input_id] + node_utils.set_socket_default(input_socket, input_value) + + # map properties + for src_prop_name, dest_prop_name in replacement.property_mapping.items(): + setattr(newnode, dest_prop_name, getattr(node, src_prop_name)) + + # map inputs + for src_socket_id, dest_socket_id in replacement.in_socket_mapping.items(): + src_socket = node.inputs[src_socket_id] + dest_socket = newnode.inputs[dest_socket_id] + NodeReplacement.replace_input_socket(tree, src_socket, dest_socket) + + # map outputs + for src_socket_id, dest_socket_id in replacement.out_socket_mapping.items(): + src_socket = node.outputs[src_socket_id] + dest_socket = newnode.outputs[dest_socket_id] + NodeReplacement.replace_output_socket(tree, src_socket, dest_socket) + + else: + print(response) + + tree.nodes.remove(node) + + +def replace_all(): + """Iterate through all logic node trees in the file and check for node updates/replacements to execute.""" + global replacement_errors + + replacement_errors.clear() + + for tree in bpy.data.node_groups: + if tree.bl_idname == "ArmLogicTreeType" or tree.bl_idname == 'ArmGroupTree': + # Use list() to make a "static" copy. It's possible to iterate over it because nodes which get removed + # from the tree leave python objects in the list + for node in list(tree.nodes): + # Blender nodes (layout) + if not isinstance(node, arm_nodes.ArmLogicTreeNode): + continue + + # That node has been removed from the tree without replace() being called on it somehow + elif node.type == '': + continue + + # Node type deleted. That's unusual. Or it has been replaced for a looong time + elif not node.is_registered_node_type(): + replacement_errors.append(('unregistered', None, tree.name, None)) + + # Invalid version number + elif not isinstance(type(node).arm_version, int): + replacement_errors.append(('bad version', node.bl_idname, tree.name, None)) + + # Actual replacement + elif node.arm_version < type(node).arm_version: + try: + replace(tree, node) + except LookupError as err: + replacement_errors.append(('update failed', node.bl_idname, tree.name, traceback.format_exc())) + except Exception as err: + replacement_errors.append(('misc.', node.bl_idname, tree.name, traceback.format_exc())) + + # Node version is newer than supported by the class + elif node.arm_version > type(node).arm_version: + replacement_errors.append(('future version', node.bl_idname, tree.name, None)) + + # If possible, make a popup about the errors and write an error report into the .blend file's folder + if len(replacement_errors) > 0: + basedir = os.path.dirname(bpy.data.filepath) + reportfile = os.path.join( + basedir, 'node_update_failure.{:s}.txt'.format( + time.strftime("%Y-%m-%dT%H-%M-%S%z") + ) + ) + + with open(reportfile, 'w') as reportf: + for error_type, node_class, tree_name, tb in replacement_errors: + if error_type == 'unregistered': + 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. 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) + elif error_type == 'bad version': + print(f"A node of type {node_class} in tree \"{tree_name}\" doesn't have version information attached to it. " + f"If so, please check that the nodes in the file are compatible with the in-code node classes. " + f"If this nodes comes from an add-on, please check that it is compatible with this version of armory.", file=reportf) + elif error_type == 'misc.': + print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, " + f"because the node's update procedure itself failed. Original exception:" + "\n" + tb + "\n", file=reportf) + else: + print(f"Whoops, we don't know what this error type (\"{error_type}\") means. You might want to report a bug here. " + f"All we know is that it comes form a node of class {node_class} in the node tree called \"{tree_name}\".", file=reportf) + + log.error(f'There were errors in the node update procedure, a detailed report has been written to {reportfile}') + + bpy.ops.arm.show_node_update_errors() + + +def node_compat_sdk2108(): + """SDK 21.08 broke compatibility with older nodes as nodes now use + custom sockets even for Blender's default data types and custom + property "constructors". This allows to listen for events for the + live patch system. + + In order to update older nodes this routine is used. It creates a + full copy of the nodes and replaces all properties and sockets with + their new equivalents. + """ + for tree in bpy.data.node_groups: + if tree.bl_idname == "ArmLogicTreeType" or tree.bl_idname == 'ArmGroupTree': + for node in list(tree.nodes): + # Don't raise exceptions for invalid unregistered nodes, this + # function didn't cause the registration problem if there is one + if not node.__class__.is_registered_node_type(): + continue + + if node.type in ('FRAME', 'REROUTE'): + continue + + newnode = tree.nodes.new(node.__class__.bl_idname) + node_utils.copy_basic_node_props(from_node=node, to_node=newnode) + + # Also copy the node's version number to _not_ prevent actual node + # replacement after this step + newnode.arm_version = node.arm_version + + # First replace all properties + for prop_name, prop in typing.get_type_hints(node.__class__, {}, {}).items(): + if isinstance(prop, bpy.props._PropertyDeferred): + if hasattr(node, prop_name) and hasattr(newnode, prop_name): + setattr(newnode, prop_name, getattr(node, prop_name)) + + # Replace sockets with new socket types + socket_replacements = { + 'NodeSocketBool': 'ArmBoolSocket', + 'NodeSocketColor': 'ArmColorSocket', + 'NodeSocketFloat': 'ArmFloatSocket', + 'NodeSocketInt': 'ArmIntSocket', + 'NodeSocketShader': 'ArmDynamicSocket', + 'NodeSocketString': 'ArmStringSocket', + 'NodeSocketVector': 'ArmVectorSocket' + } + + # Recreate all sockets + newnode.inputs.clear() + for inp in node.inputs: + inp_idname = inp.bl_idname + inp_idname = socket_replacements.get(inp_idname, inp_idname) + + newinp = newnode.inputs.new(inp_idname, inp.name, identifier=inp.identifier) + NodeReplacement.replace_input_socket(tree, inp, newinp) + + newnode.outputs.clear() + for out in node.outputs: + out_idname = out.bl_idname + out_idname = socket_replacements.get(out_idname, out_idname) + + newout = newnode.outputs.new(out_idname, out.name, identifier=out.identifier) + NodeReplacement.replace_output_socket(tree, out, newout) + + tree.nodes.remove(node) diff --git a/blender/arm/logicnode/scene/LN_add_object_to_collection.py b/blender/arm/logicnode/scene/LN_add_object_to_collection.py new file mode 100644 index 0000000000..1841927bab --- /dev/null +++ b/blender/arm/logicnode/scene/LN_add_object_to_collection.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class AddObjectToGroupNode(ArmLogicTreeNode): + """Add Object to a collection.""" + bl_idname = 'LNAddObjectToGroupNode' + bl_label = 'Add Object to Collection' + arm_section = 'collection' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Collection') + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/scene/LN_collection.py b/blender/arm/logicnode/scene/LN_collection.py new file mode 100644 index 0000000000..95d9c5ade7 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_collection.py @@ -0,0 +1,21 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class GroupNode(ArmLogicTreeNode): + """Returns the objects of the given collection as an array. + + @seeNode Get Collection""" + bl_idname = 'LNGroupNode' + bl_label = 'Collection' + arm_section = 'collection' + arm_version = 1 + + property0: HaxePointerProperty('property0', name='', type=bpy.types.Collection) + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array') + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property0', bpy.data, 'collections', icon='NONE', text='') diff --git a/blender/arm/logicnode/scene/LN_create_collection.py b/blender/arm/logicnode/scene/LN_create_collection.py new file mode 100644 index 0000000000..efdec628e2 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_create_collection.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class CreateCollectionNode(ArmLogicTreeNode): + """Creates a collection.""" + bl_idname = 'LNAddGroupNode' + bl_label = 'Create Collection' + arm_section = 'collection' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Collection') + self.add_input('ArmNodeSocketArray', 'Objects') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/scene/LN_get_collection.py b/blender/arm/logicnode/scene/LN_get_collection.py new file mode 100644 index 0000000000..74a0f05d0e --- /dev/null +++ b/blender/arm/logicnode/scene/LN_get_collection.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class GetGroupNode(ArmLogicTreeNode): + """Searches for a collection of objects with the given name and + outputs the collection's objects as an array, if found. + + @seeNode Collection""" + bl_idname = 'LNGetGroupNode' + bl_label = 'Get Collection' + arm_section = 'collection' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Name') + + self.add_output('ArmNodeSocketArray', 'Objects') diff --git a/blender/arm/logicnode/scene/LN_get_object_collection.py b/blender/arm/logicnode/scene/LN_get_object_collection.py new file mode 100644 index 0000000000..c0d7c9301c --- /dev/null +++ b/blender/arm/logicnode/scene/LN_get_object_collection.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetObjectGroupNode(ArmLogicTreeNode): + """Get Object collection.""" + bl_idname = 'LNGetObjectGroupNode' + bl_label = 'Get Object Collection' + arm_section = 'collection' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketArray', 'Collection') + self.add_output('ArmIntSocket', 'Length') diff --git a/blender/arm/logicnode/scene/LN_get_scene_active.py b/blender/arm/logicnode/scene/LN_get_scene_active.py new file mode 100644 index 0000000000..f99c72c013 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_get_scene_active.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class ActiveSceneNode(ArmLogicTreeNode): + """Returns the active scene.""" + bl_idname = 'LNActiveSceneNode' + bl_label = 'Get Scene Active' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Scene') diff --git a/blender/arm/logicnode/scene/LN_get_scene_root.py b/blender/arm/logicnode/scene/LN_get_scene_root.py new file mode 100644 index 0000000000..2dc4e4285d --- /dev/null +++ b/blender/arm/logicnode/scene/LN_get_scene_root.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class SceneRootNode(ArmLogicTreeNode): + """Returns the root object of the current scene.""" + bl_idname = 'LNSceneRootNode' + bl_label = 'Get Scene Root' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/scene/LN_global_object.py b/blender/arm/logicnode/scene/LN_global_object.py new file mode 100644 index 0000000000..ca001743f2 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_global_object.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + +class GlobalObjectNode(ArmLogicTreeNode): + """Gives access to a global object which can be used to share + information between different traits.""" + bl_idname = 'LNGlobalObjectNode' + bl_label = 'Global Object' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketObject', 'Object') diff --git a/blender/arm/logicnode/scene/LN_remove_collection.py b/blender/arm/logicnode/scene/LN_remove_collection.py new file mode 100644 index 0000000000..813c13d66f --- /dev/null +++ b/blender/arm/logicnode/scene/LN_remove_collection.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class RemoveGroupNode(ArmLogicTreeNode): + """Removes the given collection from the scene.""" + bl_idname = 'LNRemoveGroupNode' + bl_label = 'Remove Collection' + arm_section = 'collection' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Collection') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/scene/LN_remove_object_from_collection.py b/blender/arm/logicnode/scene/LN_remove_object_from_collection.py new file mode 100644 index 0000000000..b018c90c75 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_remove_object_from_collection.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class RemoveObjectFromGroupNode(ArmLogicTreeNode): + """Remove Object from a collection.""" + bl_idname = 'LNRemoveObjectFromGroupNode' + bl_label = 'Remove Object from Collection' + arm_section = 'collection' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmStringSocket', 'Collection') + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/scene/LN_remove_scene_active.py b/blender/arm/logicnode/scene/LN_remove_scene_active.py new file mode 100644 index 0000000000..4fd2ac66f1 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_remove_scene_active.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class RemoveActiveSceneNode(ArmLogicTreeNode): + """Removes the active scene.""" + bl_idname = 'LNRemoveActiveSceneNode' + bl_label = 'Remove Scene Active' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/scene/LN_set_scene_active.py b/blender/arm/logicnode/scene/LN_set_scene_active.py new file mode 100644 index 0000000000..95138c85af --- /dev/null +++ b/blender/arm/logicnode/scene/LN_set_scene_active.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetSceneNode(ArmLogicTreeNode): + """Sets the active scene.""" + bl_idname = 'LNSetSceneNode' + bl_label = 'Set Scene Active' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Scene') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'Root') diff --git a/blender/arm/logicnode/scene/LN_spawn_collection.py b/blender/arm/logicnode/scene/LN_spawn_collection.py new file mode 100644 index 0000000000..1ec176f4cb --- /dev/null +++ b/blender/arm/logicnode/scene/LN_spawn_collection.py @@ -0,0 +1,58 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class SpawnCollectionNode(ArmLogicTreeNode): + """ + Spawns a new instance of the selected `collection` from the given `scene`. + If the `scene` is empty or null, the current active scene is used. Each spawned + instance has an empty owner object to control the instance as a whole (like Blender + uses it for collection instances). + + @input Scene: Scene in which the collection belongs. + @input Collection: Collection to be spawned. + @input In: activates the node. + @input Transform: the transformation of the instance that should be + spawned. Please note that the collection's instance offset is + also taken into account. + + @output Out: activated when a collection instance was spawned. It is + not activated when no collection is selected. + @output Top-Level Objects: all objects in the last spawned + collection that are direct children of the owner object of the + collection's instance. + @output All Objects: all objects in the last spawned collection. + @output Owner Object: The owning object of the last spawned + collection's instance. + """ + bl_idname = 'LNSpawnCollectionNode' + bl_label = 'Spawn Collection' + arm_section = 'collection' + arm_version = 2 + + property0: HaxePointerProperty('property0', name='Collection', type=bpy.types.Collection) + + property1: HaxePointerProperty( + 'property1', + type=bpy.types.Scene, name='Scene', + description='The scene from which to take the object') + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketArray', 'Top-Level Objects') + self.add_output('ArmNodeSocketArray', 'All Objects') + self.add_output('ArmNodeSocketObject', 'Owner Object') + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property1', bpy.data, "scenes") + layout.prop_search(self, 'property0', bpy.data, 'collections', icon='NONE', text='') + + 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/scene/LN_spawn_scene.py b/blender/arm/logicnode/scene/LN_spawn_scene.py new file mode 100644 index 0000000000..e3df2adbf4 --- /dev/null +++ b/blender/arm/logicnode/scene/LN_spawn_scene.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SpawnSceneNode(ArmLogicTreeNode): + """Spawns the given scene.""" + bl_idname = 'LNSpawnSceneNode' + bl_label = 'Spawn Scene' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Scene') + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketObject', 'Root') diff --git a/blender/arm/logicnode/scene/__init__.py b/blender/arm/logicnode/scene/__init__.py new file mode 100644 index 0000000000..25c3eecb45 --- /dev/null +++ b/blender/arm/logicnode/scene/__init__.py @@ -0,0 +1,5 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Scene') +add_node_section(name='traits', category='Scene') +add_node_section(name='collection', category='Scene') diff --git a/blender/arm/logicnode/sound/LN_pause_speaker.py b/blender/arm/logicnode/sound/LN_pause_speaker.py new file mode 100644 index 0000000000..14fb5852f1 --- /dev/null +++ b/blender/arm/logicnode/sound/LN_pause_speaker.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class PauseSpeakerNode(ArmLogicTreeNode): + """Pauses playback of the given speaker object. The playback will be resumed + at the paused position. + + @seeNode Play Speaker + @seeNode Stop Speaker + """ + bl_idname = 'LNPauseSoundNode' + bl_label = 'Pause Speaker' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Speaker') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/sound/LN_play_sound.py b/blender/arm/logicnode/sound/LN_play_sound.py new file mode 100644 index 0000000000..90b51080ea --- /dev/null +++ b/blender/arm/logicnode/sound/LN_play_sound.py @@ -0,0 +1,97 @@ +import bpy + +from arm.logicnode.arm_nodes import * + + +class PlaySoundNode(ArmLogicTreeNode): + """Plays the given sound. + + @input Play: Plays the sound, or if paused, resumes the playback. + The exact behaviour depends on the Retrigger option (see below). + @input Pause: Pauses the playing sound. If no sound is playing, + nothing happens. + @input Stop: Stops the playing sound. If the playback is paused, + this will reset the playback position to the start of the sound. + @input Update Volume: Updates the volume of the current playback. + @input Volume: Volume of the playback. Typically ranges from 0 to 1. + + @output Out: activated once when Play is activated. + @output Running: activated while the playback is active. + @output Done: activated when the playback has finished or was + stopped manually. + + @option Sound: The sound that will be played. + @option Loop: Whether to loop the playback. + @option Retrigger: If true, the playback position will be reset to + the beginning on each activation of Play. If false, the playback + will continue at the current position. + @option Sample Rate: Manually override the sample rate of the sound + (this controls the pitch and the playback speed). + """ + bl_idname = 'LNPlaySoundRawNode' + bl_label = 'Play Sound' + bl_width_default = 200 + arm_version = 2 + + property0: HaxePointerProperty('property0', name='', type=bpy.types.Sound) + property1: HaxeBoolProperty( + 'property1', + name='Loop', + description='Play the sound in a loop', + default=False) + property2: HaxeBoolProperty( + 'property2', + name='Retrigger', + description='Play the sound from the beginning every time', + default=False) + property3: HaxeBoolProperty( + 'property3', + name='Use Custom Sample Rate', + description='If enabled, override the default sample rate', + default=False) + property4: HaxeIntProperty( + 'property4', + name='Sample Rate', + description='Set the sample rate used to play this sound', + default=44100, + min=0) + property5: HaxeBoolProperty( + 'property5', + name='Stream', + description='Stream the sound from disk', + default=False + ) + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Play') + self.add_input('ArmNodeSocketAction', 'Pause') + self.add_input('ArmNodeSocketAction', 'Stop') + self.add_input('ArmNodeSocketAction', 'Update Volume') + self.add_input('ArmFloatSocket', 'Volume', default_value=1.0) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmNodeSocketAction', 'Is Running') + self.add_output('ArmNodeSocketAction', 'Done') + + def draw_buttons(self, context, layout): + layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') + + col = layout.column(align=True) + col.prop(self, 'property5') + col.prop(self, 'property1') + col.prop(self, 'property2') + + layout.label(text="Overrides:") + # Sample rate + split = layout.split(factor=0.15, align=False) + split.prop(self, 'property3', text="") + row = split.row() + if not self.property3: + row.enabled = False + row.prop(self, 'property4') + + 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/sound/LN_play_speaker.py b/blender/arm/logicnode/sound/LN_play_speaker.py new file mode 100644 index 0000000000..d4fdeb846c --- /dev/null +++ b/blender/arm/logicnode/sound/LN_play_speaker.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class PlaySpeakerNode(ArmLogicTreeNode): + """Starts the playback of the given speaker object. If the playback was + paused, it is resumed from the paused position. + + @seeNode Pause Speaker + @seeNode Play Speaker + """ + bl_idname = 'LNPlaySoundNode' + bl_label = 'Play Speaker' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Speaker') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/sound/LN_stop_speaker.py b/blender/arm/logicnode/sound/LN_stop_speaker.py new file mode 100644 index 0000000000..4940133dfb --- /dev/null +++ b/blender/arm/logicnode/sound/LN_stop_speaker.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + +class StopSpeakerNode(ArmLogicTreeNode): + """Stops playback of the given speaker object. The playback position will be + reset to the start. + + @seeNode Pause Speaker + @seeNode Play Speaker + """ + bl_idname = 'LNStopSoundNode' + bl_label = 'Stop Speaker' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Speaker') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/sound/__init__.py b/blender/arm/logicnode/sound/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/string/LN_concatenate_string.py b/blender/arm/logicnode/string/LN_concatenate_string.py new file mode 100644 index 0000000000..5d4c1b8857 --- /dev/null +++ b/blender/arm/logicnode/string/LN_concatenate_string.py @@ -0,0 +1,34 @@ +from arm.logicnode.arm_nodes import * + +class ConcatenateStringNode(ArmLogicTreeNode): + """Concatenates the given string.""" + bl_idname = 'LNConcatenateStringNode' + bl_label = 'Concatenate String' + arm_version = 2 + min_inputs = 1 + + def __init__(self): + super(ConcatenateStringNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'Input 0') + + self.add_output('ArmStringSocket', 'String') + + def draw_buttons(self, context, layout): + 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 = 'ArmStringSocket' + column = row.column(align=True) + op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False + + 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/string/LN_parse_float.py b/blender/arm/logicnode/string/LN_parse_float.py new file mode 100644 index 0000000000..5d14bc3441 --- /dev/null +++ b/blender/arm/logicnode/string/LN_parse_float.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ParseFloatNode(ArmLogicTreeNode): + """Returns the floats that are in the given string.""" + bl_idname = 'LNParseFloatNode' + bl_label = 'Parse Float' + arm_section = 'parse' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Float') + + self.add_input('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/string/LN_parse_int.py b/blender/arm/logicnode/string/LN_parse_int.py new file mode 100644 index 0000000000..4d154675ec --- /dev/null +++ b/blender/arm/logicnode/string/LN_parse_int.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class ParseIntNode(ArmLogicTreeNode): + """Returns the Ints that are in the given string.""" + bl_idname = 'LNParseIntNode' + bl_label = 'Parse Int' + arm_section = 'parse' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'Int') + + self.add_input('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/string/LN_split_string.py b/blender/arm/logicnode/string/LN_split_string.py new file mode 100644 index 0000000000..3b7478aa9e --- /dev/null +++ b/blender/arm/logicnode/string/LN_split_string.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class SplitStringNode(ArmLogicTreeNode): + """Splits the given string.""" + bl_idname = 'LNSplitStringNode' + bl_label = 'Split String' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketArray', 'Array') + + self.add_input('ArmStringSocket', 'String') + self.add_input('ArmStringSocket', 'Split') diff --git a/blender/arm/logicnode/string/LN_string.py b/blender/arm/logicnode/string/LN_string.py new file mode 100644 index 0000000000..fe3e00e8e9 --- /dev/null +++ b/blender/arm/logicnode/string/LN_string.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class StringNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given string as a variable.""" + bl_idname = 'LNStringNode' + bl_label = 'String' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String In') + + self.add_output('ArmStringSocket', 'String Out', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() diff --git a/blender/arm/logicnode/string/LN_string_case.py b/blender/arm/logicnode/string/LN_string_case.py new file mode 100644 index 0000000000..22619e4146 --- /dev/null +++ b/blender/arm/logicnode/string/LN_string_case.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class CaseStringNode(ArmLogicTreeNode): + """Changes the given string case.""" + bl_idname = 'LNCaseStringNode' + bl_label = 'String Case' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('Upper Case', 'Upper Case', 'Upper Case'), + ('Lower Case', 'Lower Case', 'Lower Case'), + ], + name='', default='Upper Case') + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String In') + + self.add_output('ArmStringSocket', 'String Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/string/LN_string_contains.py b/blender/arm/logicnode/string/LN_string_contains.py new file mode 100644 index 0000000000..f9811baf72 --- /dev/null +++ b/blender/arm/logicnode/string/LN_string_contains.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + +class ContainsStringNode(ArmLogicTreeNode): + """Returns whether the given string contains a given part.""" + bl_idname = 'LNContainsStringNode' + bl_label = 'String Contains' + arm_version = 1 + property0: HaxeEnumProperty( + 'property0', + items = [('Contains', 'Contains', 'Contains'), + ('Starts With', 'Starts With', 'Starts With'), + ('Ends With', 'Ends With', 'Ends With'), + ], + name='', default='Contains') + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String') + self.add_input('ArmStringSocket', 'Find') + + self.add_output('ArmBoolSocket', 'Contains') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/string/LN_string_length.py b/blender/arm/logicnode/string/LN_string_length.py new file mode 100644 index 0000000000..5edf39cba5 --- /dev/null +++ b/blender/arm/logicnode/string/LN_string_length.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class LengthStringNode(ArmLogicTreeNode): + """Returns the length of the given string.""" + bl_idname = 'LNLengthStringNode' + bl_label = 'String Length' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmIntSocket', 'Length') + + self.add_input('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/string/LN_string_replace.py b/blender/arm/logicnode/string/LN_string_replace.py new file mode 100644 index 0000000000..cae6b2f42d --- /dev/null +++ b/blender/arm/logicnode/string/LN_string_replace.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class StringReplaceNode(ArmLogicTreeNode): + """Replace all ocurrences of string to find in the input String""" + bl_idname = 'LNStringReplaceNode' + bl_label = 'String Replace' + + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String') + self.add_input('ArmStringSocket', 'Find') + self.add_input('ArmStringSocket', 'Replace') + + self.add_output('ArmStringSocket', 'String') diff --git a/blender/arm/logicnode/string/LN_sub_string.py b/blender/arm/logicnode/string/LN_sub_string.py new file mode 100644 index 0000000000..647d0e646f --- /dev/null +++ b/blender/arm/logicnode/string/LN_sub_string.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SubStringNode(ArmLogicTreeNode): + """Returns a part of the given string.""" + bl_idname = 'LNSubStringNode' + bl_label = 'Sub String' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmStringSocket', 'String In') + self.add_input('ArmIntSocket', 'Start') + self.add_input('ArmIntSocket', 'End') + + self.add_output('ArmStringSocket', 'String Out') diff --git a/blender/arm/logicnode/string/__init__.py b/blender/arm/logicnode/string/__init__.py new file mode 100644 index 0000000000..587622e6b0 --- /dev/null +++ b/blender/arm/logicnode/string/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='String') +add_node_section(name='parse', category='String') diff --git a/blender/arm/logicnode/trait/LN_add_trait_to_object.py b/blender/arm/logicnode/trait/LN_add_trait_to_object.py new file mode 100644 index 0000000000..8877742b81 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_add_trait_to_object.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class AddTraitNode(ArmLogicTreeNode): + """Adds trait to the given object.""" + bl_idname = 'LNAddTraitNode' + bl_label = 'Add Trait to Object' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Trait') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/trait/LN_get_object_trait.py b/blender/arm/logicnode/trait/LN_get_object_trait.py new file mode 100644 index 0000000000..b0890ab281 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_get_object_trait.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetTraitNode(ArmLogicTreeNode): + """Searches for a trait with the specified name which is applied to the + given object and returns that trait.""" + bl_idname = 'LNGetTraitNode' + bl_label = 'Get Object Trait' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Name') + + self.add_output('ArmDynamicSocket', 'Trait') diff --git a/blender/arm/logicnode/trait/LN_get_object_traits.py b/blender/arm/logicnode/trait/LN_get_object_traits.py new file mode 100644 index 0000000000..7fd1dfae50 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_get_object_traits.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class GetObjectTraitsNode(ArmLogicTreeNode): + """Returns all traits from the given object.""" + bl_idname = 'LNGetObjectTraitsNode' + bl_label = 'Get Object Traits' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmNodeSocketArray', 'Traits') diff --git a/blender/arm/logicnode/trait/LN_get_trait_name.py b/blender/arm/logicnode/trait/LN_get_trait_name.py new file mode 100644 index 0000000000..9a34fd70bc --- /dev/null +++ b/blender/arm/logicnode/trait/LN_get_trait_name.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetTraitNameNode(ArmLogicTreeNode): + """Returns the name and the class type of the given trait.""" + bl_idname = 'LNGetTraitNameNode' + bl_label = 'Get Trait Name' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Trait') + + self.add_output('ArmStringSocket', 'Name') + self.add_output('ArmStringSocket', 'Class Type') diff --git a/blender/arm/logicnode/trait/LN_get_trait_paused.py b/blender/arm/logicnode/trait/LN_get_trait_paused.py new file mode 100644 index 0000000000..12f1ac0d89 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_get_trait_paused.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class GetTraitPausedNode(ArmLogicTreeNode): + """Returns whether the given trait is paused.""" + bl_idname = 'LNGetTraitPausedNode' + bl_label = 'Get Trait Paused' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Trait') + + self.add_output('ArmBoolSocket', 'Is Paused') diff --git a/blender/arm/logicnode/trait/LN_remove_trait.py b/blender/arm/logicnode/trait/LN_remove_trait.py new file mode 100644 index 0000000000..7e81f12cbc --- /dev/null +++ b/blender/arm/logicnode/trait/LN_remove_trait.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class RemoveTraitNode(ArmLogicTreeNode): + """Removes the given trait.""" + bl_idname = 'LNRemoveTraitNode' + bl_label = 'Remove Trait' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Trait') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/trait/LN_remove_trait_from_object.py b/blender/arm/logicnode/trait/LN_remove_trait_from_object.py new file mode 100644 index 0000000000..911b375725 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_remove_trait_from_object.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class RemoveTraitObjectNode(ArmLogicTreeNode): + """Remove trait from the given object.""" + bl_idname = 'LNRemoveTraitObjectNode' + bl_label = 'Remove Trait from Object' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Trait') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/trait/LN_self_trait.py b/blender/arm/logicnode/trait/LN_self_trait.py new file mode 100644 index 0000000000..41b09676fa --- /dev/null +++ b/blender/arm/logicnode/trait/LN_self_trait.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class SelfTraitNode(ArmLogicTreeNode): + """Returns the trait that owns this node.""" + bl_idname = 'LNSelfTraitNode' + bl_label = 'Self Trait' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Trait') diff --git a/blender/arm/logicnode/trait/LN_set_trait_paused.py b/blender/arm/logicnode/trait/LN_set_trait_paused.py new file mode 100644 index 0000000000..739692dd36 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_set_trait_paused.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetTraitPausedNode(ArmLogicTreeNode): + """Sets the paused state of the given trait.""" + bl_idname = 'LNSetTraitPausedNode' + bl_label = 'Set Trait Paused' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Trait') + self.add_input('ArmBoolSocket', 'Paused') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/trait/LN_trait.py b/blender/arm/logicnode/trait/LN_trait.py new file mode 100644 index 0000000000..2ab2191386 --- /dev/null +++ b/blender/arm/logicnode/trait/LN_trait.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + + +class TraitNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given trait as a variable. If the trait was not found or + was not exported, an error is thrown ([more information](https://github.com/armory3d/armory/wiki/troubleshooting#trait-not-exported)). + """ + bl_idname = 'LNTraitNode' + bl_label = 'Trait' + arm_version = 1 + + property0: HaxeStringProperty('property0', name='', default='') + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Trait', is_var=True) + + def draw_content(self, context, layout): + layout.prop(self, 'property0') + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.property0 = master_node.property0 diff --git a/blender/arm/logicnode/trait/__init__.py b/blender/arm/logicnode/trait/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blender/arm/logicnode/transform/LN_append_transform.py b/blender/arm/logicnode/transform/LN_append_transform.py new file mode 100644 index 0000000000..3a85088e1d --- /dev/null +++ b/blender/arm/logicnode/transform/LN_append_transform.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class AppendTransformNode(ArmLogicTreeNode): + """Appends transform to the given object.""" + bl_idname = 'LNAppendTransformNode' + bl_label = 'Append Transform' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/transform/LN_get_object_dimension.py b/blender/arm/logicnode/transform/LN_get_object_dimension.py new file mode 100644 index 0000000000..90a81391a8 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_object_dimension.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetDimensionNode(ArmLogicTreeNode): + """Returns the dimension of the given object.""" + bl_idname = 'LNGetDimensionNode' + bl_label = 'Get Object Dimension' + arm_section = 'dimension' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmVectorSocket', 'Dimension') diff --git a/blender/arm/logicnode/transform/LN_get_object_location.py b/blender/arm/logicnode/transform/LN_get_object_location.py new file mode 100644 index 0000000000..d4af86b4a3 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_object_location.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + +class GetLocationNode(ArmLogicTreeNode): + """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 = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmBoolSocket', '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_get_object_rotation.py b/blender/arm/logicnode/transform/LN_get_object_rotation.py new file mode 100644 index 0000000000..067211cfd2 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_object_rotation.py @@ -0,0 +1,91 @@ +from arm.logicnode.arm_nodes import * + +class GetRotationNode(ArmLogicTreeNode): + """Returns the current rotation of the given object.""" + bl_idname = 'LNGetRotationNode' + bl_label = 'Get Object Rotation' + arm_section = 'rotation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_output('ArmRotationSocket', 'Rotation') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + property0: HaxeEnumProperty( + 'property0', + items = [('Local', 'Local', 'Local'), + ('Global', 'Global', 'Global')], + name='', default='Local') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNGetRotationNode') + newself.property0 = 'Local' + newnodes = [newself] + + if len(self.outputs[0].links)>0: + # euler (radians) needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "EulerAngles" + converter.property1 = "Rad" + converter.property2 = "XZY" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[4].links)>0 or len(self.outputs[5].links)>0: + # quaternion needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "Quaternion" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[4].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + for link in self.outputs[5].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + if len(self.outputs[1].links)>0 or len(self.outputs[2].links)>0 or len(self.outputs[3].links)>0: + # axis/angle needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Rad" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[1].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[3].links)==0 and len(self.outputs[2].links)==0: + pass + elif len(self.outputs[3].links)==0: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + elif len(self.outputs[2].links)==0: + converter.property1 = 'Deg' + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + else: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Deg" + converter.property2 = "XYZ" # bogus + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + return newnodes diff --git a/blender/arm/logicnode/transform/LN_get_object_scale.py b/blender/arm/logicnode/transform/LN_get_object_scale.py new file mode 100644 index 0000000000..67d5d5df70 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_object_scale.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class GetScaleNode(ArmLogicTreeNode): + """Returns the scale of the given object.""" + bl_idname = 'LNGetScaleNode' + bl_label = 'Get Object Scale' + arm_section = 'scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmVectorSocket', 'Scale') diff --git a/blender/arm/logicnode/transform/LN_get_object_transform.py b/blender/arm/logicnode/transform/LN_get_object_transform.py new file mode 100644 index 0000000000..347dc81ba1 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_object_transform.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetTransformNode(ArmLogicTreeNode): + """Returns the transformation of the given object. An object's + transform consists of vectors describing its global location, + rotation and scale.""" + bl_idname = 'LNGetTransformNode' + bl_label = 'Get Object Transform' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmDynamicSocket', 'Transform') diff --git a/blender/arm/logicnode/transform/LN_get_world_orientation.py b/blender/arm/logicnode/transform/LN_get_world_orientation.py new file mode 100644 index 0000000000..9e9b7de4b3 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_get_world_orientation.py @@ -0,0 +1,23 @@ +from arm.logicnode.arm_nodes import * + +class GetWorldNode(ArmLogicTreeNode): + """Returns the world orientation of the given object.""" + bl_idname = 'LNGetWorldNode' + bl_label = 'Get World Orientation' + arm_section = 'rotation' + arm_version = 1 + + property0: HaxeEnumProperty( + 'property0', + items = [('Right', 'Right', 'The object right (X) direction'), + ('Look', 'Look', 'The object look (Y) direction'), + ('Up', 'Up', 'The object up (Z) direction')], + name='', default='Look') + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + + self.add_output('ArmVectorSocket', 'Vector') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/blender/arm/logicnode/transform/LN_look_at.py b/blender/arm/logicnode/transform/LN_look_at.py new file mode 100644 index 0000000000..79a3a02d7e --- /dev/null +++ b/blender/arm/logicnode/transform/LN_look_at.py @@ -0,0 +1,47 @@ +from arm.logicnode.arm_nodes import * + +class LookAtNode(ArmLogicTreeNode): + """Returns *a* rotation that makes something look away from X,Y or Z, and instead look in the 'from->to' direction""" + bl_idname = 'LNLookAtNode' + bl_label = 'Look At' + arm_section = 'rotation' + arm_version = 2 + + property0: HaxeEnumProperty( + 'property0', + items = [('X', ' X', 'X'), + ('-X', '-X', '-X'), + ('Y', ' Y', 'Y'), + ('-Y', '-Y', '-Y'), + ('Z', ' Z', 'Z'), + ('-Z', '-Z', '-Z')], + name='With', default='Z') + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'From Location') + self.add_input('ArmVectorSocket', 'To Location') + + self.add_output('ArmRotationSocket', 'Rotation') + + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNLookAtNode') + converter = self.id_data.nodes.new('LNSeparateRotationNode') + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return [newself, converter] diff --git a/blender/arm/logicnode/transform/LN_rotate_object.py b/blender/arm/logicnode/transform/LN_rotate_object.py new file mode 100644 index 0000000000..aab97549ec --- /dev/null +++ b/blender/arm/logicnode/transform/LN_rotate_object.py @@ -0,0 +1,105 @@ +from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation + +class RotateObjectNode(ArmLogicTreeNode): + """Rotates the given object.""" + bl_idname = 'LNRotateObjectNode' + bl_label = 'Rotate Object' + arm_section = 'rotation' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmRotationSocket', 'Rotation') + + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0_proxy') + + + # this property swaperoo is kinda janky-looking, but listen out: + # - when you reload an old file, the properties of loaded nodes can be mangled if the node class drops the property or the specific value within the property. + # -> to fix this, 'property0' needs to contain the old values so that node replacement can be done decently. + # - "but", I hear you ask, "why not make property0 a simple blender property, and create a property0v2 HaxeProperty to be bound to the haxe-time property0?" + # -> well, at the time of writing, a HaxeProperty's prop_name is only used for livepatching, not at initial setup, so a freshly-compiled game would get completely borked properties. + # solution: have a property0 HaxeProperty contain every possible value, and have a property0_proxy Property be in the UI. + + # NOTE FOR FUTURE MAINTAINERS: the value of the proxy property does **not** matter, only the value of property0 does. When eventually editing this class, you can safely drop the values in the proxy property, and *only* the proxy property. + + def on_proxyproperty_update(self, context=None): + self.property0 = self.property0_proxy + + property0_proxy: EnumProperty( + items = [('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'), + ('Global', 'Global/Parent F.O.R.', + 'Frame of reference oriented with the object\'s parent or the world')], + name='', default='Local', + update = on_proxyproperty_update + ) + property0: HaxeEnumProperty( + 'property0', + items=[('Euler Angles', 'NODE REPLACEMENT ONLY', ''), + ('Angle Axies (Radians)', 'NODE REPLACEMENT ONLY', ''), + ('Angle Axies (Degrees)', 'NODE REPLACEMENT ONLY', ''), + ('Quaternion', 'NODE REPLACEMENT ONLY', ''), + ('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'), + ('Global', 'Global/Parent F.O.R.', + 'Frame of reference oriented with the object\'s parent or the world') + ], + name='', default='Local') + + + + + + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + else: # starts with "Angle Axies" + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] diff --git a/blender/arm/logicnode/transform/LN_separate_rotation.py b/blender/arm/logicnode/transform/LN_separate_rotation.py new file mode 100644 index 0000000000..e751d9d2b5 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -0,0 +1,60 @@ +from arm.logicnode.arm_nodes import * + +class SeparateRotationNode(ArmLogicTreeNode): + """Decompose a rotation into one of its mathematical representations""" + bl_idname = 'LNSeparateRotationNode' + bl_label = 'Separate Rotation' + arm_section = 'rotation' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmRotationSocket', 'Angle') + + self.add_output('ArmVectorSocket', 'Euler Angles / Vector XYZ') + self.add_output('ArmFloatSocket', 'Angle / W') + + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == 'Quaternion': + self.outputs[0].name = "Quaternion XYZ" + self.outputs[1].name = "Quaternion W" + elif self.property0 == 'EulerAngles': + self.outputs[0].name = "Euler Angles" + self.outputs[1].name = "[unused for Euler output]" + elif self.property0 == 'AxisAngle': + self.outputs[0].name = "Axis" + self.outputs[1].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_separate_rotation.py') + + def draw_buttons(self, context, layout): + coll = layout.column(align=True) + coll.prop(self, 'property0') + if self.property0 in ('EulerAngles','AxisAngle'): + coll.prop(self, 'property1') + if self.property0=='EulerAngles': + coll.prop(self, 'property2') + + property0: HaxeEnumProperty( + 'property0', + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: HaxeEnumProperty( + 'property1', + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: HaxeEnumProperty( + 'property2', + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ') diff --git a/blender/arm/logicnode/transform/LN_separate_transform.py b/blender/arm/logicnode/transform/LN_separate_transform.py new file mode 100644 index 0000000000..2770841ac2 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_separate_transform.py @@ -0,0 +1,45 @@ +from arm.logicnode.arm_nodes import * + +class SeparateTransformNode(ArmLogicTreeNode): + """Separates the transform of the given object.""" + bl_idname = 'LNSeparateTransformNode' + bl_label = 'Separate Transform' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmVectorSocket', 'Location') + self.add_output('ArmRotationSocket', 'Rotation') + self.add_output('ArmVectorSocket', 'Scale') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNSeparateTransformNode') + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + for link in self.outputs[2].links: + self.id_data.links.new(newself.outputs[2], link.to_socket) + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + + ret = [newself] + rot_links = self.outputs[1].links + if len(rot_links) >0: + converter = self.id_data.nodes.new('LNSeparateRotationNode') + ret.append(converter) + self.id_data.links.new(newself.outputs[1], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in rot_links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return ret diff --git a/blender/arm/logicnode/transform/LN_set_object_location.py b/blender/arm/logicnode/transform/LN_set_object_location.py new file mode 100644 index 0000000000..5c9e4ac97e --- /dev/null +++ b/blender/arm/logicnode/transform/LN_set_object_location.py @@ -0,0 +1,28 @@ +from arm.logicnode.arm_nodes import * + +class SetLocationNode(ArmLogicTreeNode): + """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 = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Location') + self.add_input('ArmBoolSocket', '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) diff --git a/blender/arm/logicnode/transform/LN_set_object_rotation.py b/blender/arm/logicnode/transform/LN_set_object_rotation.py new file mode 100644 index 0000000000..2e2af8f560 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_set_object_rotation.py @@ -0,0 +1,76 @@ +from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation + +class SetRotationNode(ArmLogicTreeNode): + """Sets the rotation of the given object.""" + bl_idname = 'LNSetRotationNode' + bl_label = 'Set Object Rotation' + arm_section = 'rotation' + arm_version = 2 + + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmRotationSocket', 'Rotation') + + 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() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + elif self.property0.startswith("Angle Axies "): + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + else: + raise ValueError('nonsensical value {:s} for property0 in SetObjectRotationNode/V1. please report this to the devs.'.format(self.property0)) + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] + + # note: this is unused, but kept here so that the 'property0' field can be read during node replacement + property0: EnumProperty( + items = [('Euler Angles', '',''), + ('Angle Axies (Radians)', '', ''), + ('Angle Axies (Degrees)', '', ''), + ('Quaternion', '', '')], + name='', default='Euler Angles') diff --git a/blender/arm/logicnode/transform/LN_set_object_scale.py b/blender/arm/logicnode/transform/LN_set_object_scale.py new file mode 100644 index 0000000000..7f7221d9d2 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_set_object_scale.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + +class SetScaleNode(ArmLogicTreeNode): + """Sets the scale of the given object.""" + bl_idname = 'LNSetScaleNode' + bl_label = 'Set Object Scale' + arm_section = 'scale' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0]) + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/transform/LN_set_object_transform.py b/blender/arm/logicnode/transform/LN_set_object_transform.py new file mode 100644 index 0000000000..1a139551d0 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_set_object_transform.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class SetTransformNode(ArmLogicTreeNode): + """Sets the transform of the given object.""" + bl_idname = 'LNSetTransformNode' + bl_label = 'Set Object Transform' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/transform/LN_transform_math.py b/blender/arm/logicnode/transform/LN_transform_math.py new file mode 100644 index 0000000000..bc5a90a242 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_transform_math.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class TransformMathNode(ArmLogicTreeNode): + """Operates the two given transform values.""" + bl_idname = 'LNTransformMathNode' + bl_label = 'Transform Math' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Transform 1') + self.add_input('ArmDynamicSocket', 'Transform 2') + + self.add_output('ArmDynamicSocket', 'Result') diff --git a/blender/arm/logicnode/transform/LN_transform_to_vector.py b/blender/arm/logicnode/transform/LN_transform_to_vector.py new file mode 100644 index 0000000000..222f8abbe0 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_transform_to_vector.py @@ -0,0 +1,40 @@ +from arm.logicnode.arm_nodes import * + +class VectorFromTransformNode(ArmLogicTreeNode): + """Returns vector from the given transform.""" + bl_idname = 'LNVectorFromTransformNode' + bl_label = 'Transform to Vector' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmDynamicSocket', 'Transform') + + self.add_output('ArmVectorSocket', 'Vector') + self.add_output('ArmVectorSocket', 'Quaternion XYZ') + self.add_output('ArmFloatSocket', 'Quaternion W') + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + # note: the conditions on len(self.outputs) are take in account "old version" (pre-2020.9) nodes, which only have one output + if self.property0 == "Quaternion": + self.outputs[0].name = "Quaternion" + if len(self.outputs) > 1: + self.outputs[1].name = "Quaternion XYZ" + self.outputs[2].name = "Quaternion W" + else: + self.outputs[0].name = "Vector" + if len(self.outputs) > 1: + self.outputs[1].name = "[quaternion only]" + self.outputs[2].name = "[quaternion only]" + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + property0: HaxeEnumProperty( + 'property0', + items = [('Right', 'Right', 'The transform right (X) direction'), + ('Look', 'Look', 'The transform look (Y) direction'), + ('Up', 'Up', 'The transform up (Z) direction'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='Look', + update=on_property_update) diff --git a/blender/arm/logicnode/transform/LN_translate_object.py b/blender/arm/logicnode/transform/LN_translate_object.py new file mode 100644 index 0000000000..d5964666ae --- /dev/null +++ b/blender/arm/logicnode/transform/LN_translate_object.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class TranslateObjectNode(ArmLogicTreeNode): + """Translates (moves) the given object using the given vector in world coordinates.""" + bl_idname = 'LNTranslateObjectNode' + bl_label = 'Translate Object' + arm_section = 'location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Vector') + self.add_input('ArmBoolSocket', 'On Local Axis') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/transform/LN_translate_on_local_axis.py b/blender/arm/logicnode/transform/LN_translate_on_local_axis.py new file mode 100644 index 0000000000..08192fe628 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_translate_on_local_axis.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +class TranslateOnLocalAxisNode(ArmLogicTreeNode): + """Translates (moves) the given object using the given vector in the local coordinates.""" + bl_idname = 'LNTranslateOnLocalAxisNode' + bl_label = 'Translate On Local Axis' + arm_section = 'location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmFloatSocket', 'Speed') + self.add_input('ArmIntSocket', 'Forward/Up/Right') + self.add_input('ArmBoolSocket', 'Inverse') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py new file mode 100644 index 0000000000..c80fb36736 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_vector_to_object_orientation.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + +class VectorToObjectOrientationNode(ArmLogicTreeNode): + """Transform world coordinates into object oriented coordinates (in other words: apply object rotation to it). + + @seeNode World Vector to Object Space + @seeNode Get World Orientation + @seeNode Vector From Transform + """ + bl_idname = 'LNVectorToObjectOrientationNode' + bl_label = 'Vector to Object Orientation' + arm_section = 'rotation' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'World') + + self.add_output('ArmVectorSocket', 'Oriented') 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 new file mode 100644 index 0000000000..5bc8442c92 --- /dev/null +++ b/blender/arm/logicnode/transform/LN_world_vector_to_local_space.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + +class WorldVectorToLocalSpaceNode(ArmLogicTreeNode): + """Transform world coordinates into object local coordinates. + + @seeNode Vector to Object Orientation + @seeNode Get World Orientation + @seeNode Vector From Transform + """ + bl_idname = 'LNWorldVectorToLocalSpaceNode' + bl_label = 'World Vector to Local Space' + arm_section = 'location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'World') + + self.add_output('ArmVectorSocket', 'Local') diff --git a/blender/arm/logicnode/transform/__init__.py b/blender/arm/logicnode/transform/__init__.py new file mode 100644 index 0000000000..989fc44648 --- /dev/null +++ b/blender/arm/logicnode/transform/__init__.py @@ -0,0 +1,8 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='Transform') +add_node_section(name='location', category='Transform') +add_node_section(name='rotation', category='Transform') +add_node_section(name='scale', category='Transform') +add_node_section(name='quaternions', category='Transform') +add_node_section(name='misc', category='Transform') diff --git a/blender/arm/logicnode/tree_variables.py b/blender/arm/logicnode/tree_variables.py new file mode 100644 index 0000000000..42cab1ab84 --- /dev/null +++ b/blender/arm/logicnode/tree_variables.py @@ -0,0 +1,557 @@ +import bpy +from bpy.props import * + +import arm.log +import arm.make_state +import arm.node_utils +import arm.props_traits_props +import arm.utils +import arm.logicnode.arm_nodes + +if arm.is_reload(__name__): + arm.log = arm.reload_module(arm.log) + arm.make_state = arm.reload_module(arm.make_state) + arm.node_utils = arm.reload_module(arm.node_utils) + arm.props_traits_props = arm.reload_module(arm.props_traits_props) + arm.utils = arm.reload_module(arm.utils) + arm.logicnode.arm_nodes = arm.reload_module(arm.logicnode.arm_nodes) +else: + arm.enable_reload(__name__) + + +class ARM_PT_Variables(bpy.types.Panel): + bl_label = 'Tree Variables' + bl_idname = 'ARM_PT_Variables' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Armory' + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + def draw(self, context): + layout = self.layout + + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + node = context.active_node + if node is None or node.arm_logic_id == '': + layout.operator('arm.variable_promote_node', icon='PLUS') + else: + layout.operator('arm.variable_node_make_local', icon='TRIA_DOWN_BAR') + + row = layout.row(align=True) + col = row.column(align=True) + + num_prop_rows = max(len(tree.arm_treevariableslist), 6) + col.template_list('ARM_UL_TreeVarList', '', tree, 'arm_treevariableslist', tree, 'arm_treevariableslist_index', rows=num_prop_rows) + + col.operator('arm.variable_assign_to_node', icon='NODE') + + if len(tree.arm_treevariableslist) > 0: + selected_item = tree.arm_treevariableslist[tree.arm_treevariableslist_index] + + col.separator() + sub_row = col.row(align=True) + sub_row.alignment = 'EXPAND' + op = sub_row.operator('arm.add_var_node') + op.node_id = selected_item.name + op.node_type = selected_item.node_type + op = sub_row.operator('arm.add_setvar_node') + op.node_id = selected_item.name + op.node_type = selected_item.node_type + + col = row.column(align=True) + col.enabled = len(tree.arm_treevariableslist) > 1 + col.operator('arm_treevariableslist.move_item', icon='TRIA_UP', text='').direction = 'UP' + col.operator('arm_treevariableslist.move_item', icon='TRIA_DOWN', text='').direction = 'DOWN' + + if len(tree.arm_treevariableslist) > 0: + selected_item = tree.arm_treevariableslist[tree.arm_treevariableslist_index] + + box = layout.box() + box.label(text='Selected Variable:') + master_node = arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.get_master_node(tree, selected_item.name) + master_node.draw_content(context, box) + for inp in master_node.inputs: + if hasattr(inp, 'draw'): + inp.draw(context, box, master_node, inp.label if inp.label is not None else inp.name) + + +class ARM_OT_TreeVariablePromoteNode(bpy.types.Operator): + bl_idname = 'arm.variable_promote_node' + bl_label = 'New Var From Node' + bl_description = 'Create a tree variable from the active node and promote it to a tree variable node' + + var_name: StringProperty( + name='Name', + description='Name of the new tree variable', + default='Untitled' + ) + + @classmethod + def poll(cls, context): + if not arm.logicnode.arm_nodes.is_logic_node_edit_context(context): + return False + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + if tree is None: + return False + + node = context.active_node + if node is None or not node.bl_idname.startswith('LN'): + return False + + if not isinstance(node, arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin): + return False + + return node.arm_logic_id == '' + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self, width=400) + + def execute(self, context): + node: arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin = context.active_node + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + + var_type = node.bl_idname + var_item = ARM_PG_TreeVarListItem.create_new(tree, self.var_name, var_type) + + node.is_master_node = True + node.arm_logic_id = var_item.name + node.use_custom_color = True + node.color = var_item.color + + arm.make_state.redraw_ui = True + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + row = layout.row() + row.scale_y = 1.3 + row.activate_init = True + row.prop(self, 'var_name') + + +class ARM_OT_TreeVariableMakeLocalNode(bpy.types.Operator): + bl_idname = 'arm.variable_node_make_local' + bl_label = 'Make Node Local' + bl_description = ( + 'Remove the reference to the tree variable from the active node. ' + 'If the active node is the only node that links to the selected ' + 'tree variable, the tree variable is removed' + ) + + @classmethod + def poll(cls, context): + if not arm.logicnode.arm_nodes.is_logic_node_edit_context(context): + return False + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + if tree is None: + return False + + node = context.active_node + if node is None or not node.bl_idname.startswith('LN'): + return False + + if not isinstance(node, arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin): + return False + + return node.arm_logic_id != '' + + def execute(self, context): + node: arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin = context.active_node + node.make_local() + node.color = [0.608, 0.608, 0.608] # default color + node.use_custom_color = False + + return {'FINISHED'} + + +class ARM_OT_TreeVariableVariableAssignToNode(bpy.types.Operator): + bl_idname = 'arm.variable_assign_to_node' + bl_label = 'Assign To Node' + bl_description = ( + 'Assign the selected tree variable to the active variable node. ' + 'The variable node must have the same type as the variable' + ) + + @classmethod + def poll(cls, context): + if not arm.logicnode.arm_nodes.is_logic_node_edit_context(context): + return False + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + if tree is None or len(tree.arm_treevariableslist) == 0: + return False + + node = context.active_node + if node is None or not node.bl_idname.startswith('LN'): + return False + + if not isinstance(node, arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin): + return False + + # Only assign variables to nodes of the correct type + if node.bl_idname != tree.arm_treevariableslist[tree.arm_treevariableslist_index].node_type: + return False + + return True + + def execute(self, context): + node: arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin = context.active_node + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + + var_item = tree.arm_treevariableslist[tree.arm_treevariableslist_index] + + # Make node local first to ensure the old tree variable (if + # linked) is notified that the node is no longer linked + if node.arm_logic_id != var_item.name: + node.make_local() + + node.arm_logic_id = var_item.name + node.use_custom_color = True + node.color = var_item.color + arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.synchronize(tree, node.arm_logic_id) + + return {'FINISHED'} + + +class ARM_OT_TreeVariableListMoveItem(bpy.types.Operator): + bl_idname = 'arm_treevariableslist.move_item' + bl_label = 'Move' + bl_description = 'Move an item in the list' + bl_options = {'UNDO', 'INTERNAL'} + + direction: EnumProperty( + items=( + ('UP', 'Up', ''), + ('DOWN', 'Down', '') + ) + ) + + def execute(self, context): + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + index = tree.arm_treevariableslist_index + + max_index = len(tree.arm_treevariableslist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + new_index = max(0, min(new_index, max_index)) + + tree.arm_treevariableslist.move(index, new_index) + tree.arm_treevariableslist_index = new_index + + return{'FINISHED'} + + +class ARM_OT_AddVarGetterNode(bpy.types.Operator): + """Add a node to get the value of the selected tree variable""" + bl_idname = 'arm.add_var_node' + bl_label = 'Add Getter' + bl_options = {'GRAB_CURSOR', 'BLOCKING'} + + node_id: StringProperty() + node_type: StringProperty() + getter_node_ref = None + + @classmethod + def poll(cls, context): + if not arm.logicnode.arm_nodes.is_logic_node_edit_context(context): + return False + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + return tree is not None and len(tree.arm_treevariableslist) > 0 + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + self.execute(context) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'MOUSEMOVE': + self.getter_node_ref.location = context.space_data.cursor_location + elif event.type == 'LEFTMOUSE': # Confirm + return {'FINISHED'} + return {'RUNNING_MODAL'} + + def execute(self, context): + self.getter_node_ref = self.create_getter_node(context, self.node_type, self.node_id) + return {'FINISHED'} + + @staticmethod + def create_getter_node(context, node_type: str, node_id: str) -> arm.logicnode.arm_nodes.ArmLogicTreeNode: + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + nodes = context.space_data.path[-1].node_tree.nodes + + node = nodes.new(node_type) + node.location = context.space_data.cursor_location + node.arm_logic_id = node_id + node.use_custom_color = True + node.color = tree.arm_treevariableslist[tree.arm_treevariableslist_index].color + + arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.synchronize(tree, node.arm_logic_id) + + return node + + +class ARM_OT_AddVarSetterNode(bpy.types.Operator): + """Add a node to set the value of the selected tree variable""" + bl_idname = 'arm.add_setvar_node' + bl_label = 'Add Setter' + bl_options = {'GRAB_CURSOR', 'BLOCKING'} + + node_id: StringProperty() + node_type: StringProperty() + getter_node_ref = None + setter_node_ref = None + + @classmethod + def poll(cls, context): + if not arm.logicnode.arm_nodes.is_logic_node_edit_context(context): + return False + tree: bpy.types.NodeTree = context.space_data.path[-1].node_tree + return tree is not None and len(tree.arm_treevariableslist) > 0 + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + self.execute(context) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'MOUSEMOVE': + self.setter_node_ref.location = context.space_data.cursor_location + self.getter_node_ref.location[0] = context.space_data.cursor_location[0] + self.getter_node_ref.location[1] = context.space_data.cursor_location[1] - self.setter_node_ref.height - 17 + elif event.type == 'LEFTMOUSE': # Confirm + return {'FINISHED'} + return {'RUNNING_MODAL'} + + def execute(self, context): + nodes = context.space_data.path[-1].node_tree.nodes + + node = ARM_OT_AddVarGetterNode.create_getter_node(context, self.node_type, self.node_id) + + setter_node = nodes.new('LNSetVariableNode') + setter_node.location = context.space_data.cursor_location + + links = context.space_data.path[-1].node_tree.links + links.new(node.outputs[0], setter_node.inputs[1]) + + self.getter_node_ref = node + self.setter_node_ref = setter_node + return {'FINISHED'} + + +class ARM_PG_TreeVarListItem(bpy.types.PropertyGroup): + def _set_name(self, value: str): + old_name = self._get_name() + + tree = bpy.context.space_data.path[-1].node_tree + lst = tree.arm_treevariableslist + + if value == '': + # Don't allow empty variable names + new_name = old_name + else: + new_name = arm.utils.unique_name_in_lists(item_lists=[lst], name_attr='name', wanted_name=value, ignore_item=self) + + self['_name'] = new_name + + for node in tree.nodes: + if node.arm_logic_id == old_name: + node.arm_logic_id = new_name + + def _get_name(self) -> str: + return self.get('_name', 'Untitled') + + def _update_color(self, context): + space = context.space_data + + # Can be None if color is set before tree is initialized (upon + # updating old files to newer SDK for example) + if space is not None: + for node in space.path[-1].node_tree.nodes: + if node.arm_logic_id == self.name: + node.use_custom_color = True + node.color = self.color + + name: StringProperty( + name='Name', + description='The name of this variable', + default='Untitled', + get=_get_name, + set=_set_name + ) + + node_type: StringProperty( + name='Type', + description='The type of this variable/the bl_idname of the node\'s that may use this variable', + default='LNIntegerNode' + ) + + color: FloatVectorProperty( + name='Color', + description='The color of the nodes that link to this tree variable', + subtype='COLOR', + default=[1.0, 1.0, 1.0], + update=_update_color, + size=3, + min=0, + max=1 + ) + + @classmethod + def create_new(cls, tree: bpy.types.NodeTree, item_name: str, item_type: str) -> 'ARM_PG_TreeVarListItem': + lst = tree.arm_treevariableslist + + var_item: ARM_PG_TreeVarListItem = lst.add() + var_item['_name'] = arm.utils.unique_name_in_lists( + item_lists=[lst], name_attr='name', wanted_name=item_name, ignore_item=var_item + ) + var_item.node_type = item_type + var_item.color = arm.utils.get_random_color_rgb() + + tree.arm_treevariableslist_index = len(lst) - 1 + arm.make_state.redraw_ui = True + + return var_item + + +class ARM_UL_TreeVarList(bpy.types.UIList): + def draw_item(self, context, layout, data, item: ARM_PG_TreeVarListItem, icon, active_data, active_propname, index): + node_type = arm.utils.type_name_to_type(item.node_type).bl_label + + row = layout.row(align=True) + _row = row.row() + _row.ui_units_x = 1.0 + _row.prop(item, 'color', text='') + row.prop(item, 'name', text='', emboss=False) + row.label(text=node_type) + + +def on_update_node_logic_id(node: arm.logicnode.arm_nodes.ArmLogicTreeNode, context): + node.on_logic_id_change() + + +def node_compat_sdk2203(): + """Replace old arm_logic_id system with tree variable system.""" + for tree in bpy.data.node_groups: + if tree.bl_idname == 'ArmLogicTreeType': + # All tree variable nodes + tv_nodes: dict[str, list[arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin]] = {} + + # The type of the tree variable. If two types are found for + # a logic ID and one is dynamic, assume it's a getter node. + # Otherwise show a warning upon conflict, it was undefined + # behaviour before anyway. + tv_types: dict[str, str] = {} + + # First pass: find all tree variable nodes and decide the + # variable type in case of conflicts + node: arm.logicnode.arm_nodes.ArmLogicTreeNode + for node in list(tree.nodes): + if node.arm_logic_id != '': + if not isinstance(node, arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin): + arm.log.warn( + 'While updating the file to the current SDK' + f' version, the node {node.name} in tree' + f' {tree.name} is no variable node but had' + ' a logic ID. The logic ID was reset to' + ' prevent undefined behaviour.' + ) + node.arm_logic_id = '' + continue + + if node.arm_logic_id in tv_nodes: + tv_nodes[node.arm_logic_id].append(node) + + # Check for getter nodes and type conflicts + cur_type = tv_types[node.arm_logic_id] + if cur_type == 'LNDynamicNode': + tv_types[node.arm_logic_id] = node.bl_idname + elif cur_type != node.bl_idname and node.bl_idname != 'LNDynamicNode': + arm.log.warn( + 'Found nodes of different types with the' + ' same logic ID while updating the file' + ' to the current SDK version (undefined' + ' behaviour).\n' + f'\tConflicting types: {cur_type}, {node.bl_idname}\n' + f'\tLogic ID: {node.arm_logic_id}\n' + f'\tNew type for both nodes: {cur_type}' + ) + else: + tv_nodes[node.arm_logic_id] = [node] + tv_types[node.arm_logic_id] = node.bl_idname + + # Second pass: add the tree variable and convert all found + # tree var nodes to the correct type + for logic_id in tv_nodes.keys(): + var_type = tv_types[logic_id] + + var_item = ARM_PG_TreeVarListItem.create_new(tree, logic_id, var_type) + + for node in tv_nodes[logic_id]: + if node.bl_idname != var_type: + newnode = tree.nodes.new(var_type) + arm.node_utils.copy_basic_node_props(from_node=node, to_node=newnode) + + # Connect outputs as good as possible + for i in range(min(len(node.outputs), len(newnode.outputs))): + for out in node.outputs: + for link in out.links: + tree.links.new(newnode.outputs[i], link.to_socket) + + tree.nodes.remove(node) + node = newnode + + # Hide sockets + node.on_logic_id_change() + + node.use_custom_color = True + node.color = var_item.color + + arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.choose_new_master_node(tree, logic_id) + + +def node_compat_sdk2209(): + # See https://github.com/armory3d/armory/pull/2538 + for tree in bpy.data.node_groups: + if tree.bl_idname == "ArmLogicTreeType": + for item in tree.arm_treevariableslist: + arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.synchronize(tree, item.name) + + +REG_CLASSES = ( + ARM_PT_Variables, + ARM_OT_TreeVariableListMoveItem, + ARM_OT_TreeVariableMakeLocalNode, + ARM_OT_TreeVariableVariableAssignToNode, + ARM_OT_TreeVariablePromoteNode, + ARM_OT_AddVarGetterNode, + ARM_OT_AddVarSetterNode, + ARM_UL_TreeVarList, + ARM_PG_TreeVarListItem, +) +register_classes, unregister_classes = bpy.utils.register_classes_factory(REG_CLASSES) + + +def register(): + register_classes() + + bpy.types.Node.arm_logic_id = StringProperty( + name='ID', + description='Nodes with equal identifier share data', + default='', + update=on_update_node_logic_id + ) + + bpy.types.NodeTree.arm_treevariableslist = CollectionProperty(type=ARM_PG_TreeVarListItem) + bpy.types.NodeTree.arm_treevariableslist_index = IntProperty(name='Index for arm_variableslist', default=0) + + +def unregister(): + unregister_classes() diff --git a/blender/arm/logicnode/variable/LN_boolean.py b/blender/arm/logicnode/variable/LN_boolean.py new file mode 100644 index 0000000000..c2fe8b31de --- /dev/null +++ b/blender/arm/logicnode/variable/LN_boolean.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + + +class BooleanNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given boolean as a variable. A boolean value has just two + states: `true` and `false`.""" + bl_idname = 'LNBooleanNode' + bl_label = 'Boolean' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmBoolSocket', 'Bool In') + + self.add_output('ArmBoolSocket', 'Bool Out', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() diff --git a/blender/arm/logicnode/variable/LN_color.py b/blender/arm/logicnode/variable/LN_color.py new file mode 100644 index 0000000000..a7e5112edc --- /dev/null +++ b/blender/arm/logicnode/variable/LN_color.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + + +class ColorNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given color as a variable.""" + bl_idname = 'LNColorNode' + bl_label = 'Color' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmColorSocket', 'Color In', default_value=[1.0, 1.0, 1.0, 1.0]) + + self.add_output('ArmColorSocket', 'Color Out', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() diff --git a/blender/arm/logicnode/variable/LN_dynamic.py b/blender/arm/logicnode/variable/LN_dynamic.py new file mode 100644 index 0000000000..7fc9fe6531 --- /dev/null +++ b/blender/arm/logicnode/variable/LN_dynamic.py @@ -0,0 +1,11 @@ +from arm.logicnode.arm_nodes import * + + +class DynamicNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given dynamic value (a value with an arbitrary type) as a variable.""" + bl_idname = 'LNDynamicNode' + bl_label = 'Dynamic' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Dynamic', is_var=True) diff --git a/blender/arm/logicnode/variable/LN_float.py b/blender/arm/logicnode/variable/LN_float.py new file mode 100644 index 0000000000..6b0bcdfe2c --- /dev/null +++ b/blender/arm/logicnode/variable/LN_float.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + + +class FloatNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given float as a variable. If the set float value has more + than 3 decimal places, the displayed value in the node will be + rounded, but when you click on it you can still edit the exact + value which will be used in the game as well.""" + bl_idname = 'LNFloatNode' + bl_label = 'Float' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'Float In') + self.add_output('ArmFloatSocket', 'Float Out', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() + diff --git a/blender/arm/logicnode/variable/LN_integer.py b/blender/arm/logicnode/variable/LN_integer.py new file mode 100644 index 0000000000..4ae77359cd --- /dev/null +++ b/blender/arm/logicnode/variable/LN_integer.py @@ -0,0 +1,15 @@ +from arm.logicnode.arm_nodes import * + + +class IntegerNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given integer (a whole number) as a variable.""" + bl_idname = 'LNIntegerNode' + bl_label = 'Integer' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmIntSocket', 'Int In') + self.add_output('ArmIntSocket', 'Int Out', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() diff --git a/blender/arm/logicnode/variable/LN_mask.py b/blender/arm/logicnode/variable/LN_mask.py new file mode 100644 index 0000000000..7fb5a3ec26 --- /dev/null +++ b/blender/arm/logicnode/variable/LN_mask.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + + +class MaskNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """TO DO.""" + bl_idname = 'LNMaskNode' + bl_label = 'Mask' + arm_version = 1 + + def arm_init(self, context): + for i in range(1, 21): + label = 'Group {:02d}'.format(i) + self.inputs.new('ArmBoolSocket', label) + + self.add_output('ArmIntSocket', 'Mask', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + for i in range(len(self.inputs)): + self.inputs[i].default_value_raw = master_node.inputs[i].get_default_value() diff --git a/blender/arm/logicnode/variable/LN_retain_value.py b/blender/arm/logicnode/variable/LN_retain_value.py new file mode 100644 index 0000000000..3b9d1f5f11 --- /dev/null +++ b/blender/arm/logicnode/variable/LN_retain_value.py @@ -0,0 +1,30 @@ +from arm.logicnode.arm_nodes import * + +class RetainValueNode(ArmLogicTreeNode): + """Retains the input value. + + @input Retain: Retains the value when exeuted. + @input Value: The value that should be retained. + """ + bl_idname = 'LNRetainValueNode' + bl_label = 'Retain Value' + arm_section = 'set' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Retain') + self.add_input('ArmDynamicSocket', 'Value', is_var=True) + + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Value') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + "LNRetainValueNode", self.arm_version, + "LNRetainValueNode", 2, + in_socket_mapping={0: 0, 1: 1}, + out_socket_mapping={0: 1, 1: 0}, + ) diff --git a/blender/arm/logicnode/variable/LN_rotation.py b/blender/arm/logicnode/variable/LN_rotation.py new file mode 100644 index 0000000000..211599298a --- /dev/null +++ b/blender/arm/logicnode/variable/LN_rotation.py @@ -0,0 +1,70 @@ +from arm.logicnode.arm_nodes import * + + +class RotationNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """A rotation, created from one of its possible mathematical representations""" + bl_idname = 'LNRotationNode' + bl_label = 'Rotation' + bl_description = 'Create a Rotation object, describing the difference between two orientations (internally represented as a quaternion for efficiency)' + #arm_section = 'rotation' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Euler Angles / Vector XYZ') + self.add_input('ArmFloatSocket', 'Angle / W') + + self.add_output('ArmRotationSocket', 'Out', is_var=True) + + def on_property_update(self, context): + """called by the EnumProperty, used to update the node socket labels""" + if self.property0 == "Quaternion": + self.inputs[0].name = "Quaternion XYZ" + self.inputs[1].name = "Quaternion W" + elif self.property0 == "EulerAngles": + self.inputs[0].name = "Euler Angles" + self.inputs[1].name = "[unused for Euler input]" + elif self.property0 == "AxisAngle": + self.inputs[0].name = "Axis" + self.inputs[1].name = "Angle" + else: + raise ValueError('No nodesocket labels for current input mode: check self-consistancy of LN_rotation.py') + + def draw_content(self, context, layout): + coll = layout.column(align=True) + coll.prop(self, 'property0') + if self.property0 in ('EulerAngles','AxisAngle'): + coll.prop(self, 'property1') + if self.property0=='EulerAngles': + coll.prop(self, 'property2') + + property0: HaxeEnumProperty( + 'property0', + items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), + ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), + ('Quaternion', 'Quaternion', 'Quaternion')], + name='', default='EulerAngles', + update=on_property_update) + + property1: HaxeEnumProperty( + 'property1', + items=[('Deg', 'Degrees', 'Degrees'), + ('Rad', 'Radians', 'Radians')], + name='', default='Rad') + property2: HaxeEnumProperty( + 'property2', + items=[('XYZ','XYZ','XYZ'), + ('XZY','XZY (legacy Armory euler order)','XZY (legacy Armory euler order)'), + ('YXZ','YXZ','YXZ'), + ('YZX','YZX','YZX'), + ('ZXY','ZXY','ZXY'), + ('ZYX','ZYX','ZYX')], + name='', default='XYZ' + ) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.property0 = master_node.property0 + self.property1 = master_node.property1 + self.property2 = master_node.property2 + + for i in range(len(self.inputs)): + self.inputs[i].default_value_raw = master_node.inputs[i].get_default_value() diff --git a/blender/arm/logicnode/variable/LN_scene.py b/blender/arm/logicnode/variable/LN_scene.py new file mode 100644 index 0000000000..f3fbc5758c --- /dev/null +++ b/blender/arm/logicnode/variable/LN_scene.py @@ -0,0 +1,36 @@ +import bpy + +import arm.utils +from arm.logicnode.arm_nodes import * + + +class SceneNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given scene as a variable.""" + bl_idname = 'LNSceneNode' + bl_label = 'Scene' + arm_version = 2 + + @property + def property0_get(self): + if self.property0 == None: + return '' + if self.property0.name not in bpy.data.scenes: + return self.property0.name + return arm.utils.asset_name(bpy.data.scenes[self.property0.name]) + + property0: HaxePointerProperty('property0', name='', type=bpy.types.Scene) + + def arm_init(self, context): + self.add_output('ArmDynamicSocket', 'Scene', is_var=True) + + def draw_content(self, context, layout): + layout.prop_search(self, 'property0', bpy.data, 'scenes', icon='NONE', text='') + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.property0 = master_node.property0 + + 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/variable/LN_set_variable.py b/blender/arm/logicnode/variable/LN_set_variable.py new file mode 100644 index 0000000000..411aca6ac7 --- /dev/null +++ b/blender/arm/logicnode/variable/LN_set_variable.py @@ -0,0 +1,21 @@ +from arm.logicnode.arm_nodes import * + +class SetVariableNode(ArmLogicTreeNode): + """Sets the value of the given variable. + + @input Variable: this socket must be connected to a variable node + (recognized by the little dot inside the socket). The value that + is stored inside the connected node is changed upon activation. + @input Value: the value that should be written into the variable. + """ + bl_idname = 'LNSetVariableNode' + bl_label = 'Set Variable' + arm_section = 'set' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Variable', is_var=True) + self.add_input('ArmDynamicSocket', 'Value') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/variable/LN_transform.py b/blender/arm/logicnode/variable/LN_transform.py new file mode 100644 index 0000000000..d0c838d5e5 --- /dev/null +++ b/blender/arm/logicnode/variable/LN_transform.py @@ -0,0 +1,56 @@ +from arm.logicnode.arm_nodes import * + + +class TransformNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the location, rotation and scale values as a transform.""" + bl_idname = 'LNTransformNode' + bl_label = 'Transform' + arm_version = 2 + + def arm_init(self, context): + self.add_input('ArmVectorSocket', 'Location') + self.add_input('ArmRotationSocket', 'Rotation') + self.add_input('ArmVectorSocket', 'Scale', default_value=[1.0, 1.0, 1.0]) + self.add_output('ArmDynamicSocket', 'Transform', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + self.inputs[0].default_value_raw = master_node.inputs[0].get_default_value() + self.inputs[2].default_value_raw = master_node.inputs[2].get_default_value() + + self.inputs[1].default_value_mode = master_node.inputs[1].default_value_mode + self.inputs[1].default_value_unit = master_node.inputs[1].default_value_unit + self.inputs[1].default_value_order = master_node.inputs[1].default_value_order + self.inputs[1].default_value_s0 = master_node.inputs[1].default_value_s0 + self.inputs[1].default_value_s1 = master_node.inputs[1].default_value_s1 + self.inputs[1].default_value_s2 = master_node.inputs[1].default_value_s2 + self.inputs[1].default_value_s3 = master_node.inputs[1].default_value_s3 + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNTransformNode') + ret = [newself] + + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + for link in self.inputs[2].links: + self.id_data.links.new(link.from_socket, newself.inputs[2]) + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + + links_rot = self.inputs[1].links + if len(links_rot) > 0: + converter = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(converter.outputs[0], newself.inputs[1]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + ret.append(converter) + for link in links_rot: + self.id_data.links.new(link.from_socket, converter.inputs[0]) + + return ret diff --git a/blender/arm/logicnode/variable/LN_vector.py b/blender/arm/logicnode/variable/LN_vector.py new file mode 100644 index 0000000000..2acd80dafb --- /dev/null +++ b/blender/arm/logicnode/variable/LN_vector.py @@ -0,0 +1,19 @@ +from arm.logicnode.arm_nodes import * + + +class VectorNode(ArmLogicVariableNodeMixin, ArmLogicTreeNode): + """Stores the given 3D vector as a variable.""" + bl_idname = 'LNVectorNode' + bl_label = 'Vector' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmFloatSocket', 'X') + self.add_input('ArmFloatSocket', 'Y') + self.add_input('ArmFloatSocket', 'Z') + + self.add_output('ArmVectorSocket', 'Vector', is_var=True) + + def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): + for i in range(len(self.inputs)): + self.inputs[i].default_value_raw = master_node.inputs[i].get_default_value() diff --git a/blender/arm/logicnode/variable/__init__.py b/blender/arm/logicnode/variable/__init__.py new file mode 100644 index 0000000000..e2f6f6debe --- /dev/null +++ b/blender/arm/logicnode/variable/__init__.py @@ -0,0 +1,4 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='set', category='Variable') +add_node_section(name='default', category='Variable') diff --git a/blender/arm/make.py b/blender/arm/make.py new file mode 100644 index 0000000000..626b6774d9 --- /dev/null +++ b/blender/arm/make.py @@ -0,0 +1,977 @@ +import errno +import glob +import json +import os +from queue import Queue +import re +import shlex +import shutil +import stat +from string import Template +import subprocess +import threading +import time +from typing import Callable +import webbrowser + +import bpy + +from arm import assets +from arm.exporter import ArmoryExporter +import arm.lib.make_datas +import arm.lib.server +import arm.live_patch as live_patch +import arm.log as log +import arm.make_logic as make_logic +import arm.make_renderpath as make_renderpath +import arm.make_state as state +import arm.make_world as make_world +import arm.utils +import arm.utils_vs +import arm.write_data as write_data + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + arm.exporter = arm.reload_module(arm.exporter) + from arm.exporter import ArmoryExporter + arm.lib.make_datas = arm.reload_module(arm.lib.make_datas) + arm.lib.server = arm.reload_module(arm.lib.server) + live_patch = arm.reload_module(live_patch) + log = arm.reload_module(log) + make_logic = arm.reload_module(make_logic) + make_renderpath = arm.reload_module(make_renderpath) + state = arm.reload_module(state) + make_world = arm.reload_module(make_world) + arm.utils = arm.reload_module(arm.utils) + arm.utils_vs = arm.reload_module(arm.utils_vs) + write_data = arm.reload_module(write_data) +else: + arm.enable_reload(__name__) + +scripts_mtime = 0 # Monitor source changes +profile_time = 0 + +# 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() + + print(*cmd) + p = subprocess.Popen(cmd) + + 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) + + # Open json file + json_name = shader_name + '.json' + with open(json_name, encoding='utf-8') as f: + json_file = f.read() + json_data = json.loads(json_file) + + fp = arm.utils.get_fp_build() + arm.lib.make_datas.make(res, shader_name, json_data, fp, defs, make_variants) + + path = fp + '/compiled/Shaders' + contexts = json_data['contexts'] + for ctx in contexts: + for s in ['vertex_shader', 'fragment_shader', 'geometry_shader', 'tesscontrol_shader', 'tesseval_shader']: + if s in ctx: + shutil.copy(ctx[s], path + '/' + ctx[s].split('/')[-1]) + +def remove_readonly(func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + +def export_data(fp, sdk_path): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + if wrd.arm_verbose_output: + print(f'Armory v{wrd.arm_version} ({wrd.arm_commit})') + print(f'Blender: {bpy.app.version_string}, Target: {state.target}, GAPI: {arm.utils.get_gapi()}') + + # Clean compiled variants if cache is disabled + build_dir = arm.utils.get_fp_build() + if not wrd.arm_cache_build: + if os.path.isdir(build_dir + '/debug/html5-resources'): + shutil.rmtree(build_dir + '/debug/html5-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/krom-resources'): + shutil.rmtree(build_dir + '/krom-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/debug/krom-resources'): + shutil.rmtree(build_dir + '/debug/krom-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/windows-resources'): + shutil.rmtree(build_dir + '/windows-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/linux-resources'): + shutil.rmtree(build_dir + '/linux-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/osx-resources'): + shutil.rmtree(build_dir + '/osx-resources', onerror=remove_readonly) + if os.path.isdir(build_dir + '/compiled/Shaders'): + shutil.rmtree(build_dir + '/compiled/Shaders', onerror=remove_readonly) + + raw_shaders_path = sdk_path + '/armory/Shaders/' + assets_path = sdk_path + '/armory/Assets/' + export_physics = bpy.data.worlds['Arm'].arm_physics != 'Disabled' + export_navigation = bpy.data.worlds['Arm'].arm_navigation != 'Disabled' + export_ui = bpy.data.worlds['Arm'].arm_ui != 'Disabled' + export_network = bpy.data.worlds['Arm'].arm_network != 'Disabled' + + assets.reset() + + # Build node trees + ArmoryExporter.import_traits = [] + make_logic.build() + make_world.build() + make_renderpath.build() + + # Export scene data + assets.embedded_data = sorted(list(set(assets.embedded_data))) + physics_found = False + navigation_found = False + ui_found = False + network_found = False + ArmoryExporter.compress_enabled = state.is_publish and wrd.arm_asset_compression + ArmoryExporter.optimize_enabled = state.is_publish and wrd.arm_optimize_data + if not os.path.exists(build_dir + '/compiled/Assets'): + os.makedirs(build_dir + '/compiled/Assets') + + # Make all 'MESH' and 'EMPTY' objects visible to the depsgraph (we pass + # this to the exporter further below) with a temporary "zoo" collection + # in the current scene. We do this to ensure that (among other things) + # modifiers are applied to all exported objects. + export_coll = bpy.data.collections.new("export_coll") + bpy.context.scene.collection.children.link(export_coll) + export_coll_names = set(export_coll.all_objects.keys()) + for scene in bpy.data.scenes: + if scene == bpy.context.scene: + continue + for o in scene.collection.all_objects: + if o.type in ('MESH', 'EMPTY'): + if o.name not in export_coll_names: + export_coll.objects.link(o) + export_coll_names.add(o.name) + depsgraph = bpy.context.evaluated_depsgraph_get() + bpy.data.collections.remove(export_coll) # Destroy the "zoo" collection + + for scene in bpy.data.scenes: + if scene.arm_export: + ext = '.lz4' if ArmoryExporter.compress_enabled else '.arm' + asset_path = build_dir + '/compiled/Assets/' + arm.utils.safestr(scene.name) + ext + ArmoryExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) + if ArmoryExporter.export_physics: + physics_found = True + if ArmoryExporter.export_navigation: + navigation_found = True + if ArmoryExporter.export_ui: + ui_found = True + if ArmoryExporter.export_network: + network_found = True + assets.add(asset_path) + + if physics_found is False: # Disable physics if no rigid body is exported + export_physics = False + + if navigation_found is False: + export_navigation = False + + if ui_found is False: + export_ui = False + + if network_found == False: + export_network = False + + if wrd.arm_ui == 'Enabled': + export_ui = True + + if wrd.arm_network == 'Enabled': + export_network = True + + modules = [] + if wrd.arm_audio == 'Enabled': + modules.append('audio') + if export_physics: + modules.append('physics') + if export_navigation: + modules.append('navigation') + if export_ui: + modules.append('ui') + if export_network: + modules.append('network') + + defs = arm.utils.def_strings_to_array(wrd.world_defs) + cdefs = arm.utils.def_strings_to_array(wrd.compo_defs) + + if wrd.arm_verbose_output: + log.info('Exported modules: '+', '.join(modules)) + log.info('Shader flags: '+', '.join(defs)) + log.info('Compositor flags: '+', '.join(cdefs)) + log.info('Khafile flags: '+', '.join(assets.khafile_defs)) + + # Render path is configurable at runtime + has_config = wrd.arm_write_config or os.path.exists(arm.utils.get_fp() + '/Bundled/config.arm') + + # Write compiled.inc + shaders_path = build_dir + '/compiled/Shaders' + if not os.path.exists(shaders_path): + os.makedirs(shaders_path) + write_data.write_compiledglsl(defs + cdefs, make_variants=has_config) + + # Write referenced shader passes + if not os.path.isfile(build_dir + '/compiled/Shaders/shader_datas.arm') or state.last_world_defs != wrd.world_defs: + res = {'shader_datas': []} + + for ref in assets.shader_passes: + # Ensure shader pass source exists + if not os.path.exists(raw_shaders_path + '/' + ref): + continue + assets.shader_passes_assets[ref] = [] + compile_shader_pass(res, raw_shaders_path, ref, defs + cdefs, make_variants=has_config) + + # Workaround to also export non-material world shaders + res['shader_datas'] += make_world.shader_datas + + if rpdat.arm_lens or rpdat.arm_lut: + for shader_pass in res["shader_datas"]: + for context in shader_pass["contexts"]: + for texture_unit in context["texture_units"]: + # Lens Texture + if rpdat.arm_lens_texture != '' and rpdat.arm_lens_texture != 'lenstexture.jpg' and "link" in texture_unit and texture_unit["link"] == "$lenstexture.jpg": + texture_unit["link"] = f"${rpdat.arm_lens_texture}" + # LUT Colorgrading + if rpdat.arm_lut_texture != '' and rpdat.arm_lut_texture != 'luttexture.jpg' and "link" in texture_unit and texture_unit["link"] == "$luttexture.jpg": + texture_unit["link"] = f"${rpdat.arm_lut_texture}" + + arm.utils.write_arm(shaders_path + '/shader_datas.arm', res) + + if wrd.arm_debug_console and rpdat.rp_renderer == 'Deferred': + # Copy deferred shader so that it can include compiled.inc + line_deferred_src = os.path.join(sdk_path, 'armory', 'Shaders', 'debug_draw', 'line_deferred.frag.glsl') + line_deferred_dst = os.path.join(shaders_path, 'line_deferred.frag.glsl') + shutil.copyfile(line_deferred_src, line_deferred_dst) + + for ref in assets.shader_passes: + for s in assets.shader_passes_assets[ref]: + assets.add_shader(shaders_path + '/' + s + '.glsl') + for file in assets.shaders_external: + name = file.split('/')[-1].split('\\')[-1] + target = build_dir + '/compiled/Shaders/' + name + if not os.path.exists(target): + shutil.copy(file, target) + state.last_world_defs = wrd.world_defs + + # Reset path + os.chdir(fp) + + # Copy std shaders + if not os.path.isdir(build_dir + '/compiled/Shaders/std'): + shutil.copytree(raw_shaders_path + 'std', build_dir + '/compiled/Shaders/std') + + # Write config.arm + resx, resy = arm.utils.get_render_resolution(arm.utils.get_active_scene()) + if wrd.arm_write_config: + write_data.write_config(resx, resy) + + # Change project version (Build, Publish) + if (not state.is_play) and (wrd.arm_project_version_autoinc): + wrd.arm_project_version = arm.utils.change_version_project(wrd.arm_project_version) + + # Write khafile.js + write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, state.is_publish, ArmoryExporter.import_traits) + + # Write Main.hx - depends on write_khafilejs for writing number of assets + scene_name = arm.utils.get_project_scene_name() + write_data.write_mainhx(scene_name, resx, resy, state.is_play, state.is_publish) + if scene_name != state.last_scene or resx != state.last_resx or resy != state.last_resy: + wrd.arm_recompile = True + state.last_resx = resx + state.last_resy = resy + state.last_scene = scene_name + +def compile(assets_only=False): + wrd = bpy.data.worlds['Arm'] + fp = arm.utils.get_fp() + os.chdir(fp) + + node_path = arm.utils.get_node_path() + khamake_path = arm.utils.get_khamake_path() + cmd = [node_path, khamake_path] + + # Custom exporter + if state.target == "custom": + if len(wrd.arm_exporterlist) > 0: + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + if item.arm_project_target == 'custom' and item.arm_project_khamake != '': + for s in item.arm_project_khamake.split(' '): + cmd.append(s) + state.proc_build = run_proc(cmd, build_done) + else: + target_name = state.target + kha_target_name = arm.utils.get_kha_target(target_name) + if kha_target_name != '': + cmd.append(kha_target_name) + ffmpeg_path = arm.utils.get_ffmpeg_path() + if ffmpeg_path not in (None, ''): + cmd.append('--ffmpeg') + cmd.append(ffmpeg_path) # '"' + ffmpeg_path + '"' + + state.export_gapi = arm.utils.get_gapi() + cmd.append('-g') + cmd.append(state.export_gapi) + # Windows - Set Visual Studio Version + if state.target.startswith('windows'): + cmd.append('--visualstudio') + cmd.append(arm.utils_vs.version_to_khamake_id[wrd.arm_project_win_list_vs]) + + if arm.utils.get_legacy_shaders() or 'ios' in state.target: + if 'html5' in state.target or 'ios' in state.target: + pass + else: + cmd.append('--shaderversion') + cmd.append('110') + elif 'android' in state.target or 'html5' in state.target: + cmd.append('--shaderversion') + cmd.append('300') + else: + cmd.append('--shaderversion') + cmd.append('330') + + if '_VR' in wrd.world_defs: + cmd.append('--vr') + cmd.append('webvr') + + if arm.utils.get_pref_or_default('khamake_debug', False): + cmd.append('--debug') + + if arm.utils.get_rp().rp_renderer == 'Raytracer': + cmd.append('--raytrace') + cmd.append('dxr') + dxc_path = fp + '/HlslShaders/dxc.exe' + subprocess.Popen([dxc_path, '-Zpr', '-Fo', fp + '/Bundled/raytrace.cso', '-T', 'lib_6_3', fp + '/HlslShaders/raytrace.hlsl']).wait() + + if arm.utils.get_khamake_threads() != 1: + cmd.append('--parallelAssetConversion') + cmd.append(str(arm.utils.get_khamake_threads())) + + compilation_server = False + + cmd.append('--to') + if (kha_target_name == 'krom' and not state.is_publish) or (kha_target_name == 'html5' and not state.is_publish): + cmd.append(arm.utils.build_dir() + '/debug') + # Start compilation server + if kha_target_name == 'krom' and arm.utils.get_compilation_server() and not assets_only and wrd.arm_cache_build: + compilation_server = True + arm.lib.server.run_haxe(arm.utils.get_haxe_path()) + else: + cmd.append(arm.utils.build_dir()) + + if not wrd.arm_verbose_output: + cmd.append("--quiet") + + #Project needs to be compiled at least once + #before compilation server can work + if not os.path.exists(arm.utils.build_dir() + '/debug/krom/krom.js') and not state.is_publish: + state.proc_build = run_proc(cmd, build_done) + else: + if assets_only or compilation_server: + cmd.append('--nohaxe') + cmd.append('--noproject') + if len(wrd.arm_exporterlist) > 0: + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + if item.arm_project_khamake != "": + for s in item.arm_project_khamake.split(" "): + cmd.append(s) + state.proc_build = run_proc(cmd, assets_done if compilation_server else build_done) + if bpy.app.background: + if state.proc_build.returncode == 0: + build_success() + else: + log.error('Build failed') + +def build(target, is_play=False, is_publish=False, is_export=False): + global profile_time + profile_time = time.time() + + state.target = target + state.is_play = is_play + state.is_publish = is_publish + state.is_export = is_export + + # Save blend + if arm.utils.get_save_on_build(): + bpy.ops.wm.save_mainfile() + + log.clear(clear_warnings=True, clear_errors=True) + + # Set camera in active scene + active_scene = arm.utils.get_active_scene() + if active_scene.camera == None: + for o in active_scene.objects: + if o.type == 'CAMERA': + active_scene.camera = o + break + + # Get paths + sdk_path = arm.utils.get_sdk_path() + raw_shaders_path = sdk_path + '/armory/Shaders/' + + # Set dir + fp = arm.utils.get_fp() + os.chdir(fp) + + # Create directories + wrd = bpy.data.worlds['Arm'] + sources_path = 'Sources/' + arm.utils.safestr(wrd.arm_project_package) + if not os.path.exists(sources_path): + os.makedirs(sources_path) + + # Save external scripts edited inside Blender + write_texts = False + for text in bpy.data.texts: + if text.filepath != '' and text.is_dirty: + write_texts = True + break + if write_texts: + area = bpy.context.area + if area is not None: + old_type = area.type + area.type = 'TEXT_EDITOR' + for text in bpy.data.texts: + if text.filepath != '' and text.is_dirty and os.path.isfile(text.filepath): + area.spaces[0].text = text + bpy.ops.text.save() + area.type = old_type + + # Save internal Haxe scripts + for text in bpy.data.texts: + if text.filepath == '' and text.name[-3:] == '.hx': + with open('Sources/' + arm.utils.safestr(wrd.arm_project_package) + '/' + text.name, 'w', encoding='utf-8') as f: + f.write(text.as_string()) + + # Export data + export_data(fp, sdk_path) + + if state.target == 'html5': + w, h = arm.utils.get_render_resolution(arm.utils.get_active_scene()) + write_data.write_indexhtml(w, h, is_publish) + # Bundle files from include dir + if os.path.isdir('include'): + dest = '/html5/' if is_publish else '/debug/html5/' + for fn in glob.iglob(os.path.join('include', '**'), recursive=False): + shutil.copy(fn, arm.utils.build_dir() + dest + os.path.basename(fn)) + +def play_done(): + """Called if the player was stopped/terminated.""" + if state.proc_play is not None: + if state.proc_play.returncode != 0: + log.warn(f'Player exited code {state.proc_play.returncode}') + state.proc_play = None + state.redraw_ui = True + log.clear() + live_patch.stop() + +def assets_done(): + if state.proc_build == None: + return + result = state.proc_build.poll() + if result == 0: + # Connect to the compilation server + os.chdir(arm.utils.build_dir() + '/debug/') + cmd = [arm.utils.get_haxe_path(), '--connect', '6000', 'project-krom.hxml'] + state.proc_build = run_proc(cmd, compilation_server_done) + else: + state.proc_build = None + state.redraw_ui = True + log.error('Build failed, check console') + +def compilation_server_done(): + if state.proc_build == None: + return + result = state.proc_build.poll() + if result == 0: + if os.path.exists('krom/krom.js.temp'): + os.chmod('krom/krom.js', stat.S_IWRITE) + os.remove('krom/krom.js') + os.rename('krom/krom.js.temp', 'krom/krom.js') + build_done() + else: + state.proc_build = None + state.redraw_ui = True + log.error('Build failed, check console') + +def build_done(): + wrd = bpy.data.worlds['Arm'] + log.info('Finished in {:0.3f}s'.format(time.time() - profile_time)) + if log.num_warnings > 0: + log.print_warn(f'{log.num_warnings} warning{"s" if log.num_warnings > 1 else ""} occurred during compilation') + if state.proc_build is None: + return + result = state.proc_build.poll() + state.proc_build = None + state.redraw_ui = True + if result == 0: + bpy.data.worlds['Arm'].arm_recompile = False + build_success() + else: + log.error('Build failed, check console') + + +def runtime_to_target(): + wrd = bpy.data.worlds['Arm'] + if wrd.arm_runtime == 'Krom': + return 'krom' + return 'html5' + +def get_khajs_path(target): + if target == 'krom': + return arm.utils.build_dir() + '/debug/krom/krom.js' + return arm.utils.build_dir() + '/debug/html5/kha.js' + +def play(): + global scripts_mtime + wrd = bpy.data.worlds['Arm'] + + build(target=runtime_to_target(), is_play=True) + + khajs_path = get_khajs_path(state.target) + if not wrd.arm_cache_build or \ + not os.path.isfile(khajs_path) or \ + assets.khafile_defs_last != assets.khafile_defs or \ + state.last_target != state.target: + wrd.arm_recompile = True + + state.last_target = state.target + + # Trait sources modified + state.mod_scripts = [] + script_path = arm.utils.get_fp() + '/Sources/' + arm.utils.safestr(wrd.arm_project_package) + if os.path.isdir(script_path): + new_mtime = scripts_mtime + for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): + mtime = os.path.getmtime(fn) + if scripts_mtime < mtime: + arm.utils.fetch_script_props(fn) # Trait props + fn = fn.split('Sources/')[1] + fn = fn[:-3] #.hx + fn = fn.replace('/', '.') + state.mod_scripts.append(fn) + wrd.arm_recompile = True + if new_mtime < mtime: + new_mtime = mtime + scripts_mtime = new_mtime + if len(state.mod_scripts) > 0: # Trait props + arm.utils.fetch_trait_props() + + compile(assets_only=(not wrd.arm_recompile)) + +def build_success(): + log.clear() + wrd = bpy.data.worlds['Arm'] + + if state.is_play: + cmd = [] + width, height = arm.utils.get_render_resolution(arm.utils.get_active_scene()) + if wrd.arm_runtime == 'Browser': + os.chdir(arm.utils.get_fp()) + prefs = arm.utils.get_arm_preferences() + host = 'localhost' + t = threading.Thread(name='localserver', + target=arm.lib.server.run_tcp, + args=(prefs.html5_server_port, + prefs.html5_server_log), + daemon=True) + t.start() + build_dir = arm.utils.build_dir() + path = '{}/debug/html5/'.format(build_dir) + url = 'http://{}:{}/{}'.format(host, prefs.html5_server_port, path) + browser = webbrowser.get() + browsername = None + if hasattr(browser, "name"): + browsername = getattr(browser,'name') + elif hasattr(browser,"_name"): + browsername = getattr(browser,'_name') + envvar = 'ARMORY_PLAY_HTML5' + if envvar in os.environ: + envcmd = os.environ[envvar] + if len(envcmd) == 0: + log.warn(f"Your {envvar} environment variable is set to an empty string") + else: + tplstr = Template(envcmd).safe_substitute({ + 'host': host, + 'port': prefs.html5_server_port, + 'width': width, + 'height': height, + 'url': url, + 'path': path, + 'dir': build_dir, + 'browser': browsername + }) + cmd = re.split(' +', tplstr) + if len(cmd) == 0: + if browsername in (None, '', 'default'): + webbrowser.open(url) + return + cmd = [browsername, url] + elif wrd.arm_runtime == 'Krom': + if wrd.arm_live_patch: + live_patch.start() + open(arm.utils.get_fp_build() + '/debug/krom/krom.patch', 'w', encoding='utf-8').close() + krom_location, krom_path = arm.utils.krom_paths() + path = arm.utils.get_fp_build() + '/debug/krom' + path_resources = path + '-resources' + pid = os.getpid() + os.chdir(krom_location) + envvar = 'ARMORY_PLAY_KROM' + if envvar in os.environ: + envcmd = os.environ[envvar] + if len(envcmd) == 0: + log.warn(f"Your {envvar} environment variable is set to an empty string") + else: + tplstr = Template(envcmd).safe_substitute({ + 'pid': pid, + 'audio': wrd.arm_audio != 'Disabled', + 'location': krom_location, + 'krom_path': krom_path, + 'path': path, + 'resources': path_resources, + 'width': width, + 'height': height + }) + cmd = re.split(' +', tplstr) + if len(cmd) == 0: + cmd = [krom_path, path, path_resources] + if arm.utils.get_os() == 'win': + cmd.append('--consolepid') + cmd.append(str(pid)) + if wrd.arm_audio == 'Disabled': + cmd.append('--nosound') + try: + state.proc_play = run_proc(cmd, play_done) + except: + log.warn('Failed to start player') + if wrd.arm_runtime == 'Browser': + webbrowser.open(url) + + elif state.is_publish: + sdk_path = arm.utils.get_sdk_path() + target_name = arm.utils.get_kha_target(state.target) + files_path = os.path.join(arm.utils.get_fp_build(), target_name) + + if target_name in ('html5', 'krom') and wrd.arm_minify_js: + # Minify JS + minifier_path = sdk_path + '/lib/armory_tools/uglifyjs/bin/uglifyjs' + if target_name == 'html5': + jsfile = files_path + '/kha.js' + else: + jsfile = files_path + '/krom.js' + args = [arm.utils.get_node_path(), minifier_path, jsfile, '-o', jsfile] + proc = subprocess.Popen(args) + proc.wait() + + if target_name == 'krom': + # Copy Krom binaries + if state.target == 'krom-windows': + gapi = state.export_gapi + ext = '' if gapi == 'direct3d11' else '_' + gapi + krom_location = sdk_path + '/Krom/Krom' + ext + '.exe' + shutil.copy(krom_location, files_path + '/Krom.exe') + krom_exe = arm.utils.safestr(wrd.arm_project_name) + '.exe' + os.rename(files_path + '/Krom.exe', files_path + '/' + krom_exe) + elif state.target == 'krom-linux': + krom_location = sdk_path + '/Krom/Krom' + shutil.copy(krom_location, files_path) + krom_exe = arm.utils.safestr(wrd.arm_project_name) + os.rename(files_path + '/Krom', files_path + '/' + krom_exe) + krom_exe = './' + krom_exe + else: + krom_location = sdk_path + '/Krom/Krom.app' + shutil.copytree(krom_location, files_path + '/Krom.app') + game_files = os.listdir(files_path) + for f in game_files: + f = files_path + '/' + f + if os.path.isfile(f): + shutil.move(f, files_path + '/Krom.app/Contents/MacOS') + krom_exe = arm.utils.safestr(wrd.arm_project_name) + '.app' + os.rename(files_path + '/Krom.app', files_path + '/' + krom_exe) + + # Rename + ext = state.target.split('-')[-1] # krom-windows + new_files_path = files_path + '-' + ext + os.rename(files_path, new_files_path) + files_path = new_files_path + + if target_name == 'html5': + project_path = files_path + print('Exported HTML5 package to ' + project_path) + elif target_name.startswith('ios') or target_name.startswith('osx'): # TODO: to macos + project_path = files_path + '-build' + print('Exported XCode project to ' + project_path) + elif target_name.startswith('windows'): + project_path = files_path + '-build' + vs_info = arm.utils_vs.get_supported_version(wrd.arm_project_win_list_vs) + print(f'Exported {vs_info["name"]} project to {project_path}') + elif target_name.startswith('android'): + project_name = arm.utils.safesrc(wrd.arm_project_name + '-' + wrd.arm_project_version) + project_path = os.path.join(files_path + '-build', project_name) + print('Exported Android Studio project to ' + project_path) + elif target_name.startswith('krom'): + project_path = files_path + print('Exported Krom package to ' + project_path) + else: + project_path = files_path + '-build' + print('Exported makefiles to ' + project_path) + + if not bpy.app.background and arm.utils.get_arm_preferences().open_build_directory: + arm.utils.open_folder(project_path) + + # Android build APK + if target_name.startswith('android'): + if (arm.utils.get_project_android_build_apk()) and (len(arm.utils.get_android_sdk_root_path()) > 0): + print("\nBuilding APK") + # Check settings + path_sdk = arm.utils.get_android_sdk_root_path() + if len(path_sdk) > 0: + # Check Environment Variables - ANDROID_SDK_ROOT + if os.getenv('ANDROID_SDK_ROOT') is None: + # Set value from settings + os.environ['ANDROID_SDK_ROOT'] = path_sdk + else: + project_path = '' + + # Build start + if len(project_path) > 0: + os.chdir(project_path) # set work folder + if arm.utils.get_os_is_windows(): + state.proc_publish_build = run_proc(os.path.join(project_path, "gradlew.bat assembleDebug"), done_gradlew_build) + else: + cmd = shlex.split(os.path.join(project_path, "gradlew assembleDebug")) + state.proc_publish_build = run_proc(cmd, done_gradlew_build) + else: + print('\nBuilding APK Warning: ANDROID_SDK_ROOT is not specified in environment variables and "Android SDK Path" setting is not specified in preferences: \n- If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender;\n- If you specify the setting "Android SDK Path" in the preferences, then repeat operation "Publish"') + + # HTML5 After Publish + if target_name.startswith('html5'): + if len(arm.utils.get_html5_copy_path()) > 0 and (wrd.arm_project_html5_copy): + project_name = arm.utils.safesrc(wrd.arm_project_name +'-'+ wrd.arm_project_version) + dst = os.path.join(arm.utils.get_html5_copy_path(), project_name) + if os.path.exists(dst): + shutil.rmtree(dst) + try: + shutil.copytree(project_path, dst) + print("Copied files to " + dst) + except OSError as exc: + if exc.errno == errno.ENOTDIR: + shutil.copy(project_path, dst) + else: raise + if len(arm.utils.get_link_web_server()) and (wrd.arm_project_html5_start_browser): + link_html5_app = arm.utils.get_link_web_server() +'/'+ project_name + print("Running a browser with a link " + link_html5_app) + webbrowser.open(link_html5_app) + + # Windows After Publish + if target_name.startswith('windows') and wrd.arm_project_win_build != 'nothing' and arm.utils.get_os_is_windows(): + project_name = arm.utils.safesrc(wrd.arm_project_name + '-' + wrd.arm_project_version) + + # Open in Visual Studio + if wrd.arm_project_win_build == 'open': + print('\nOpening in Visual Studio: ' + arm.utils_vs.get_sln_path()) + _ = arm.utils_vs.open_project_in_vs(wrd.arm_project_win_list_vs) + + # Compile + elif wrd.arm_project_win_build.startswith('compile'): + if wrd.arm_project_win_build == 'compile': + print('\nCompiling project ' + arm.utils_vs.get_vcxproj_path()) + elif wrd.arm_project_win_build == 'compile_and_run': + print('\nCompiling and running project ' + arm.utils_vs.get_vcxproj_path()) + + success = arm.utils_vs.enable_vsvars_env(wrd.arm_project_win_list_vs, done_vs_vars) + if not success: + state.redraw_ui = True + log.error('Compile failed, check console') + + +def done_gradlew_build(): + if state.proc_publish_build is None: + return + result = state.proc_publish_build.poll() + if result == 0: + state.proc_publish_build = None + + wrd = bpy.data.worlds['Arm'] + path_apk = os.path.join(arm.utils.get_fp_build(), arm.utils.get_kha_target(state.target)) + project_name = arm.utils.safesrc(wrd.arm_project_name +'-'+ wrd.arm_project_version) + path_apk = os.path.join(path_apk + '-build', project_name, 'app', 'build', 'outputs', 'apk', 'debug') + + print("\nBuild APK to " + path_apk) + # Rename APK + apk_name = 'app-debug.apk' + file_name = os.path.join(path_apk, apk_name) + if wrd.arm_project_android_rename_apk: + apk_name = project_name + '.apk' + os.rename(file_name, os.path.join(path_apk, apk_name)) + file_name = os.path.join(path_apk, apk_name) + print("\nRename APK to " + apk_name) + # Copy APK + if wrd.arm_project_android_copy_apk: + shutil.copyfile(file_name, os.path.join(arm.utils.get_android_apk_copy_path(), apk_name)) + print("Copy APK to " + arm.utils.get_android_apk_copy_path()) + # Open directory with APK + if arm.utils.get_android_open_build_apk_directory(): + arm.utils.open_folder(path_apk) + # Open directory after copy APK + if arm.utils.get_android_apk_copy_open_directory(): + arm.utils.open_folder(arm.utils.get_android_apk_copy_path()) + # Running emulator + if wrd.arm_project_android_run_avd: + run_android_emulators(arm.utils.get_android_emulator_name()) + state.redraw_ui = True + else: + state.proc_publish_build = None + state.redraw_ui = True + os.environ['ANDROID_SDK_ROOT'] = '' + log.error('Building the APK failed, check console') + +def run_android_emulators(avd_name): + if len(avd_name.strip()) == 0: + return + print('\nRunning Emulator "'+ avd_name +'"') + path_file = arm.utils.get_android_emulator_file() + if len(path_file) > 0: + if arm.utils.get_os_is_windows(): + run_proc(path_file + " -avd "+ avd_name, None) + else: + cmd = shlex.split(path_file + " -avd "+ avd_name) + run_proc(cmd, None) + else: + print('Update List Emulators Warning: File "'+ path_file +'" not found. Check that the variable ANDROID_SDK_ROOT is correct in environment variables or in "Android SDK Path" setting: \n- If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender;\n- If you specify the setting "Android SDK Path", then repeat operation "Publish"') + + +def done_vs_vars(): + if state.proc_publish_build is None: + return + + result = state.proc_publish_build.poll() + if result == 0: + state.proc_publish_build = None + + wrd = bpy.data.worlds['Arm'] + success = arm.utils_vs.compile_in_vs(wrd.arm_project_win_list_vs, done_vs_build) + if not success: + state.proc_publish_build = None + state.redraw_ui = True + log.error('Compile failed, check console') + else: + state.proc_publish_build = None + state.redraw_ui = True + log.error('Compile failed, check console') + + +def done_vs_build(): + if state.proc_publish_build is None: + return + + result = state.proc_publish_build.poll() + if result == 0: + state.proc_publish_build = None + + wrd = bpy.data.worlds['Arm'] + project_path = os.path.join(arm.utils.get_fp_build(), arm.utils.get_kha_target(state.target)) + '-build' + if wrd.arm_project_win_build_arch == 'x64': + path = os.path.join(project_path, 'x64', wrd.arm_project_win_build_mode) + else: + path = os.path.join(project_path, wrd.arm_project_win_build_mode) + print('\nCompilation completed in ' + path) + # Run + if wrd.arm_project_win_build == 'compile_and_run': + # Copying the executable file + res_path = os.path.join(arm.utils.get_fp_build(), arm.utils.get_kha_target(state.target)) + file_name = arm.utils.safesrc(wrd.arm_project_name +'-'+ wrd.arm_project_version) + '.exe' + print('\nCopy the executable file from ' + path + ' to ' + res_path) + shutil.copyfile(os.path.join(path, file_name), os.path.join(res_path, file_name)) + path = res_path + # Run project + cmd = os.path.join('"' + res_path, file_name + '"') + print('Run the executable file to ' + cmd) + os.chdir(res_path) # set work folder + subprocess.Popen(cmd, shell=True) + # Open Build Directory + if wrd.arm_project_win_build_open: + arm.utils.open_folder(path) + state.redraw_ui = True + else: + state.proc_publish_build = None + state.redraw_ui = True + log.error('Compile failed, check console') + +def clean(): + os.chdir(arm.utils.get_fp()) + wrd = bpy.data.worlds['Arm'] + + # Remove build and compiled data + try: + if os.path.isdir(arm.utils.build_dir()): + shutil.rmtree(arm.utils.build_dir(), onerror=remove_readonly) + if os.path.isdir(arm.utils.get_fp() + '/build'): # Kode Studio build dir + shutil.rmtree(arm.utils.get_fp() + '/build', onerror=remove_readonly) + except: + print('Armory Warning: Some files in the build folder are locked') + + # Remove compiled nodes + pkg_dir = arm.utils.safestr(wrd.arm_project_package).replace('.', '/') + nodes_path = 'Sources/' + pkg_dir + '/node/' + if os.path.isdir(nodes_path): + shutil.rmtree(nodes_path, onerror=remove_readonly) + + # Remove khafile/Main.hx + if os.path.isfile('khafile.js'): + os.remove('khafile.js') + if os.path.isfile('Sources/Main.hx'): + os.remove('Sources/Main.hx') + + # Remove Sources/ dir if empty + if os.path.exists('Sources/' + pkg_dir) and os.listdir('Sources/' + pkg_dir) == []: + shutil.rmtree('Sources/' + pkg_dir, onerror=remove_readonly) + if os.path.exists('Sources') and os.listdir('Sources') == []: + shutil.rmtree('Sources/', onerror=remove_readonly) + + # Remove Shape key Textures + if os.path.exists('MorphTargets/'): + shutil.rmtree('MorphTargets/', onerror=remove_readonly) + + # To recache signatures for batched materials + for mat in bpy.data.materials: + mat.signature = '' + mat.arm_cached = False + + # Restart compilation server + if arm.utils.get_compilation_server(): + arm.lib.server.kill_haxe() + + log.info('Project cleaned') diff --git a/blender/arm/make_logic.py b/blender/arm/make_logic.py new file mode 100644 index 0000000000..bd306d455d --- /dev/null +++ b/blender/arm/make_logic.py @@ -0,0 +1,371 @@ +import os +from typing import Optional, TextIO + +import bpy + +from arm.exporter import ArmoryExporter +import arm.log +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + arm.exporter = arm.reload_module(arm.exporter) + from arm.exporter import ArmoryExporter + arm.log = arm.reload_module(arm.log) + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +parsed_nodes = [] +parsed_ids = dict() # Sharing node data +function_nodes = dict() +function_node_outputs = dict() +group_name = '' + + +def get_logic_trees() -> list['arm.nodes_logic.ArmLogicTree']: + ar = [] + for node_group in bpy.data.node_groups: + if node_group.bl_idname == 'ArmLogicTreeType': + node_group.use_fake_user = True # Keep fake references for now + ar.append(node_group) + return ar + + +# Generating node sources +def build(): + os.chdir(arm.utils.get_fp()) + trees = get_logic_trees() + if len(trees) > 0: + # Make sure package dir exists + nodes_path = 'Sources/' + arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package).replace(".", "/") + "/node" + if not os.path.exists(nodes_path): + os.makedirs(nodes_path) + # Export node scripts + for tree in trees: + build_node_tree(tree) + +def build_node_tree(node_group: 'arm.nodes_logic.ArmLogicTree'): + global parsed_nodes + global parsed_ids + global function_nodes + global function_node_outputs + global group_name + parsed_nodes = [] + parsed_ids = dict() + function_nodes = dict() + function_node_outputs = dict() + root_nodes = get_root_nodes(node_group) + + pack_path = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + path = 'Sources/' + pack_path.replace('.', '/') + '/node/' + + group_name = arm.node_utils.get_export_tree_name(node_group, do_warn=True) + file = path + group_name + '.hx' + + if node_group.arm_cached and os.path.isfile(file): + return + + wrd = bpy.data.worlds['Arm'] + + with open(file, 'w', encoding="utf-8") as f: + f.write('package ' + pack_path + '.node;\n\n') + f.write('@:access(armory.logicnode.LogicNode)') + f.write('@:keep class ' + group_name + ' extends armory.logicnode.LogicTree {\n\n') + f.write('\tvar functionNodes:Map;\n\n') + f.write('\tvar functionOutputNodes:Map;\n\n') + f.write('\tpublic function new() {\n') + f.write('\t\tsuper();\n') + if wrd.arm_debug_console: + f.write('\t\tname = "' + group_name + '";\n') + f.write('\t\tthis.functionNodes = new Map();\n') + f.write('\t\tthis.functionOutputNodes = new Map();\n') + if arm.utils.is_livepatch_enabled(): + # Store a reference to this trait instance in Logictree.nodeTrees + f.write('\t\tvar nodeTrees = armory.logicnode.LogicTree.nodeTrees;\n') + f.write(f'\t\tif (nodeTrees.exists("{group_name}")) ' + '{\n') + f.write(f'\t\t\tnodeTrees["{group_name}"].push(this);\n') + f.write('\t\t} else {\n') + f.write(f'\t\t\tnodeTrees["{group_name}"] = cast [this];\n') + f.write('\t\t}\n') + f.write('\t\tnotifyOnRemove(() -> { nodeTrees.remove("' + group_name + '"); });\n') + f.write('\t\tnotifyOnAdd(add);\n') + f.write('\t}\n\n') + f.write('\toverride public function add() {\n') + for node in root_nodes: + build_node(node, f) + f.write('\t}\n') + + # Create node functions + for node_name in function_nodes: + node = function_nodes[node_name] + function_name = node.function_name + f.write('\n\tpublic function ' + function_name + '(') + for i in range(0, len(node.outputs) - 1): + if i != 0: f.write(', ') + f.write('arg' + str(i) + ':Dynamic') + f.write(') {\n') + f.write('\t\tvar functionNode = this.functionNodes["' + node_name + '"];\n') + f.write('\t\tfunctionNode.args = [];\n') + for i in range(0, len(node.outputs) - 1): + f.write('\t\tfunctionNode.args.push(arg' + str(i) + ');\n') + f.write('\t\tfunctionNode.run(0);\n') + if function_node_outputs.get(function_name) != None: + f.write('\t\treturn this.functionOutputNodes["' + function_node_outputs[function_name] + '"].result;\n') + f.write('\t}\n\n') + f.write('}') + node_group.arm_cached = True + + +def build_node_group_tree(node_group: 'arm.nodes_logic.ArmLogicTree', f: TextIO, group_node_name: str): + """Builds the given node tree as a node group""" + + root_nodes = get_root_nodes(node_group) + + group_input_name = "" + group_output_name = "" + tree_name = arm.node_utils.get_export_tree_name(node_group) + + # Get names of group input and out nodes if they exist + for node in node_group.nodes: + if node.bl_idname == 'LNGroupInputsNode': + group_input_name = group_node_name + '_' + tree_name + arm.node_utils.get_export_node_name(node) + if node.bl_idname == 'LNGroupOutputsNode': + group_output_name = group_node_name + '_' + tree_name + arm.node_utils.get_export_node_name(node) + + for node in root_nodes: + build_node(node, f, group_node_name + '_' + tree_name) + node_group.arm_cached = True + return group_input_name, group_output_name + + +def build_node(node: bpy.types.Node, f: TextIO, name_prefix: str = None) -> Optional[str]: + """Builds the given node and returns its name. f is an opened file object.""" + global parsed_nodes + global parsed_ids + + use_live_patch = arm.utils.is_livepatch_enabled() + + link_group = False + + if node.type == 'REROUTE': + if len(node.inputs) > 0 and len(node.inputs[0].links) > 0: + return build_node(node.inputs[0].links[0].from_node, f) + else: + return None + + # Get node name + name = arm.node_utils.get_export_node_name(node) + if name_prefix is not None: + name = name_prefix + name + + # Check and parse group nodes if they exist + if node.bl_idname == 'LNCallGroupNode': + prop = node.group_tree + if prop is not None: + group_input_name, group_output_name = build_node_group_tree(prop, f, name) + link_group = True + + # Link tree variable nodes using IDs + if node.arm_logic_id != '': + parse_id = node.arm_logic_id + if name_prefix is not None: + parse_id = name_prefix + parse_id + if parse_id in parsed_ids: + return parsed_ids[parse_id] + parsed_ids[parse_id] = name + + # Check if node already exists + if name in parsed_nodes: + # Check if node groups were parsed + if not link_group: + return name + else: + return group_output_name + + parsed_nodes.append(name) + + if not link_group: + # Create node + node_type = node.bl_idname[2:] # Discard 'LN' prefix + f.write('\t\tvar ' + name + ' = new armory.logicnode.' + node_type + '(this);\n') + + # Handle Function Nodes if no node groups exist + if node_type == 'FunctionNode' and name_prefix is None: + f.write('\t\tthis.functionNodes.set("' + name + '", ' + name + ');\n') + function_nodes[name] = node + elif node_type == 'FunctionOutputNode' and name_prefix is None: + f.write('\t\tthis.functionOutputNodes.set("' + name + '", ' + name + ');\n') + # Index function output name by corresponding function name + function_node_outputs[node.function_name] = name + wrd = bpy.data.worlds['Arm'] + + # Watch in debug console + if node.arm_watch and wrd.arm_debug_console: + f.write('\t\t' + name + '.name = "' + name[1:] + '";\n') + f.write('\t\t' + name + '.watch(true);\n') + + elif use_live_patch: + f.write('\t\t' + name + '.name = "' + name[1:] + '";\n') + f.write(f'\t\tthis.nodes["{name[1:]}"] = {name};\n') + + # Properties + for prop_py_name, prop_hx_name in arm.node_utils.get_haxe_property_names(node): + prop = arm.node_utils.haxe_format_prop_value(node, prop_py_name) + f.write('\t\t' + name + '.' + prop_hx_name + ' = ' + prop + ';\n') + + # Avoid unnecessary input/output array resizes + f.write(f'\t\t{name}.preallocInputs({len(node.inputs)});\n') + f.write(f'\t\t{name}.preallocOutputs({len(node.outputs)});\n') + + # Create inputs + if link_group: + # Replace Call Node Group Node name with Group Input Node name + name = group_input_name + for idx, inp in enumerate(node.inputs): + # True if the input is connected to a unlinked reroute + # somewhere down the reroute line + unconnected = False + + # Is linked -> find the connected node + if inp.is_linked: + n = inp.links[0].from_node + socket = inp.links[0].from_socket + + # Follow reroutes first + while n.type == "REROUTE": + if len(n.inputs) == 0 or not n.inputs[0].is_linked: + unconnected = True + break + + socket = n.inputs[0].links[0].from_socket + n = n.inputs[0].links[0].from_node + if not unconnected: + # Ignore warnings if "Any" socket type is used + if inp.bl_idname != 'ArmAnySocket' and socket.bl_idname != 'ArmAnySocket': + if (inp.bl_idname == 'ArmNodeSocketAction' and socket.bl_idname != 'ArmNodeSocketAction') or \ + (socket.bl_idname == 'ArmNodeSocketAction' and inp.bl_idname != 'ArmNodeSocketAction'): + arm.log.warn(f'Sockets do not match in logic node tree "{group_name}": node "{node.name}", socket "{inp.name}"') + + inp_name = build_node(n, f, name_prefix) + for i in range(0, len(n.outputs)): + if n.outputs[i] == socket: + inp_from = i + from_type = arm.node_utils.get_socket_type(socket) + break + + # Not linked -> create node with default values + else: + inp_name = build_default_node(inp) + inp_from = 0 + from_type = arm.node_utils.get_socket_type(inp) + + # The input is linked to a reroute, but the reroute is unlinked + if unconnected: + inp_name = build_default_node(inp) + inp_from = 0 + from_type = arm.node_utils.get_socket_type(inp) + + # Add input + f.write(f'\t\t{"var __link = " if use_live_patch else ""}armory.logicnode.LogicNode.addLink({inp_name}, {name}, {inp_from}, {idx});\n') + if use_live_patch: + to_type = arm.node_utils.get_socket_type(inp) + f.write(f'\t\t__link.fromType = "{from_type}";\n') + f.write(f'\t\t__link.toType = "{to_type}";\n') + f.write(f'\t\t__link.toValue = {arm.node_utils.haxe_format_socket_val(inp.get_default_value())};\n') + + # Create outputs + if link_group: + # Replace Call Node Group Node name with Group Output Node name + name = group_output_name + for idx, out in enumerate(node.outputs): + # Linked outputs are already handled after iterating over inputs + # above, so only unconnected outputs are handled here + if not out.is_linked: + f.write(f'\t\t{"var __link = " if use_live_patch else ""}armory.logicnode.LogicNode.addLink({name}, {build_default_node(out)}, {idx}, 0);\n') + if use_live_patch: + out_type = arm.node_utils.get_socket_type(out) + f.write(f'\t\t__link.fromType = "{out_type}";\n') + f.write(f'\t\t__link.toType = "{out_type}";\n') + f.write(f'\t\t__link.toValue = {arm.node_utils.haxe_format_socket_val(out.get_default_value())};\n') + + return name + +# Expects an output socket +# It first checks all outgoing links for non-reroute nodes and adds them to a list +# Then it recursively checks all the discoverey reroute nodes +# Returns all non reroute nodes which are directly or indirectly connected to this output. +def collect_nodes_from_output(out, f): + outputs = [] + reroutes = [] + # skipped if there are no links + for l in out.links: + n = l.to_node + if n.type == 'REROUTE': + # collect all rerouts and process them later + reroutes.append(n) + else: + # immediatly add the current node + outputs.append(build_node(n, f)) + for reroute in reroutes: + for o in reroute.outputs: + outputs = outputs + collect_nodes_from_output(o, f) + return outputs + +def get_root_nodes(node_group): + roots = [] + for node in node_group.nodes: + if node.bl_idname == 'NodeUndefined': + arm.log.warn('Undefined logic nodes in ' + node_group.name) + return [] + if node.type == 'FRAME': + continue + linked = False + for out in node.outputs: + if out.is_linked: + linked = True + break + if not linked: # Assume node with no connected outputs as roots + roots.append(node) + return roots + +def build_default_node(inp: bpy.types.NodeSocket): + """Creates a new node to give a not connected input socket a value""" + is_custom_socket = isinstance(inp, arm.logicnode.arm_sockets.ArmCustomSocket) + + if is_custom_socket: + # ArmCustomSockets need to implement get_default_value() + default_value = inp.get_default_value() + else: + if hasattr(inp, 'default_value'): + default_value = inp.default_value + else: + default_value = None + + default_value = arm.node_utils.haxe_format_socket_val(default_value, array_outer_brackets=False) + + inp_type = arm.node_utils.get_socket_type(inp) + + if inp_type == 'VECTOR': + return f'new armory.logicnode.VectorNode(this, {default_value})' + elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion. + return f'new armory.logicnode.RotationNode(this, {default_value})' + elif inp_type in ('RGB', 'RGBA'): + return f'new armory.logicnode.ColorNode(this, {default_value})' + elif inp_type == 'VALUE': + return f'new armory.logicnode.FloatNode(this, {default_value})' + elif inp_type == 'INT': + return f'new armory.logicnode.IntegerNode(this, {default_value})' + elif inp_type == 'BOOLEAN': + return f'new armory.logicnode.BooleanNode(this, {default_value})' + elif inp_type == 'STRING': + return f'new armory.logicnode.StringNode(this, {default_value})' + elif inp_type == 'NONE': + return 'new armory.logicnode.NullNode(this)' + elif inp_type == 'OBJECT': + return f'new armory.logicnode.ObjectNode(this, {default_value})' + elif is_custom_socket: + return f'new armory.logicnode.DynamicNode(this, {default_value})' + else: + return 'new armory.logicnode.NullNode(this)' diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py new file mode 100644 index 0000000000..1e1d296b05 --- /dev/null +++ b/blender/arm/make_renderpath.py @@ -0,0 +1,454 @@ +from typing import Callable, Optional + +import os +import bpy + +import arm.api +import arm.assets as assets +import arm.log as log +import arm.make_state as state +import arm.utils + +if arm.is_reload(__name__): + arm.api = arm.reload_module(arm.api) + assets = arm.reload_module(assets) + log = arm.reload_module(log) + state = arm.reload_module(state) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +callback: Optional[Callable[[], None]] = None + + +def add_world_defs(): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + # Screen-space ray-traced shadows + if rpdat.arm_ssrs: + wrd.world_defs += '_SSRS' + assets.add_khafile_def('rp_ssrs') + + if rpdat.arm_micro_shadowing: + wrd.world_defs += '_MicroShadowing' + + if rpdat.arm_two_sided_area_light: + wrd.world_defs += '_TwoSidedAreaLight' + + # Store contexts + if rpdat.rp_hdr == False: + wrd.world_defs += '_LDR' + + if wrd.arm_light_ies_texture != '': + wrd.world_defs += '_LightIES' + assets.add_embedded_data('iestexture.png') + + if wrd.arm_light_clouds_texture != '': + wrd.world_defs += '_LightClouds' + assets.add_embedded_data('cloudstexture.png') + + if rpdat.rp_renderer == 'Deferred': + assets.add_khafile_def('arm_deferred') + wrd.world_defs += '_Deferred' + + # GI + voxelao = False + has_voxels = arm.utils.voxel_support() + if has_voxels and rpdat.arm_material_model == 'Full': + if rpdat.rp_voxels: + voxelao = True + # Shadows + if rpdat.rp_shadows: + wrd.world_defs += '_ShadowMap' + if rpdat.rp_shadowmap_cascades != '1': + wrd.world_defs += '_CSM' + assets.add_khafile_def('arm_csm') + if rpdat.rp_shadowmap_atlas: + assets.add_khafile_def('arm_shadowmap_atlas') + wrd.world_defs += '_ShadowMapAtlas' + if rpdat.rp_shadowmap_atlas_single_map: + assets.add_khafile_def('arm_shadowmap_atlas_single_map') + wrd.world_defs += '_SingleAtlas' + assets.add_khafile_def('rp_shadowmap_atlas_max_size_point={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_point))) + assets.add_khafile_def('rp_shadowmap_atlas_max_size_spot={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_spot))) + assets.add_khafile_def('rp_shadowmap_atlas_max_size_sun={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_sun))) + assets.add_khafile_def('rp_shadowmap_atlas_max_size={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size))) + + assets.add_khafile_def('rp_max_lights_cluster={0}'.format(int(rpdat.rp_max_lights_cluster))) + assets.add_khafile_def('rp_max_lights={0}'.format(int(rpdat.rp_max_lights))) + if rpdat.rp_shadowmap_atlas_lod: + assets.add_khafile_def('arm_shadowmap_atlas_lod') + assets.add_khafile_def('rp_shadowmap_atlas_lod_subdivisions={0}'.format(int(rpdat.rp_shadowmap_atlas_lod_subdivisions))) + # SS + if rpdat.rp_ssgi == 'RTAO': + wrd.world_defs += '_RTGI' + if rpdat.arm_ssgi_rays == '9': + wrd.world_defs += '_SSGICone9' + if rpdat.rp_autoexposure: + wrd.world_defs += '_AutoExposure' + + has_voxels = arm.utils.voxel_support() + if rpdat.rp_voxels and has_voxels and rpdat.arm_material_model == 'Full': + wrd.world_defs += '_VoxelCones' + rpdat.arm_voxelgi_cones + if rpdat.arm_voxelgi_revoxelize: + assets.add_khafile_def('arm_voxelgi_revox') + if rpdat.arm_voxelgi_camera: + wrd.world_defs += '_VoxelGICam' + if rpdat.arm_voxelgi_temporal: + assets.add_khafile_def('arm_voxelgi_temporal') + wrd.world_defs += '_VoxelGITemporal' + wrd.world_defs += '_VoxelAOvar' # Write a shader variant + if rpdat.arm_voxelgi_shadows: + wrd.world_defs += '_VoxelShadow' + if rpdat.arm_voxelgi_occ == 0.0: + wrd.world_defs += '_VoxelAONoTrace' + + if arm.utils.get_legacy_shaders() or 'ios' in state.target: + wrd.world_defs += '_Legacy' + assets.add_khafile_def('arm_legacy') + + # Light defines + point_lights = 0 + for bo in bpy.data.objects: # TODO: temp + if bo.arm_export and bo.type == 'LIGHT': + light = bo.data + if light.type == 'AREA' and '_LTC' not in wrd.world_defs: + point_lights += 1 + wrd.world_defs += '_LTC' + assets.add_khafile_def('arm_ltc') + if light.type == 'SUN' and '_Sun' not in wrd.world_defs: + wrd.world_defs += '_Sun' + if light.type == 'POINT' or light.type == 'SPOT': + point_lights += 1 + if light.type == 'SPOT' and '_Spot' not in wrd.world_defs: + wrd.world_defs += '_Spot' + assets.add_khafile_def('arm_spot') + + if not rpdat.rp_shadowmap_atlas: + if point_lights == 1: + wrd.world_defs += '_SinglePoint' + elif point_lights > 1: + wrd.world_defs += '_Clusters' + assets.add_khafile_def('arm_clusters') + else: + wrd.world_defs += '_SMSizeUniform' + wrd.world_defs += '_Clusters' + assets.add_khafile_def('arm_clusters') + + if '_Rad' in wrd.world_defs and '_Brdf' not in wrd.world_defs: + wrd.world_defs += '_Brdf' + +def build(): + rpdat = arm.utils.get_rp() + project_path = arm.utils.get_fp() + + if rpdat.rp_driver != 'Armory' and arm.api.drivers[rpdat.rp_driver]['make_rpath'] != None: + arm.api.drivers[rpdat.rp_driver]['make_rpath']() + return + + assets_path = arm.utils.get_sdk_path() + '/armory/Assets/' + wrd = bpy.data.worlds['Arm'] + + wrd.compo_defs = '' + + add_world_defs() + + mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' + if not mobile_mat: + # Always include + assets.add(assets_path + 'brdf.png') + assets.add_embedded_data('brdf.png') + + if rpdat.rp_hdr: + assets.add_khafile_def('rp_hdr') + + assets.add_khafile_def('rp_renderer={0}'.format(rpdat.rp_renderer)) + if rpdat.rp_depthprepass: + assets.add_khafile_def('rp_depthprepass') + + if rpdat.rp_shadows: + assets.add_khafile_def('rp_shadowmap') + assets.add_khafile_def('rp_shadowmap_cascade={0}'.format(arm.utils.get_cascade_size(rpdat))) + assets.add_khafile_def('rp_shadowmap_cube={0}'.format(rpdat.rp_shadowmap_cube)) + + if arm.utils.get_gapi() == 'metal': + assets.add_shader_pass('clear_color_depth_pass') + assets.add_shader_pass('clear_color_pass') + assets.add_shader_pass('clear_depth_pass') + + assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) + if rpdat.rp_background == 'World': + if '_EnvClouds' in wrd.world_defs: + assets.add(assets_path + 'clouds_base.raw') + assets.add_embedded_data('clouds_base.raw') + assets.add(assets_path + 'clouds_detail.raw') + assets.add_embedded_data('clouds_detail.raw') + assets.add(assets_path + 'clouds_map.png') + assets.add_embedded_data('clouds_map.png') + + assets.add_shader_pass('copy_pass') + if rpdat.rp_renderer == 'Forward': + assets.add_khafile_def('rp_forward') + + if rpdat.rp_render_to_texture: + assets.add_khafile_def('rp_render_to_texture') + + if rpdat.rp_renderer == 'Forward' and not rpdat.rp_compositornodes: + assets.add_shader_pass('copy_pass') + + if rpdat.rp_compositornodes: + assets.add_khafile_def('rp_compositornodes') + compo_depth = False + if rpdat.arm_tonemap != 'Off': + wrd.compo_defs = '_CTone' + rpdat.arm_tonemap + if rpdat.rp_antialiasing == 'FXAA': + wrd.compo_defs += '_CFXAA' + if rpdat.arm_letterbox: + wrd.compo_defs += '_CLetterbox' + if rpdat.arm_distort: + wrd.compo_defs += '_CDistort' + if rpdat.arm_grain: + wrd.compo_defs += '_CGrain' + if rpdat.arm_sharpen: + wrd.compo_defs += '_CSharpen' + if bpy.data.scenes[0].view_settings.exposure != 0.0: + wrd.compo_defs += '_CExposure' + if rpdat.arm_fog: + wrd.compo_defs += '_CFog' + compo_depth = True + + focus_distance = 0.0 + if len(bpy.data.cameras) > 0 and bpy.data.cameras[0].dof.use_dof: + focus_distance = bpy.data.cameras[0].dof.focus_distance + + if focus_distance > 0.0: + wrd.compo_defs += '_CDOF' + compo_depth = True + if rpdat.arm_fisheye: + wrd.compo_defs += '_CFishEye' + if rpdat.arm_vignette: + wrd.compo_defs += '_CVignette' + if rpdat.arm_lensflare: + wrd.compo_defs += '_CGlare' + compo_depth = True + if rpdat.arm_lens: + if os.path.isfile(project_path + '/Bundled/' + rpdat.arm_lens_texture): + wrd.compo_defs += '_CLensTex' + assets.add_embedded_data(rpdat.arm_lens_texture) + if rpdat.arm_lens_texture_masking: + wrd.compo_defs += '_CLensTexMasking' + else: + log.warn('Filepath for Lens texture is invalid.') + if rpdat.arm_lut: + if os.path.isfile(project_path + '/Bundled/' + rpdat.arm_lut_texture): + wrd.compo_defs += '_CLUT' + assets.add_embedded_data(rpdat.arm_lut_texture) + else: + log.warn('Filepath for LUT texture is invalid.') + if '_CDOF' in wrd.compo_defs or '_CFXAA' in wrd.compo_defs or '_CSharpen' in wrd.compo_defs: + wrd.compo_defs += '_CTexStep' + if '_CDOF' in wrd.compo_defs or '_CFog' in wrd.compo_defs or '_CGlare' in wrd.compo_defs: + wrd.compo_defs += '_CCameraProj' + if compo_depth: + wrd.compo_defs += '_CDepth' + assets.add_khafile_def('rp_compositordepth') + if rpdat.rp_pp: + wrd.compo_defs += '_CPostprocess' + + assets.add_shader_pass('compositor_pass') + + assets.add_khafile_def('rp_antialiasing={0}'.format(rpdat.rp_antialiasing)) + + if rpdat.rp_antialiasing == 'SMAA' or rpdat.rp_antialiasing == 'TAA': + assets.add_shader_pass('smaa_edge_detect') + assets.add_shader_pass('smaa_blend_weight') + assets.add_shader_pass('smaa_neighborhood_blend') + assets.add(assets_path + 'smaa_area.png') + assets.add(assets_path + 'smaa_search.png') + assets.add_embedded_data('smaa_area.png') + assets.add_embedded_data('smaa_search.png') + wrd.world_defs += '_SMAA' + if rpdat.rp_antialiasing == 'TAA': + assets.add_shader_pass('taa_pass') + assets.add_shader_pass('copy_pass') + + if rpdat.rp_antialiasing == 'TAA' or rpdat.rp_motionblur == 'Object': + assets.add_khafile_def('arm_veloc') + wrd.world_defs += '_Veloc' + if rpdat.rp_antialiasing == 'TAA': + assets.add_khafile_def('arm_taa') + + assets.add_khafile_def('rp_supersampling={0}'.format(rpdat.rp_supersampling)) + if rpdat.rp_supersampling == '4': + assets.add_shader_pass('supersample_resolve') + + assets.add_khafile_def('rp_ssgi={0}'.format(rpdat.rp_ssgi)) + if rpdat.rp_ssgi != 'Off': + wrd.world_defs += '_SSAO' + if rpdat.rp_ssgi == 'SSAO': + assets.add_shader_pass('ssao_pass') + assets.add_shader_pass('blur_edge_pass') + else: + assets.add_shader_pass('ssgi_pass') + assets.add_shader_pass('blur_edge_pass') + if rpdat.arm_ssgi_half_res: + assets.add_khafile_def('rp_ssgi_half') + + if rpdat.rp_bloom: + assets.add_khafile_def('rp_bloom') + assets.add_shader_pass('bloom_pass') + + if rpdat.arm_bloom_quality == 'low': + wrd.compo_defs += '_BloomQualityLow' + elif rpdat.arm_bloom_quality == 'medium': + wrd.compo_defs += '_BloomQualityMedium' + else: + wrd.compo_defs += '_BloomQualityHigh' + + if rpdat.arm_bloom_anti_flicker: + wrd.compo_defs += '_BloomAntiFlicker' + + if rpdat.rp_ssr: + wrd.world_defs += '_SSR' + assets.add_khafile_def('rp_ssr') + assets.add_shader_pass('ssr_pass') + assets.add_shader_pass('blur_adaptive_pass') + if rpdat.arm_ssr_half_res: + assets.add_khafile_def('rp_ssr_half') + + if rpdat.rp_ss_refraction: + wrd.world_defs += '_SSRefraction' + assets.add_khafile_def('rp_ssrefr') + assets.add_shader_pass('ssrefr_pass') + + if rpdat.rp_overlays: + assets.add_khafile_def('rp_overlays') + + if rpdat.rp_translucency: + assets.add_khafile_def('rp_translucency') + assets.add_shader_pass('translucent_resolve') + + if rpdat.rp_stereo: + assets.add_khafile_def('rp_stereo') + assets.add_khafile_def('arm_vr') + wrd.world_defs += '_VR' + + has_voxels = arm.utils.voxel_support() + if rpdat.rp_voxels and has_voxels: + assets.add_khafile_def('rp_voxels={0}'.format(rpdat.rp_voxels)) + assets.add_khafile_def('rp_voxelgi_resolution={0}'.format(rpdat.rp_voxelgi_resolution)) + assets.add_khafile_def('rp_voxelgi_resolution_z={0}'.format(rpdat.rp_voxelgi_resolution_z)) + if rpdat.arm_voxelgi_shadows: + assets.add_khafile_def('rp_voxelgi_shadows') + if rpdat.arm_rp_resolution == 'Custom': + assets.add_khafile_def('rp_resolution_filter={0}'.format(rpdat.arm_rp_resolution_filter)) + + if rpdat.rp_renderer == 'Deferred': + if rpdat.arm_material_model == 'Full': + assets.add_shader_pass('deferred_light') + + else: # mobile, solid + assets.add_shader_pass('deferred_light_' + rpdat.arm_material_model.lower()) + assets.add_khafile_def('rp_material_' + rpdat.arm_material_model.lower()) + + if len(bpy.data.lightprobes) > 0: + wrd.world_defs += '_Probes' + assets.add_khafile_def('rp_probes') + assets.add_shader_pass('probe_planar') + assets.add_shader_pass('probe_cubemap') + assets.add_shader_pass('copy_pass') + + if rpdat.rp_volumetriclight: + assets.add_khafile_def('rp_volumetriclight') + assets.add_shader_pass('volumetric_light') + assets.add_shader_pass('blur_bilat_pass') + assets.add_shader_pass('blur_bilat_blend_pass') + assets.add(assets_path + 'blue_noise64.png') + assets.add_embedded_data('blue_noise64.png') + + if rpdat.rp_decals: + assets.add_khafile_def('rp_decals') + + if rpdat.rp_water: + assets.add_khafile_def('rp_water') + assets.add_shader_pass('water_pass') + assets.add_shader_pass('copy_pass') + assets.add(assets_path + 'water_base.png') + assets.add_embedded_data('water_base.png') + assets.add(assets_path + 'water_detail.png') + assets.add_embedded_data('water_detail.png') + assets.add(assets_path + 'water_foam.png') + assets.add_embedded_data('water_foam.png') + + if rpdat.rp_blending: + assets.add_khafile_def('rp_blending') + + if rpdat.rp_depth_texture: + assets.add_khafile_def('rp_depth_texture') + assets.add_shader_pass('copy_pass') + + if rpdat.rp_sss: + assets.add_khafile_def('rp_sss') + wrd.world_defs += '_SSS' + assets.add_shader_pass('sss_pass') + + if (rpdat.rp_ssr and rpdat.arm_ssr_half_res) or (rpdat.rp_ssgi != 'Off' and rpdat.arm_ssgi_half_res): + assets.add_shader_pass('downsample_depth') + + if rpdat.rp_motionblur != 'Off': + assets.add_khafile_def('rp_motionblur={0}'.format(rpdat.rp_motionblur)) + assets.add_shader_pass('copy_pass') + if rpdat.rp_motionblur == 'Camera': + assets.add_shader_pass('motion_blur_pass') + else: + assets.add_shader_pass('motion_blur_veloc_pass') + + if rpdat.rp_compositornodes and rpdat.rp_autoexposure: + assets.add_khafile_def('rp_autoexposure') + assets.add_shader_pass('histogram_pass') + + if rpdat.rp_dynres: + assets.add_khafile_def('rp_dynres') + + if rpdat.rp_pp: + assets.add_khafile_def('rp_pp') + + if rpdat.rp_chromatic_aberration: + assets.add_shader_pass('copy_pass') + assets.add_khafile_def('rp_chromatic_aberration') + assets.add_shader_pass('chromatic_aberration_pass') + + ignoreIrr = False + + for obj in bpy.data.objects: + if obj.type == "MESH": + for slot in obj.material_slots: + mat = slot.material + + if mat: #Check if not NoneType + + if mat.arm_ignore_irradiance: + ignoreIrr = True + + if ignoreIrr: + wrd.world_defs += '_IgnoreIrr' + + gbuffer2 = '_IgnoreIrr' in wrd.world_defs or '_Veloc' in wrd.world_defs + if gbuffer2: + assets.add_khafile_def('rp_gbuffer2') + wrd.world_defs += '_gbuffer2' + + if callback is not None: + callback() + + +def get_num_gbuffer_rts_deferred() -> int: + """Return the number of render targets required for the G-Buffer.""" + wrd = bpy.data.worlds['Arm'] + + num = 2 + for flag in ('_gbuffer2', '_EmissionShaded', '_SSRefraction'): + if flag in wrd.world_defs: + num += 1 + return num diff --git a/blender/arm/make_state.py b/blender/arm/make_state.py new file mode 100644 index 0000000000..289e3834cc --- /dev/null +++ b/blender/arm/make_state.py @@ -0,0 +1,20 @@ +import arm + +if not arm.is_reload(__name__): + arm.enable_reload(__name__) + + redraw_ui = False + target = 'krom' + last_target = 'krom' + export_gapi = '' + last_resx = 0 + last_resy = 0 + last_scene = '' + last_world_defs = '' + proc_play = None + proc_build = None + proc_publish_build = None + mod_scripts = [] + is_export = False + is_play = False + is_publish = False diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py new file mode 100644 index 0000000000..78edd99125 --- /dev/null +++ b/blender/arm/make_world.py @@ -0,0 +1,392 @@ +import os + +import bpy + +import arm.assets as assets +import arm.log as log +from arm.material import make_shader +from arm.material.parser_state import ParserState, ParserContext +from arm.material.shader import ShaderContext, Shader +import arm.material.cycles as cycles +import arm.node_utils as node_utils +import arm.utils +import arm.write_probes as write_probes + +if arm.is_reload(__name__): + arm.assets = arm.reload_module(arm.assets) + arm.log = arm.reload_module(arm.log) + arm.material = arm.reload_module(arm.material) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState, ParserContext + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import ShaderContext, Shader + cycles = arm.reload_module(cycles) + node_utils = arm.reload_module(node_utils) + arm.utils = arm.reload_module(arm.utils) + write_probes = arm.reload_module(write_probes) +else: + arm.enable_reload(__name__) + +callback = None +shader_datas = [] + + +def build(): + """Builds world shaders for all exported worlds.""" + global shader_datas + + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') + + wrd.world_defs = '' + worlds = [] + shader_datas = [] + + with write_probes.setup_envmap_render(): + + for scene in bpy.data.scenes: + world = scene.world + + # Only export worlds from enabled scenes and only once per world + if scene.arm_export and world is not None and world not in worlds: + worlds.append(world) + + world.arm_envtex_name = '' + create_world_shaders(world) + + if rpdat.arm_irradiance: + # Plain background color + if '_EnvCol' in world.world_defs: + world_name = arm.utils.safestr(world.name) + # Irradiance json file name + world.arm_envtex_name = world_name + world.arm_envtex_irr_name = world_name + write_probes.write_color_irradiance(world_name, world.arm_envtex_color) + + # Render world to envmap for (ir)radiance, if no + # other probes are exported + elif world.arm_envtex_name == '': + image_file = write_probes.render_envmap(envpath, world) + image_filepath = os.path.join(envpath, image_file) + + world.arm_envtex_name = image_file + world.arm_envtex_irr_name = os.path.basename(image_filepath).rsplit('.', 1)[0] + + write_radiance = rpdat.arm_radiance and not mobile_mat + mip_count = write_probes.write_probes(image_filepath, write_probes.ENVMAP_FORMAT == 'JPEG', False, world.arm_envtex_num_mips, write_radiance) + world.arm_envtex_num_mips = mip_count + + if write_radiance: + # Set world def, everything else is handled by write_probes() + wrd.world_defs += '_Rad' + + write_probes.check_last_cmft_time() + + +def create_world_shaders(world: bpy.types.World): + """Creates fragment and vertex shaders for the given world.""" + global shader_datas + world_name = arm.utils.safestr(world.name) + pass_name = 'World_' + world_name + + shader_props = { + 'name': world_name, + 'depth_write': False, + 'compare_mode': 'less', + 'cull_mode': 'clockwise', + 'color_attachments': ['_HDR'], + 'vertex_elements': [{'name': 'pos', 'data': 'float3'}, {'name': 'nor', 'data': 'float3'}] + } + shader_data = {'name': world_name + '_data', 'contexts': [shader_props]} + + # ShaderContext expects a material, but using a world also works + shader_context = ShaderContext(world, shader_data, shader_props) + vert = shader_context.make_vert(custom_name="World_" + world_name) + frag = shader_context.make_frag(custom_name="World_" + world_name) + + # Update name, make_vert() and make_frag() above need another name + # to work + shader_context.data['name'] = pass_name + + vert.add_out('vec3 normal') + vert.add_uniform('mat4 SMVP', link="_skydomeMatrix") + + frag.add_include('compiled.inc') + frag.add_in('vec3 normal') + frag.add_out('vec4 fragColor') + + frag.write_attrib('vec3 n = normalize(normal);') + + vert.write('''normal = nor; + vec4 position = SMVP * vec4(pos, 1.0); + gl_Position = vec4(position);''') + + build_node_tree(world, frag, vert, shader_context) + + # TODO: Rework shader export so that it doesn't depend on materials + # to prevent workaround code like this + rel_path = os.path.join(arm.utils.build_dir(), 'compiled', 'Shaders') + full_path = os.path.join(arm.utils.get_fp(), rel_path) + if not os.path.exists(full_path): + os.makedirs(full_path) + + # Output: World_[world_name].[frag/vert].glsl + make_shader.write_shader(rel_path, shader_context.vert, 'vert', world_name, 'World') + make_shader.write_shader(rel_path, shader_context.frag, 'frag', world_name, 'World') + + # Write shader data file + shader_data_file = pass_name + '_data.arm' + arm.utils.write_arm(os.path.join(full_path, shader_data_file), {'contexts': [shader_context.data]}) + shader_data_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Shaders', shader_data_file) + assets.add_shader_data(shader_data_path) + + assets.add_shader_pass(pass_name) + assets.shader_passes_assets[pass_name] = shader_context.data + shader_datas.append({'contexts': [shader_context.data], 'name': pass_name}) + + +def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: ShaderContext): + """Generates the shader code for the given world.""" + world_name = arm.utils.safestr(world.name) + world.world_defs = '' + rpdat = arm.utils.get_rp() + wrd = bpy.data.worlds['Arm'] + + if callback is not None: + callback() + + # film_transparent, do not render + if bpy.context.scene is not None and bpy.context.scene.render.film_transparent: + world.world_defs += '_EnvCol' + frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') + frag.write('fragColor.rgb = backgroundCol;') + return + + parser_state = ParserState(ParserContext.WORLD, world.name, world) + parser_state.con = con + parser_state.curshader = frag + parser_state.frag = frag + parser_state.vert = vert + cycles.state = parser_state + + # Traverse world node tree + is_parsed = False + if world.node_tree is not None: + output_node = node_utils.get_node_by_type(world.node_tree, 'OUTPUT_WORLD') + if output_node is not None: + is_parsed = parse_world_output(world, output_node, frag) + + # No world nodes/no output node, use background color + if not is_parsed: + solid_mat = rpdat.arm_material_model == 'Solid' + if rpdat.arm_irradiance and not solid_mat: + world.world_defs += '_Irr' + col = world.color + world.arm_envtex_color = [col[0], col[1], col[2], 1.0] + world.arm_envtex_strength = 1.0 + world.world_defs += '_EnvCol' + + # Clouds enabled + if rpdat.arm_clouds and world.arm_use_clouds: + world.world_defs += '_EnvClouds' + # Also set this flag globally so that the required textures are + # included + wrd.world_defs += '_EnvClouds' + frag_write_clouds(world, frag) + + if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: + frag.add_uniform('float envmapStrength', link='_envmapStrength') + + # Clear background color + if '_EnvCol' in world.world_defs: + frag.write('fragColor.rgb = backgroundCol;') + + elif '_EnvTex' in world.world_defs and '_EnvLDR' in world.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));') + + if '_EnvClouds' in world.world_defs: + frag.write('if (pos.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, pos), clamp(pos.z * 5.0, 0, 1));') + + if '_EnvLDR' in world.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));') + + # Mark as non-opaque + frag.write('fragColor.a = 0.0;') + + finalize(frag, vert) + + +def finalize(frag: Shader, vert: Shader): + """Checks the given fragment shader for completeness and adds + variable initializations if required. + TODO: Merge with make_finalize? + """ + if frag.contains('pos') and not frag.contains('vec3 pos'): + frag.write_attrib('vec3 pos = -n;') + + if frag.contains('vVec') and not frag.contains('vec3 vVec'): + # For worlds, the camera seems to be always at origin in + # Blender, so we can just use the normals as the incoming vector + frag.write_attrib('vec3 vVec = n;') + + for var in ('bposition', 'mposition', 'wposition'): + if (frag.contains(var) and not frag.contains(f'vec3 {var}')) or vert.contains(var): + frag.add_in(f'vec3 {var}') + vert.add_out(f'vec3 {var}') + vert.write(f'{var} = pos;') + + if frag.contains('wtangent') and not frag.contains('vec3 wtangent'): + frag.write_attrib('vec3 wtangent = vec3(0.0);') + + if frag.contains('texCoord') and not frag.contains('vec2 texCoord'): + frag.add_in('vec2 texCoord') + vert.add_out('vec2 texCoord') + # World has no UV map + vert.write('texCoord = vec2(1.0, 1.0);') + + +def parse_world_output(world: bpy.types.World, node_output: bpy.types.Node, frag: Shader) -> bool: + """Parse the world's output node. Return `False` when the node has + no connected surface input.""" + surface_node = node_utils.find_node_by_link(world.node_tree, node_output, node_output.inputs[0]) + if surface_node is None: + return False + + parse_surface(world, surface_node, frag) + return True + + +def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Shader): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + solid_mat = rpdat.arm_material_model == 'Solid' + + if node_surface.type in ('BACKGROUND', 'EMISSION'): + # Append irradiance define + if rpdat.arm_irradiance and not solid_mat: + wrd.world_defs += '_Irr' + + # Extract environment strength + # Todo: follow/parse strength input + world.arm_envtex_strength = node_surface.inputs[1].default_value + + # Color + out = cycles.parse_vector_input(node_surface.inputs[0]) + frag.write(f'fragColor.rgb = {out};') + + if not node_surface.inputs[0].is_linked: + solid_mat = rpdat.arm_material_model == 'Solid' + if rpdat.arm_irradiance and not solid_mat: + world.world_defs += '_Irr' + world.arm_envtex_color = node_surface.inputs[0].default_value + world.arm_envtex_strength = 1.0 + + else: + log.warn(f'World node type {node_surface.type} must not be connected to the world output node!') + + # Invalidate the parser state for subsequent executions + cycles.state = None + + +def frag_write_clouds(world: bpy.types.World, frag: Shader): + """References: + GPU PRO 7 - Real-time Volumetric Cloudscapes + https://www.guerrilla-games.com/read/the-real-time-volumetric-cloudscapes-of-horizon-zero-dawn + https://github.com/sebh/TileableVolumeNoise + """ + frag.add_uniform('sampler3D scloudsBase', link='$clouds_base.raw') + frag.add_uniform('sampler3D scloudsDetail', link='$clouds_detail.raw') + frag.add_uniform('sampler2D scloudsMap', link='$clouds_map.png') + frag.add_uniform('float time', link='_time') + + frag.add_const('float', 'cloudsLower', str(round(world.arm_clouds_lower * 100) / 100)) + frag.add_const('float', 'cloudsUpper', str(round(world.arm_clouds_upper * 100) / 100)) + frag.add_const('vec2', 'cloudsWind', 'vec2(' + str(round(world.arm_clouds_wind[0] * 100) / 100) + ',' + str(round(world.arm_clouds_wind[1] * 100) / 100) + ')') + frag.add_const('float', 'cloudsPrecipitation', str(round(world.arm_clouds_precipitation * 100) / 100)) + frag.add_const('float', 'cloudsSecondary', str(round(world.arm_clouds_secondary * 100) / 100)) + frag.add_const('float', 'cloudsSteps', str(round(world.arm_clouds_steps * 100) / 100)) + + frag.add_function('''float remap(float old_val, float old_min, float old_max, float new_min, float new_max) { +\treturn new_min + (((old_val - old_min) / (old_max - old_min)) * (new_max - new_min)); +}''') + + frag.add_function('''float getDensityHeightGradientForPoint(float height, float cloud_type) { +\tconst vec4 stratusGrad = vec4(0.02f, 0.05f, 0.09f, 0.11f); +\tconst vec4 stratocumulusGrad = vec4(0.02f, 0.2f, 0.48f, 0.625f); +\tconst vec4 cumulusGrad = vec4(0.01f, 0.0625f, 0.78f, 1.0f); +\tfloat stratus = 1.0f - clamp(cloud_type * 2.0f, 0, 1); +\tfloat stratocumulus = 1.0f - abs(cloud_type - 0.5f) * 2.0f; +\tfloat cumulus = clamp(cloud_type - 0.5f, 0, 1) * 2.0f; +\tvec4 cloudGradient = stratusGrad * stratus + stratocumulusGrad * stratocumulus + cumulusGrad * cumulus; +\treturn smoothstep(cloudGradient.x, cloudGradient.y, height) - smoothstep(cloudGradient.z, cloudGradient.w, height); +}''') + + frag.add_function('''float sampleCloudDensity(vec3 p) { +\tfloat cloud_base = textureLod(scloudsBase, p, 0).r * 40; // Base noise +\tvec3 weather_data = textureLod(scloudsMap, p.xy, 0).rgb; // Weather map +\tcloud_base *= getDensityHeightGradientForPoint(p.z, weather_data.b); // Cloud type +\tcloud_base = remap(cloud_base, weather_data.r, 1.0, 0.0, 1.0); // Coverage +\tcloud_base *= weather_data.r; +\tfloat cloud_detail = textureLod(scloudsDetail, p, 0).r * 2; // Detail noise +\tfloat cloud_detail_mod = mix(cloud_detail, 1.0 - cloud_detail, clamp(p.z * 10.0, 0, 1)); +\tcloud_base = remap(cloud_base, cloud_detail_mod * 0.2, 1.0, 0.0, 1.0); +\treturn cloud_base; +}''') + + func_cloud_radiance = 'float cloudRadiance(vec3 p, vec3 dir) {\n' + if '_EnvSky' in world.world_defs: + # Nishita sky + if 'vec3 sunDir' in frag.uniforms: + func_cloud_radiance += '\tvec3 sun_dir = sunDir;\n' + # Hosek + else: + func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n' + else: + func_cloud_radiance += '\tvec3 sun_dir = vec3(0, 0, -1);\n' + func_cloud_radiance += '''\tconst int steps = 8; +\tfloat step_size = 0.5 / float(steps); +\tfloat d = 0.0; +\tp += sun_dir * step_size; +\tfor(int i = 0; i < steps; ++i) { +\t\td += sampleCloudDensity(p + sun_dir * float(i) * step_size); +\t} +\treturn 1.0 - d; +}''' + frag.add_function(func_cloud_radiance) + + func_trace_clouds = '''vec3 traceClouds(vec3 sky, vec3 dir) { +\tconst float step_size = 0.5 / float(cloudsSteps); +\tfloat T = 1.0; +\tfloat C = 0.0; +\tvec2 uv = dir.xy / dir.z * 0.4 * cloudsLower + cloudsWind * time * 0.02; + +\tfor (int i = 0; i < cloudsSteps; ++i) { +\t\tfloat h = float(i) / float(cloudsSteps); +\t\tvec3 p = vec3(uv * 0.04, h); +\t\tfloat d = sampleCloudDensity(p); + +\t\tif (d > 0) { +\t\t\t// float radiance = cloudRadiance(p, dir); +\t\t\tC += T * exp(h) * d * step_size * 0.6 * cloudsPrecipitation; +\t\t\tT *= exp(-d * step_size); +\t\t\tif (T < 0.01) break; +\t\t} +\t\tuv += (dir.xy / dir.z) * step_size * cloudsUpper; +\t} +''' + + if world.arm_darken_clouds: + func_trace_clouds += '\t// Darken clouds when the sun is low\n' + if '_EnvSky' in world.world_defs: + # Nishita sky + if 'vec3 sunDir' in frag.uniforms: + func_trace_clouds += '\tC *= smoothstep(-0.02, 0.25, sunDir.z);\n' + # Hosek + else: + func_trace_clouds += '\tC *= smoothstep(0.04, 0.32, hosekSunDirection.z);\n' + + func_trace_clouds += '\treturn vec3(C) + sky * T;\n}' + frag.add_function(func_trace_clouds) diff --git a/blender/arm/material/__init__.py b/blender/arm/material/__init__.py new file mode 100644 index 0000000000..661b77675f --- /dev/null +++ b/blender/arm/material/__init__.py @@ -0,0 +1 @@ +import arm diff --git a/blender/arm/material/arm_nodes/__init__.py b/blender/arm/material/arm_nodes/__init__.py new file mode 100644 index 0000000000..d872b16ec5 --- /dev/null +++ b/blender/arm/material/arm_nodes/__init__.py @@ -0,0 +1,6 @@ +"""Import all nodes""" +import glob +from os.path import dirname, basename, isfile + +modules = glob.glob(dirname(__file__) + "/*.py") +__all__ = [basename(f)[:-3] for f in modules if isfile(f)] diff --git a/blender/arm/material/arm_nodes/arm_nodes.py b/blender/arm/material/arm_nodes/arm_nodes.py new file mode 100644 index 0000000000..9e6f3de2d8 --- /dev/null +++ b/blender/arm/material/arm_nodes/arm_nodes.py @@ -0,0 +1,15 @@ +from typing import Type + +from bpy.types import Node +import nodeitems_utils + +nodes = [] +category_items = {} + + +def add_node(node_class: Type[Node], category: str): + global nodes + nodes.append(node_class) + if category_items.get(category) is None: + category_items[category] = [] + category_items[category].append(nodeitems_utils.NodeItem(node_class.bl_idname)) diff --git a/blender/arm/material/arm_nodes/custom_particle_node.py b/blender/arm/material/arm_nodes/custom_particle_node.py new file mode 100644 index 0000000000..bc2730dee9 --- /dev/null +++ b/blender/arm/material/arm_nodes/custom_particle_node.py @@ -0,0 +1,192 @@ +from bpy.props import * +from bpy.types import Node + +from arm.material.arm_nodes.arm_nodes import add_node +from arm.material.shader import Shader +from arm.material.cycles import * + +if arm.is_reload(__name__): + import arm + arm.material.arm_nodes.arm_nodes = arm.reload_module(arm.material.arm_nodes.arm_nodes) + from arm.material.arm_nodes.arm_nodes import add_node + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader + arm.material.cycles = arm.reload_module(arm.material.cycles) + from arm.material.cycles import * +else: + arm.enable_reload(__name__) + + +class CustomParticleNode(Node): + """Input data for paricles.""" + bl_idname = 'ArmCustomParticleNode' + bl_label = 'Custom Particle' + bl_icon = 'NONE' + + posX: BoolProperty( + name="", + description="enable translation along x", + default=False, + ) + + posY: BoolProperty( + name="", + description="enable translation along y", + default=False, + ) + + posZ: BoolProperty( + name="", + description="enable translation along z", + default=False, + ) + + rotX: BoolProperty( + name="", + description="enable rotation along x", + default=False, + ) + + rotY: BoolProperty( + name="", + description="enable rotation along y", + default=False, + ) + + rotZ: BoolProperty( + name="", + description="enable rotation along z", + default=False, + ) + + sclX: BoolProperty( + name="", + description="enable scaling along x", + default=False, + ) + + sclY: BoolProperty( + name="", + description="enable scaling along y", + default=False, + ) + + sclZ: BoolProperty( + name="", + description="enable scaling along z", + default=False, + ) + + billBoard: BoolProperty( + name="Bill Board", + description="Enable Bill Board", + default=False, + ) + + def init(self, context): + self.inputs.new('NodeSocketVector', 'Position') + self.inputs.new('NodeSocketVector', 'Rotation') + self.inputs.new('NodeSocketVector', 'Scale') + + def draw_buttons(self, context, layout): + + grid0 = layout.grid_flow(row_major=True, columns=4, align=False) + + grid0.label(text="") + grid0.label(text=" X") + grid0.label(text=" Y") + grid0.label(text=" Z") + + grid0.label(text="Pos") + grid0.prop(self, "posX") + grid0.prop(self, "posY") + grid0.prop(self, "posZ") + + grid0.label(text="Rot") + grid0.prop(self, "rotX") + grid0.prop(self, "rotY") + grid0.prop(self, "rotZ") + + grid0.label(text="Scl") + grid0.prop(self, "sclX") + grid0.prop(self, "sclY") + grid0.prop(self, "sclZ") + + layout.prop(self, "billBoard") + + def parse(self, vertshdr: Shader, part_con) -> None: + + if self.sclX or self.sclY or self.sclZ: + scl = parse_vector_input(self.inputs[2]) + + if self.sclX: + vertshdr.write(f'spos.x *= {scl}.x;') + + if self.sclY: + vertshdr.write(f'spos.y *= {scl}.y;') + + if self.sclX: + vertshdr.write(f'spos.z *= {scl}.z;') + + if self.billBoard: + vertshdr.add_uniform('mat4 WV', '_worldViewMatrix') + vertshdr.write('spos = mat4(transpose(mat3(WV))) * spos;') + + if self.rotX or self.rotY or self.rotZ: + rot = parse_vector_input(self.inputs[1]) + + if self.rotX and not self.rotY and not self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(1.0, 0.0, 0.0,') + vertshdr.write(f' 0.0, cos({rot}.x), sin({rot}.x),') + vertshdr.write(f' 0.0, -sin({rot}.x), cos({rot}.x));') + + if not self.rotX and self.rotY and not self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.y), 0.0, -sin({rot}.y),') + vertshdr.write(f' 0.0, 1.0, 0.0,') + vertshdr.write(f' sin({rot}.y), 0.0, cos({rot}.y));') + + if not self.rotX and not self.rotY and self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.z), sin({rot}.z), 0.0,') + vertshdr.write(f' -sin({rot}.z), cos({rot}.z), 0.0,') + vertshdr.write(f' 0.0, 0.0, 1.0);') + + if self.rotX and self.rotY and not self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.y), 0.0, -sin({rot}.y),') + vertshdr.write(f' sin({rot}.y) * sin({rot}.x), cos({rot}.x), cos({rot}.y) * sin({rot}.x),') + vertshdr.write(f' sin({rot}.y) * cos({rot}.x), -sin({rot}.x), cos({rot}.y) * cos({rot}.x));') + + if self.rotX and not self.rotY and self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.z), sin({rot}.z), 0.0,') + vertshdr.write(f' -sin({rot}.z) * cos({rot}.x), cos({rot}.z) * cos({rot}.x), sin({rot}.x),') + vertshdr.write(f' sin({rot}.z) * sin({rot}.x), -cos({rot}.z) * sin({rot}.x), cos({rot}.x));') + + if not self.rotX and self.rotY and self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.z) * cos({rot}.y), sin({rot}.z) * cos({rot}.y), -sin({rot}.y),') + vertshdr.write(f' -sin({rot}.z) , cos({rot}.z), 0.0,') + vertshdr.write(f' cos({rot}.z) * sin({rot}.y), sin({rot}.z) * sin({rot}.y), cos({rot}.y));') + + if self.rotX and self.rotY and self.rotZ: + vertshdr.write(f'mat3 part_rot_mat = mat3(cos({rot}.z) * cos({rot}.y), sin({rot}.z) * cos({rot}.y), -sin({rot}.y),') + vertshdr.write(f' -sin({rot}.z) * cos({rot}.x) + cos({rot}.z) * sin({rot}.y) * sin({rot}.x), cos({rot}.z) * cos({rot}.x) + sin({rot}.z) * sin({rot}.y) * sin({rot}.x), cos({rot}.y) * sin({rot}.x),') + vertshdr.write(f' sin({rot}.z) * sin({rot}.x) + cos({rot}.z) * sin({rot}.y) * cos({rot}.x), -cos({rot}.z) * sin({rot}.x) + sin({rot}.z) * sin({rot}.y) * cos({rot}.x), cos({rot}.y) * cos({rot}.x));') + + vertshdr.write('spos.xyz = part_rot_mat * spos.xyz;') + if (part_con.data['name'] == 'mesh' or part_con.data['name'] == 'translucent' or part_con.data['name'] == 'refraction'): + vertshdr.write('wnormal = transpose(inverse(part_rot_mat)) * wnormal;') + + if self.posX or self.posY or self.posZ: + pos = parse_vector_input(self.inputs[0]) + + if self.posX: + vertshdr.write(f'spos.x += {pos}.x;') + + if self.posY: + vertshdr.write(f'spos.y += {pos}.y;') + + if self.posZ: + vertshdr.write(f'spos.z += {pos}.z;') + + vertshdr.write('wposition = vec4(W * spos).xyz;') + + +add_node(CustomParticleNode, category='Armory') diff --git a/blender/arm/material/arm_nodes/shader_data_node.py b/blender/arm/material/arm_nodes/shader_data_node.py new file mode 100644 index 0000000000..d4f530ee00 --- /dev/null +++ b/blender/arm/material/arm_nodes/shader_data_node.py @@ -0,0 +1,107 @@ +from bpy.props import * +from bpy.types import Node, NodeSocket + +import arm +from arm.material.arm_nodes.arm_nodes import add_node +from arm.material.parser_state import ParserState +from arm.material.shader import Shader + +if arm.is_reload(__name__): + arm.material.arm_nodes.arm_nodes = arm.reload_module(arm.material.arm_nodes.arm_nodes) + from arm.material.arm_nodes.arm_nodes import add_node + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader +else: + arm.enable_reload(__name__) + + +class ShaderDataNode(Node): + """Allows access to shader data such as uniforms and inputs.""" + bl_idname = 'ArmShaderDataNode' + bl_label = 'Shader Data' + bl_icon = 'NONE' + + input_type: EnumProperty( + items = [('input', 'Input', 'Shader Input'), + ('uniform', 'Uniform', 'Uniform value')], + name='Input Type', + default='input', + description="The kind of data that should be retrieved") + + input_source: EnumProperty( + items = [('frag', 'Fragment Shader', 'Take the input from the fragment shader'), + ('vert', 'Vertex Shader', 'Take the input from the vertex shader and pass it through to the fragment shader')], + name='Input Source', + default='vert', + description="Where to take the input value from") + + variable_type: EnumProperty( + items = [('int', 'int', 'int'), + ('float', 'float', 'float'), + ('vec2', 'vec2', 'vec2'), + ('vec3', 'vec3', 'vec3'), + ('vec4', 'vec4', 'vec4'), + ('sampler2D', 'sampler2D', 'sampler2D')], + name='Variable Type', + default='vec3', + description="The type of the variable") + + variable_name: StringProperty(name="Variable Name", description="The name of the variable") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + col.label(text="Input Type:") + # Use a row to expand horizontally + col.row().prop(self, "input_type", expand=True) + + split = layout.split(factor=0.5, align=True) + col_left = split.column() + col_right = split.column() + + if self.input_type == "input": + col_left.label(text="Input Source") + col_right.prop(self, "input_source", text="") + + col_left.label(text="Variable Type") + col_right.prop(self, "variable_type", text="") + col_left.label(text="Variable Name") + col_right.prop(self, "variable_name", text="") + + def init(self, context): + self.outputs.new('NodeSocketColor', 'Color') + self.outputs.new('NodeSocketVector', 'Vector') + self.outputs.new('NodeSocketFloat', 'Float') + self.outputs.new('NodeSocketInt', 'Int') + + def __parse(self, out_socket: NodeSocket, state: ParserState) -> str: + if self.input_type == "uniform": + state.frag.add_uniform(f'{self.variable_type} {self.variable_name}', link=self.variable_name) + state.vert.add_uniform(f'{self.variable_type} {self.variable_name}', link=self.variable_name) + + if self.variable_type == "sampler2D": + state.frag.add_uniform('vec2 screenSize', link='_screenSize') + return f'textureLod({self.variable_name}, gl_FragCoord.xy / screenSize, 0.0).rgb' + + return self.variable_name + + else: + if self.input_source == "frag": + state.frag.add_in(f'{self.variable_type} {self.variable_name}') + return self.variable_name + + # Reroute input from vertex shader to fragment shader (input must exist!) + else: + state.vert.add_out(f'{self.variable_type} out_{self.variable_name}') + state.frag.add_in(f'{self.variable_type} out_{self.variable_name}') + + state.vert.write(f'out_{self.variable_name} = {self.variable_name};') + return 'out_' + self.variable_name + + @staticmethod + def parse(node: 'ShaderDataNode', out_socket: NodeSocket, state: ParserState) -> str: + return node.__parse(out_socket, state) + + +add_node(ShaderDataNode, category='Armory') diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py new file mode 100644 index 0000000000..50e0807cb0 --- /dev/null +++ b/blender/arm/material/cycles.py @@ -0,0 +1,967 @@ +# +# This module builds upon Cycles nodes work licensed as +# Copyright 2011-2013 Blender Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import os +import shutil +from typing import Any, Dict, Optional, Tuple + +import bpy + +import arm.assets +import arm.log as log +import arm.make_state +import arm.material.cycles_functions as c_functions +import arm.material.node_meta as node_meta +import arm.material.mat_state as mat_state +from arm.material.parser_state import ParserState, ParserContext, ParserPass +from arm.material.shader import Shader, ShaderContext, floatstr, vec3str +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + arm.assets = arm.reload_module(arm.assets) + log = arm.reload_module(log) + arm.make_state = arm.reload_module(arm.make_state) + c_functions = arm.reload_module(c_functions) + arm.material.cycles_nodes = arm.reload_module(arm.material.cycles_nodes) + node_meta = arm.reload_module(node_meta) + from arm.material.cycles_nodes import * + mat_state = arm.reload_module(mat_state) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState, ParserContext, ParserPass + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader, ShaderContext, floatstr, vec3str + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +# Particle info export +particle_info: Dict[str, bool] = {} + +state: Optional[ParserState] + + +def parse(nodes, con: ShaderContext, + vert: Shader, frag: Shader, geom: Shader, tesc: Shader, tese: Shader, + parse_surface=True, parse_opacity=True, parse_displacement=True, basecol_only=False): + global state + + state = ParserState(ParserContext.OBJECT, mat_state.material.name) + + state.parse_surface = parse_surface + state.parse_opacity = parse_opacity + state.parse_displacement = parse_displacement + state.basecol_only = basecol_only + + state.con = con + + state.vert = vert + state.frag = frag + state.geom = geom + state.tesc = tesc + state.tese = tese + + output_node = node_by_type(nodes, 'OUTPUT_MATERIAL') + if output_node is not None: + custom_particle_node = node_by_name(nodes, 'ArmCustomParticleNode') + parse_material_output(output_node, custom_particle_node) + + # Make sure that individual functions in this module aren't called with an incorrect/old parser state, set it to + # None so that it will raise exceptions when not set + state = None + + +def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.Node): + global particle_info + + parse_surface = state.parse_surface + parse_opacity = state.parse_opacity + parse_displacement = state.parse_displacement + particle_info = { + 'index': False, + 'age': False, + 'lifetime': False, + 'location': False, + 'size': False, + 'velocity': False, + 'angular_velocity': False + } + wrd = bpy.data.worlds['Arm'] + + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + + # Surface + if parse_surface or parse_opacity: + state.parents = [] + state.parsed = set() + state.normal_parsed = False + curshader = state.frag + state.curshader = curshader + + out_basecol, out_roughness, out_metallic, out_occlusion, out_specular, out_opacity, out_rior, out_emission_col = parse_shader_input(node.inputs[0]) + if parse_surface: + curshader.write(f'basecol = {out_basecol};') + curshader.write(f'roughness = {out_roughness};') + curshader.write(f'metallic = {out_metallic};') + curshader.write(f'occlusion = {out_occlusion};') + curshader.write(f'specular = {out_specular};') + curshader.write(f'emissionCol = {out_emission_col};') + + if mat_state.emission_type == mat_state.EmissionType.SHADELESS: + if '_EmissionShadeless' not in wrd.world_defs: + wrd.world_defs += '_EmissionShadeless' + elif mat_state.emission_type == mat_state.EmissionType.SHADED: + if '_EmissionShaded' not in wrd.world_defs: + wrd.world_defs += '_EmissionShaded' + arm.assets.add_khafile_def('rp_gbuffer_emission') + + if parse_opacity: + curshader.write('opacity = {0};'.format(out_opacity)) + curshader.write('rior = {0};'.format(out_rior)) + + # Volume + # parse_volume_input(node.inputs[1]) + + # Displacement + if parse_displacement and disp_enabled() and node.inputs[2].is_linked: + state.parents = [] + state.parsed = set() + state.normal_parsed = False + rpdat = arm.utils.get_rp() + if rpdat.arm_rp_displacement == 'Tessellation' and state.tese is not None: + state.curshader = state.tese + else: + state.curshader = state.vert + out_disp = parse_displacement_input(node.inputs[2]) + state.curshader.write('vec3 disp = {0};'.format(out_disp)) + + if custom_particle_node is not None: + if not (parse_displacement and disp_enabled() and node.inputs[2].is_linked): + state.parents = [] + state.parsed = set() + state.normal_parsed = False + + state.curshader = state.vert + custom_particle_node.parse(state.curshader, state.con) + + +def parse_group(node, socket): # Entering group + index = socket_index(node, socket) + output_node = node_by_type(node.node_tree.nodes, 'GROUP_OUTPUT') + if output_node is None: + return + inp = output_node.inputs[index] + state.parents.append(node) + out_group = parse_input(inp) + state.parents.pop() + return out_group + + +def parse_group_input(node: bpy.types.Node, socket: bpy.types.NodeSocket): + index = socket_index(node, socket) + parent = state.parents.pop() # Leaving group + inp = parent.inputs[index] + res = parse_input(inp) + state.parents.append(parent) # Return to group + return res + + +def parse_input(inp: bpy.types.NodeSocket): + if inp.type == 'SHADER': + return parse_shader_input(inp) + elif inp.type in ('RGB', 'RGBA', 'VECTOR'): + return parse_vector_input(inp) + elif inp.type == 'VALUE': + return parse_value_input(inp) + + +def parse_shader_input(inp: bpy.types.NodeSocket) -> Tuple[str, ...]: + # Follow input + if inp.is_linked: + link = inp.links[0] + if link.from_node.type == 'REROUTE': + return parse_shader_input(link.from_node.inputs[0]) + + if link.from_socket.type != 'SHADER': + log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a shader socket') + state.reset_outs() + return state.get_outs() + + return parse_shader(link.from_node, link.from_socket) + + else: + # Return default shader values + state.reset_outs() + return state.get_outs() + + +def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[str, ...]: + supported_node_types = ( + 'MIX_SHADER', + 'ADD_SHADER', + 'BSDF_PRINCIPLED', + 'BSDF_DIFFUSE', + 'BSDF_GLOSSY', + 'AMBIENT_OCCLUSION', + 'BSDF_ANISOTROPIC', + 'EMISSION', + 'BSDF_GLASS', + 'HOLDOUT', + 'SUBSURFACE_SCATTERING', + 'BSDF_TRANSLUCENT', + 'BSDF_TRANSPARENT', + 'BSDF_VELVET', + ) + + state.reset_outs() + + if node.type in supported_node_types: + node_meta.get_node_meta(node).parse_func(node, socket, state) + + elif node.type == 'GROUP': + if node.node_tree.name.startswith('Armory PBR'): + if state.parse_surface: + # Normal + if node.inputs[5].is_linked and node.inputs[5].links[0].from_node.type == 'NORMAL_MAP': + log.warn(tree_name() + ' - Do not use Normal Map node with Armory PBR, connect Image Texture directly') + parse_normal_map_color_input(node.inputs[5]) + + emission_factor = f'clamp({parse_value_input(node.inputs[6])}, 0.0, 1.0)' + basecol = parse_vector_input(node.inputs[0]) + + # Multiply base color with inverse of emission factor to + # copy behaviour of the Mix Shader node used in the group + # (less base color -> less shading influence) + state.out_basecol = f'({basecol} * (1 - {emission_factor}))' + + state.out_occlusion = parse_value_input(node.inputs[2]) + state.out_roughness = parse_value_input(node.inputs[3]) + state.out_metallic = parse_value_input(node.inputs[4]) + + # Emission + if node.inputs[6].is_linked or node.inputs[6].default_value != 0.0: + state.out_emission_col = f'({basecol} * {emission_factor})' + mat_state.emission_type = mat_state.EmissionType.SHADED + if state.parse_opacity: + state.out_opacity = parse_value_input(node.inputs[1]) + else: + return parse_group(node, socket) + + elif node.type == 'GROUP_INPUT': + return parse_group_input(node, socket) + + elif node.type == 'CUSTOM': + if node.bl_idname == 'ArmShaderDataNode': + return node_meta.get_node_meta(node).parse_func(node, socket, state) + + else: + log.warn(f'Node tree "{tree_name()}": material node type {node.type} not supported') + + return state.get_outs() + + +def parse_displacement_input(inp): + if inp.is_linked: + l = inp.links[0] + if l.from_node.type == 'REROUTE': + return parse_displacement_input(l.from_node.inputs[0]) + return parse_vector_input(inp) + else: + return None + + +def parse_vector_input(inp: bpy.types.NodeSocket) -> vec3str: + """Return the parsed result of the given input socket.""" + # Follow input + if inp.is_linked: + link = inp.links[0] + if link.from_node.type == 'REROUTE': + return parse_vector_input(link.from_node.inputs[0]) + res_var = write_result(link) + st = link.from_socket.type + if st in ('RGB', 'RGBA', 'VECTOR'): + return res_var + elif st in ('VALUE', 'INT'): + return f'vec3({res_var})' + else: + log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a vector-like socket') + return to_vec3([0.0, 0.0, 0.0]) + + # Unlinked reroute + elif inp.type == 'VALUE': + return to_vec3([0.0, 0.0, 0.0]) + + # Use direct socket value + else: + if mat_batch() and inp.is_uniform: + return to_uniform(inp) + else: + return to_vec3(inp.default_value) + + +def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: + """Parses the vector/color output value from the given node and socket.""" + supported_node_types = ( + 'ATTRIBUTE', + + # RGB outputs + 'RGB', + 'TEX_BRICK', + 'TEX_CHECKER', + 'TEX_ENVIRONMENT', + 'TEX_GRADIENT', + 'TEX_IMAGE', + 'TEX_MAGIC', + 'TEX_MUSGRAVE', + 'TEX_NOISE', + 'TEX_POINTDENSITY', + 'TEX_SKY', + 'TEX_VORONOI', + 'TEX_WAVE', + 'VERTEX_COLOR', + 'BRIGHTCONTRAST', + 'GAMMA', + 'HUE_SAT', + 'INVERT', + 'MIX_RGB', + 'BLACKBODY', + 'VALTORGB', + 'CURVE_VEC', + 'CURVE_RGB', + 'COMBINE_COLOR', + 'COMBHSV', + 'COMBRGB', + 'WAVELENGTH', + + # Vector outputs + 'CAMERA', + 'NEW_GEOMETRY', + 'HAIR_INFO', + 'OBJECT_INFO', + 'PARTICLE_INFO', + 'TANGENT', + 'TEX_COORD', + 'UVMAP', + 'BUMP', + 'MAPPING', + 'NORMAL', + 'NORMAL_MAP', + 'VECT_TRANSFORM', + 'COMBXYZ', + 'VECT_MATH', + 'DISPLACEMENT', + 'VECTOR_ROTATE', + ) + + if node.type in supported_node_types: + return node_meta.get_node_meta(node).parse_func(node, socket, state) + + elif node.type == 'GROUP': + return parse_group(node, socket) + + elif node.type == 'GROUP_INPUT': + return parse_group_input(node, socket) + + elif node.type == 'CUSTOM': + if node.bl_idname == 'ArmShaderDataNode': + return node_meta.get_node_meta(node).parse_func(node, socket, state) + + log.warn(f'Node tree "{tree_name()}": material node type {node.type} not supported') + return "vec3(0, 0, 0)" + + +def parse_normal_map_color_input(inp, strength_input=None): + frag = state.frag + + if state.basecol_only or not inp.is_linked or state.normal_parsed: + return + + state.normal_parsed = True + frag.write_normal += 1 + if not get_arm_export_tangents() or mat_get_material().arm_decal: # Compute TBN matrix + frag.write('vec3 texn = ({0}) * 2.0 - 1.0;'.format(parse_vector_input(inp))) + frag.write('texn.y = -texn.y;') + frag.add_include('std/normals.glsl') + frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);') + frag.write('n = TBN * normalize(texn);') + else: + frag.write('n = ({0}) * 2.0 - 1.0;'.format(parse_vector_input(inp))) + if strength_input is not None: + strength = parse_value_input(strength_input) + if strength != '1.0': + frag.write('n.xy *= {0};'.format(strength)) + frag.write('n = normalize(TBN * n);') + state.con.add_elem('tang', 'short4norm') + frag.write_normal -= 1 + + +def parse_value_input(inp: bpy.types.NodeSocket) -> floatstr: + # Follow input + if inp.is_linked: + link = inp.links[0] + + if link.from_node.type == 'REROUTE': + return parse_value_input(link.from_node.inputs[0]) + + res_var = write_result(link) + socket_type = link.from_socket.type + if socket_type in ('RGB', 'RGBA', 'VECTOR'): + # RGB to BW + return rgb_to_bw(res_var) + elif socket_type in ('VALUE', 'INT'): + return res_var + else: + log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a scalar value socket') + return '0.0' + + # Use value from socket + else: + if mat_batch() and inp.is_uniform: + return to_uniform(inp) + else: + return to_vec1(inp.default_value) + + +def parse_value(node, socket): + supported_node_types = ( + 'ATTRIBUTE', + 'CAMERA', + 'FRESNEL', + 'NEW_GEOMETRY', + 'HAIR_INFO', + 'LAYER_WEIGHT', + 'LIGHT_PATH', + 'OBJECT_INFO', + 'PARTICLE_INFO', + 'VALUE', + 'WIREFRAME', + 'TEX_BRICK', + 'TEX_CHECKER', + 'TEX_GRADIENT', + 'TEX_IMAGE', + 'TEX_MAGIC', + 'TEX_MUSGRAVE', + 'TEX_NOISE', + 'TEX_POINTDENSITY', + 'TEX_VORONOI', + 'TEX_WAVE', + 'LIGHT_FALLOFF', + 'NORMAL', + 'CLAMP', + 'VALTORGB', + 'MATH', + 'RGBTOBW', + 'SEPARATE_COLOR', + 'SEPHSV', + 'SEPRGB', + 'SEPXYZ', + 'VECT_MATH', + 'MAP_RANGE', + ) + + if node.type in supported_node_types: + return node_meta.get_node_meta(node).parse_func(node, socket, state) + + elif node.type == 'GROUP': + if node.node_tree.name.startswith('Armory PBR'): + # Displacement + if socket == node.outputs[1]: + return parse_value_input(node.inputs[7]) + else: + return None + else: + return parse_group(node, socket) + + elif node.type == 'GROUP_INPUT': + return parse_group_input(node, socket) + + elif node.type == 'CUSTOM': + if node.bl_idname == 'ArmShaderDataNode': + return node_meta.get_node_meta(node).parse_func(node, socket, state) + + log.warn(f'Node tree "{tree_name()}": material node type {node.type} not supported') + return '0.0' + + +def vector_curve(name, fac, points): + curshader = state.curshader + + # Write Ys array + ys_var = name + '_ys' + state.get_parser_pass_suffix() + curshader.write('float {0}[{1}];'.format(ys_var, len(points))) # TODO: Make const + for i in range(0, len(points)): + curshader.write('{0}[{1}] = {2};'.format(ys_var, i, points[i].location[1])) + # Get index + fac_var = name + '_fac' + state.get_parser_pass_suffix() + curshader.write('float {0} = {1};'.format(fac_var, fac)) + index = '0' + for i in range(1, len(points)): + index += ' + ({0} > {1} ? 1 : 0)'.format(fac_var, points[i].location[0]) + # Write index + index_var = name + '_i' + state.get_parser_pass_suffix() + curshader.write('int {0} = {1};'.format(index_var, index)) + # Linear + # Write Xs array + facs_var = name + '_xs' + state.get_parser_pass_suffix() + curshader.write('float {0}[{1}];'.format(facs_var, len(points))) # TODO: Make const + for i in range(0, len(points)): + curshader.write('{0}[{1}] = {2};'.format(facs_var, i, points[i].location[0])) + # Map vector + return 'mix({0}[{1}], {0}[{1} + 1], ({2} - {3}[{1}]) * (1.0 / ({3}[{1} + 1] - {3}[{1}]) ))'.format(ys_var, index_var, fac_var, facs_var) + +def write_normal(inp): + if inp.is_linked and inp.links[0].from_node.type != 'GROUP_INPUT': + normal_res = parse_vector_input(inp) + if normal_res != None: + state.curshader.write('n = {0};'.format(normal_res)) + + +def is_parsed(node_store_name: str): + return node_store_name in state.parsed + + +def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: + """Return the name of the variable that stores the parsed result + from the given node and socket.""" + return node_name(node.name) + '_' + safesrc(socket.name) + '_res' + + +def write_result(link: bpy.types.NodeLink) -> Optional[str]: + """Write the parsed result of the given node link to the shader.""" + res_var = res_var_name(link.from_node, link.from_socket) + + need_dxdy_offset = node_need_reevaluation_for_screenspace_derivative(link.from_node) + if need_dxdy_offset: + res_var += state.get_parser_pass_suffix() + + # Unparsed node + if not is_parsed(res_var): + state.parsed.add(res_var) + st = link.from_socket.type + + if st in ('RGB', 'RGBA', 'VECTOR'): + res = parse_vector(link.from_node, link.from_socket) + if res is None: + log.error(f'{link.from_node.name} returned `None` while parsing!') + return None + state.curshader.write(f'vec3 {res_var} = {res};') + + elif st == 'VALUE': + res = parse_value(link.from_node, link.from_socket) + if res is None: + log.error(f'{link.from_node.name} returned `None` while parsing!') + return None + if link.from_node.type == "VALUE" and not link.from_node.arm_material_param: + state.curshader.add_const('float', res_var, res) + else: + state.curshader.write(f'float {res_var} = {res};') + + if state.dxdy_varying_input_value: + state.curshader.write(f'{res_var} = {apply_screenspace_derivative_offset_if_required(res_var)};') + state.dxdy_varying_input_value = False + + # Normal map already parsed, return + elif link.from_node.type == 'NORMAL_MAP': + return None + + return res_var + + +def write_procedurals(): + if state.curshader not in state.procedurals_written: + state.curshader.add_function(c_functions.str_tex_proc) + state.procedurals_written.add(state.curshader) + + +def glsl_type(socket_type: str): + """Socket to glsl type.""" + if socket_type in ('RGB', 'RGBA', 'VECTOR'): + return 'vec3' + else: + return 'float' + +def to_uniform(inp: bpy.types.NodeSocket): + uname = safesrc(inp.node.name) + safesrc(inp.name) + state.curshader.add_uniform(glsl_type(inp.type) + ' ' + uname) + return uname + +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, default_value=None, is_arm_mat_param=None): + curshader = state.curshader + + tex_store = store_var_name(node) + + if node_need_reevaluation_for_screenspace_derivative(node): + tex_store += state.get_parser_pass_suffix() + + if is_parsed(tex_store): + return tex_store + state.parsed.add(tex_store) + + if is_arm_mat_param is None: + mat_bind_texture(tex) + state.con.add_elem('tex', 'short2norm') + 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]) + if triplanar: + uv_name = 'vec3({0}.x, 1.0 - {0}.y, {0}.z)'.format(uv_name) + else: + uv_name = 'vec2({0}.x, 1.0 - {0}.y)'.format(uv_name) + else: + uv_name = 'vec3(texCoord.xy, 0.0)' if triplanar else 'texCoord' + if triplanar: + if not curshader.has_include('std/mapping.glsl'): + curshader.add_include('std/mapping.glsl') + if state.normal_parsed: + nor = 'TBN[2]' + else: + nor = 'n' + curshader.write('vec4 {0} = vec4(triplanarMapping({1}, {2}, {3}), 0.0);'.format(tex_store, tex_name, nor, uv_name)) + else: + if mat_state.texture_grad: + curshader.write('vec4 {0} = textureGrad({1}, {2}.xy, g2.xy, g2.zw);'.format(tex_store, tex_name, uv_name)) + else: + curshader.write('vec4 {0} = texture({1}, {2}.xy);'.format(tex_store, tex_name, uv_name)) + + if to_linear: + curshader.write('{0}.rgb = pow({0}.rgb, vec3(2.2));'.format(tex_store)) + + return tex_store + + +def apply_screenspace_derivative_offset_if_required(coords: str) -> str: + """Apply screen-space derivative offsets to the given coordinates, + if required by the current ParserPass. + """ + # Derivative functions are only available in fragment shaders + if state.curshader.shader_type == 'frag': + if state.current_pass == ParserPass.DX_SCREEN_SPACE: + coords = f'({coords}) + {dfdx_fine(coords)}' + elif state.current_pass == ParserPass.DY_SCREEN_SPACE: + coords = f'({coords}) + {dfdy_fine(coords)}' + + return '(' + coords + ')' + + +def node_need_reevaluation_for_screenspace_derivative(node: bpy.types.Node) -> bool: + if state.current_pass not in (ParserPass.DX_SCREEN_SPACE, ParserPass.DY_SCREEN_SPACE): + return False + + should_compute_offset = node_meta.get_node_meta(node).compute_dxdy_variants + + if should_compute_offset == node_meta.ComputeDXDYVariant.ALWAYS: + return True + elif should_compute_offset == node_meta.ComputeDXDYVariant.NEVER: + return False + + # ComputeDXDYVariant.DYNAMIC + for inp in node.inputs: + c_node, _ = arm.node_utils.input_get_connected_node(inp) + if c_node is None: + continue + + if node_need_reevaluation_for_screenspace_derivative(c_node): + return True + + return False + + +def dfdx_fine(val: str) -> str: + # GL_ARB_derivative_control is unavailable in OpenGL ES (= no fine/coarse variants), + # OES_standard_derivatives is automatically enabled in kha.SystemImpl + return f'dFdx({val})' if arm.utils.is_gapi_gl_es() else f'dFdxFine({val})' + + +def dfdy_fine(val: str) -> str: + return f'dFdy({val})' if arm.utils.is_gapi_gl_es() else f'dFdyFine({val})' + + +def to_vec1(v): + return str(v) + + +def to_vec2(v): + return f'vec2({v[0]}, {v[1]})' + + +def to_vec3(v): + return f'vec3({v[0]}, {v[1]}, {v[2]})' + + +def cast_value(val: str, from_type: str, to_type: str) -> str: + """Casts a value that is already parsed in a glsl string to another + value in a string. + + vec2 types are not supported (not used in the node editor) and there + is no cast towards int types. If casting from vec3 to vec4, the w + coordinate/alpha channel is filled with a 1. + + If this function is called with invalid parameters, a TypeError is + raised. + """ + if from_type == to_type: + return val + + if from_type in ('int', 'float'): + if to_type in ('int', 'float'): + return val + elif to_type in ('vec2', 'vec3', 'vec4'): + return f'{to_type}({val})' + + elif from_type == 'vec3': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec4': + return f'vec4({val}, 1.0)' + + elif from_type == 'vec4': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec3': + return f'{val}.xyz' + + raise TypeError("Invalid type cast in shader!") + + +def rgb_to_bw(res_var: vec3str) -> floatstr: + # Blender uses the default OpenColorIO luma coefficients which + # originally come from the Rec. 709 standard (see ITU-R BT.709-6 Item 3.3) + return f'dot({res_var}, vec3(0.2126, 0.7152, 0.0722))' + + +def node_by_type(nodes, ntype: str) -> bpy.types.Node: + for n in nodes: + if n.type == ntype: + return n + +def node_by_name(nodes, name: str) -> bpy.types.Node: + for n in nodes: + if n.bl_idname == name: + return n + +def socket_index(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> int: + for i in range(0, len(node.outputs)): + if node.outputs[i] == socket: + return i + + +def node_name(s: str) -> str: + """Return a unique and safe name for a node for shader code usage.""" + for p in state.parents: + s = p.name + '_' + s + if state.curshader.write_textures > 0: + s += '_texread' + s = safesrc(s) + if '__' in s: # Consecutive _ are reserved + s = s.replace('_', '_x') + return s + +## + + +def make_texture( + image: bpy.types.Image, tex_name: str, matname: str, + interpolation: str, extension: str, +) -> Optional[Dict[str, Any]]: + """Creates a texture binding entry for the scene's export data + ('bind_textures') for a given texture image. + """ + tex = {'name': tex_name} + + if image is None: + return None + + if matname is None: + matname = mat_state.material.name + + # Get filepath + filepath = image.filepath + if filepath == '': + if image.packed_file is not None: + filepath = './' + image.name + has_ext = filepath.endswith(('.jpg', '.png', '.hdr')) + if not has_ext: + # Raw bytes, write converted .jpg to /unpacked + filepath += '.raw' + + elif image.source == "GENERATED": + unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + + filepath = os.path.join(unpack_path, image.name + ".jpg") + arm.utils.convert_image(image, filepath, "JPEG") + + else: + log.warn(matname + '/' + image.name + ' - invalid file path') + return None + else: + filepath = arm.utils.to_absolute_path(filepath, image.library) + + # Reference image name + texpath = arm.utils.asset_path(filepath) + texfile = arm.utils.extract_filename(filepath) + tex['file'] = arm.utils.safestr(texfile) + s = tex['file'].rsplit('.', 1) + + if len(s) == 1: + log.warn(matname + '/' + image.name + ' - file extension required for image name') + return None + + ext = s[1].lower() + do_convert = ext not in ('jpg', 'png', 'hdr', 'mp4') # Convert image + if do_convert: + new_ext = 'png' if (ext in ('tga', 'dds')) else 'jpg' + tex['file'] = tex['file'].rsplit('.', 1)[0] + '.' + new_ext + + if image.packed_file is not None or not is_ascii(texfile): + # Extract packed data / copy non-ascii texture + unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + unpack_filepath = os.path.join(unpack_path, tex['file']) + + if do_convert: + if not os.path.isfile(unpack_filepath): + fmt = 'PNG' if new_ext == 'png' else 'JPEG' + arm.utils.convert_image(image, unpack_filepath, file_format=fmt) + else: + + # Write bytes if size is different or file does not exist yet + if image.packed_file is not None: + if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size: + with open(unpack_filepath, 'wb') as f: + f.write(image.packed_file.data) + # Copy non-ascii texture + else: + if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != os.path.getsize(texpath): + shutil.copy(texpath, unpack_filepath) + + arm.assets.add(unpack_filepath) + + else: + if not os.path.isfile(arm.utils.asset_path(filepath)): + log.warn('Material ' + matname + '/' + image.name + ' - file not found(' + filepath + ')') + return None + + if do_convert: + unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + converted_path = os.path.join(unpack_path, tex['file']) + # TODO: delete cache when file changes + if not os.path.isfile(converted_path): + fmt = 'PNG' if new_ext == 'png' else 'JPEG' + arm.utils.convert_image(image, converted_path, file_format=fmt) + arm.assets.add(converted_path) + else: + # Link image path to assets + # TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows + if arm.utils.get_os() == 'win': + s = filepath.rsplit('.', 1) + arm.assets.add(arm.utils.asset_path(s[0] + '.' + s[1].lower())) + else: + arm.assets.add(arm.utils.asset_path(filepath)) + + # if image_format != 'RGBA32': + # tex['format'] = image_format + + rpdat = arm.utils.get_rp() + texfilter = rpdat.arm_texture_filter + if texfilter == 'Anisotropic': + interpolation = 'Smart' + elif texfilter == 'Linear': + interpolation = 'Linear' + elif texfilter == 'Point': + interpolation = 'Closest' + + if interpolation == 'Cubic': # Mipmap linear + tex['mipmap_filter'] = 'linear' + tex['generate_mipmaps'] = True + elif interpolation == 'Smart': # Mipmap anisotropic + tex['min_filter'] = 'anisotropic' + tex['mipmap_filter'] = 'linear' + tex['generate_mipmaps'] = True + elif interpolation == 'Closest': + tex['min_filter'] = 'point' + tex['mag_filter'] = 'point' + # else defaults to linear + + if extension != 'REPEAT': # Extend or clip + tex['u_addressing'] = 'clamp' + tex['v_addressing'] = 'clamp' + + if image.source == 'MOVIE': + tex['source'] = 'movie' + tex['min_filter'] = 'linear' + tex['mag_filter'] = 'linear' + tex['mipmap_filter'] = 'no' + tex['generate_mipmaps'] = False + + return tex + + +def make_texture_from_image_node(image_node: bpy.types.ShaderNodeTexImage, tex_name: str, matname: str = None) -> Optional[Dict[str, Any]]: + if matname is None: + matname = mat_state.material.name + + return make_texture(image_node.image, tex_name, matname, image_node.interpolation, image_node.extension) + + +def is_pow(num): + return ((num & (num - 1)) == 0) and num != 0 + +def is_ascii(s): + return len(s) == len(s.encode()) + +## + +def get_arm_export_tangents(): + return bpy.data.worlds['Arm'].arm_export_tangents + +def safesrc(name): + return arm.utils.safesrc(name) + +def disp_enabled(): + return arm.utils.disp_enabled(arm.make_state.target) + +def assets_add(path): + arm.assets.add(path) + +def assets_add_embedded_data(path): + arm.assets.add_embedded_data(path) + +def tree_name() -> str: + return state.tree_name + +def mat_batch(): + return mat_state.batch + +def mat_bind_texture(tex): + mat_state.bind_textures.append(tex) + +def mat_get_material(): + return mat_state.material + +def mat_get_material_users(): + return mat_state.mat_users diff --git a/blender/arm/material/cycles_functions.py b/blender/arm/material/cycles_functions.py new file mode 100644 index 0000000000..1bd3e340f3 --- /dev/null +++ b/blender/arm/material/cycles_functions.py @@ -0,0 +1,517 @@ +str_tex_proc = """ +// +// By Morgan McGuire @morgan3d, http://graphicscodex.com +float hash_f(const float n) { return fract(sin(n) * 1e4); } +float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } +float hash_f(const vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); } + +float noise(const vec3 x) { + const vec3 step = vec3(110, 241, 171); + + vec3 i = floor(x); + vec3 f = fract(x); + + // For performance, compute the base input to a 1D hash from the integer part of the argument and the + // incremental change to the 1D based on the 3D -> 1D wrapping + float n = dot(i, step); + + vec3 u = f * f * (3.0 - 2.0 * f); + return mix(mix(mix( hash_f(n + dot(step, vec3(0, 0, 0))), hash_f(n + dot(step, vec3(1, 0, 0))), u.x), + mix( hash_f(n + dot(step, vec3(0, 1, 0))), hash_f(n + dot(step, vec3(1, 1, 0))), u.x), u.y), + mix(mix( hash_f(n + dot(step, vec3(0, 0, 1))), hash_f(n + dot(step, vec3(1, 0, 1))), u.x), + mix( hash_f(n + dot(step, vec3(0, 1, 1))), hash_f(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z); +} + +// Shader-code adapted from Blender +// https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_wave.glsl & /gpu_shader_material_fractal_noise.glsl +float fractal_noise(const vec3 p, const float o) +{ + float fscale = 1.0; + float amp = 1.0; + float sum = 0.0; + float octaves = clamp(o, 0.0, 16.0); + int n = int(octaves); + for (int i = 0; i <= n; i++) { + float t = noise(fscale * p); + sum += t * amp; + amp *= 0.5; + fscale *= 2.0; + } + float rmd = octaves - floor(octaves); + if (rmd != 0.0) { + float t = noise(fscale * p); + float sum2 = sum + t * amp; + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1.0); + sum2 *= float(pow(2, n + 1)) / float(pow(2, n + 2) - 1); + return (1.0 - rmd) * sum + rmd * sum2; + } + else { + sum *= float(pow(2, n)) / float(pow(2, n + 1) - 1); + return sum; + } +} +""" + +str_tex_checker = """ +vec3 tex_checker(const vec3 co, const vec3 col1, const vec3 col2, const float scale) { + // Prevent precision issues on unit coordinates + vec3 p = (co + 0.000001 * 0.999999) * scale; + float xi = abs(floor(p.x)); + float yi = abs(floor(p.y)); + float zi = abs(floor(p.z)); + bool check = ((mod(xi, 2.0) == mod(yi, 2.0)) == bool(mod(zi, 2.0))); + return check ? col1 : col2; +} +float tex_checker_f(const vec3 co, const float scale) { + vec3 p = (co + 0.000001 * 0.999999) * scale; + float xi = abs(floor(p.x)); + float yi = abs(floor(p.y)); + float zi = abs(floor(p.z)); + return float((mod(xi, 2.0) == mod(yi, 2.0)) == bool(mod(zi, 2.0))); +} +""" + +str_tex_voronoi = """ +//Shader-code adapted from Blender +//https://github.com/sobotka/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_tex_voronoi.glsl +float voronoi_distance(const vec3 a, const vec3 b, const int metric, const float exponent) +{ + if (metric == 0) // SHD_VORONOI_EUCLIDEAN + { + return distance(a, b); + } + else if (metric == 1) // SHD_VORONOI_MANHATTAN + { + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); + } + else if (metric == 2) // SHD_VORONOI_CHEBYCHEV + { + return max(abs(a.x - b.x), max(abs(a.y - b.y), abs(a.z - b.z))); + } + else if (metric == 3) // SHD_VORONOI_MINKOWSKI + { + return pow(pow(abs(a.x - b.x), exponent) + pow(abs(a.y - b.y), exponent) + + pow(abs(a.z - b.z), exponent), + 1.0 / exponent); + } + else { + return 0.5; + } +} + +vec3 tex_voronoi(const vec3 coord, const float r, const int metric, const int outp, const float scale, const float exp) +{ + float randomness = clamp(r, 0.0, 1.0); + + vec3 scaledCoord = coord * scale; + vec3 cellPosition = floor(scaledCoord); + vec3 localPosition = scaledCoord - cellPosition; + + float minDistance = 8.0; + vec3 targetOffset, targetPosition; + for (int k = -1; k <= 1; k++) { + for (int j = -1; j <= 1; j++) { + for (int i = -1; i <= 1; i++) { + vec3 cellOffset = vec3(float(i), float(j), float(k)); + vec3 pointPosition = cellOffset; + if(randomness != 0.) { + pointPosition += vec3(hash_f(cellPosition+cellOffset), hash_f(cellPosition+cellOffset+972.37), hash_f(cellPosition+cellOffset+342.48)) * randomness;} + float distanceToPoint = voronoi_distance(pointPosition, localPosition, metric, exp); + if (distanceToPoint < minDistance) { + targetOffset = cellOffset; + minDistance = distanceToPoint; + targetPosition = pointPosition; + } + } + } + } + if(outp == 0){return vec3(minDistance);} + else if(outp == 1) { + if(randomness == 0.) {return vec3(hash_f(cellPosition+targetOffset), hash_f(cellPosition+targetOffset+972.37), hash_f(cellPosition+targetOffset+342.48));} + return (targetPosition - targetOffset)/randomness; + } + return (targetPosition + cellPosition) / scale; +} +""" + +# Based on https://www.shadertoy.com/view/4sfGzS +# Copyright © 2013 Inigo Quilez +# The MIT License - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# float tex_noise_f(const vec3 x) { +# vec3 p = floor(x); +# vec3 f = fract(x); +# f = f * f * (3.0 - 2.0 * f); +# vec2 uv = (p.xy + vec2(37.0, 17.0) * p.z) + f.xy; +# vec2 rg = texture(snoise256, (uv + 0.5) / 256.0).yx; +# return mix(rg.x, rg.y, f.z); +# } +# By Morgan McGuire @morgan3d, http://graphicscodex.com Reuse permitted under the BSD license. +# https://www.shadertoy.com/view/4dS3Wd +str_tex_noise = """ +float tex_noise(const vec3 p, const float detail, const float distortion) { + vec3 pk = p; + if (distortion != 0.0) { + pk += vec3(noise(p) * distortion); + } + return fractal_noise(pk, detail); +} +""" + +# Based on noise created by Nikita Miropolskiy, nikat/2013 +# Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License +str_tex_musgrave = """ +vec3 random3(const vec3 c) { + float j = 4096.0 * sin(dot(c, vec3(17.0, 59.4, 15.0))); + vec3 r; + r.z = fract(512.0 * j); + j *= 0.125; + r.x = fract(512.0 * j); + j *= 0.125; + r.y = fract(512.0 * j); + return r - 0.5; +} +float tex_musgrave_f(const vec3 p) { + const float F3 = 0.3333333; + const float G3 = 0.1666667; + vec3 s = floor(p + dot(p, vec3(F3))); + vec3 x = p - s + dot(s, vec3(G3)); + vec3 e = step(vec3(0.0), x - x.yzx); + vec3 i1 = e*(1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy*(1.0 - e); + vec3 x1 = x - i1 + G3; + vec3 x2 = x - i2 + 2.0*G3; + vec3 x3 = x - 1.0 + 3.0*G3; + vec4 w, d; + w.x = dot(x, x); + w.y = dot(x1, x1); + w.z = dot(x2, x2); + w.w = dot(x3, x3); + w = max(0.6 - w, 0.0); + d.x = dot(random3(s), x); + d.y = dot(random3(s + i1), x1); + d.z = dot(random3(s + i2), x2); + d.w = dot(random3(s + 1.0), x3); + w *= w; + w *= w; + d *= w; + return clamp(dot(d, vec4(52.0)), 0.0, 1.0); +} +""" + +# col: the incoming color +# shift: a vector containing the hue shift, the saturation modificator, the value modificator and the mix factor in this order +# this does the following: +# make rgb col to hsv +# apply hue shift through addition, sat/val through multiplication +# return an rgb color, mixed with the original one +str_hue_sat = """ +vec3 hsv_to_rgb(const vec3 c) { + const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} +vec3 rgb_to_hsv(const vec3 c) { + const vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} +vec3 hue_sat(const vec3 col, const vec4 shift) { + vec3 hsv = rgb_to_hsv(col); + hsv.x += shift.x; + hsv.y *= shift.y; + hsv.z *= shift.z; + return mix(hsv_to_rgb(hsv), col, shift.w); +} +""" + +# https://twitter.com/Donzanoid/status/903424376707657730 +str_wavelength_to_rgb = """ +vec3 wavelength_to_rgb(const float t) { + vec3 r = t * 2.1 - vec3(1.8, 1.14, 0.3); + return 1.0 - r * r; +} +""" + +str_tex_magic = """ +vec3 tex_magic(const vec3 p) { + float a = 1.0 - (sin(p.x) + sin(p.y)); + float b = 1.0 - sin(p.x - p.y); + float c = 1.0 - sin(p.x + p.y); + return vec3(a, b, c); +} +float tex_magic_f(const vec3 p) { + vec3 c = tex_magic(p); + return (c.x + c.y + c.z) / 3.0; +} +""" + +str_tex_brick = """ +vec3 tex_brick(vec3 p, const vec3 c1, const vec3 c2, const vec3 c3) { + p /= vec3(0.9, 0.49, 0.49) / 2; + if (fract(p.y * 0.5) > 0.5) p.x += 0.5; + p = fract(p); + vec3 b = step(p, vec3(0.95, 0.9, 0.9)); + return mix(c3, c1, b.x * b.y * b.z); +} +float tex_brick_f(vec3 p) { + p /= vec3(0.9, 0.49, 0.49) / 2; + if (fract(p.y * 0.5) > 0.5) p.x += 0.5; + p = fract(p); + vec3 b = step(p, vec3(0.95, 0.9, 0.9)); + return mix(1.0, 0.0, b.x * b.y * b.z); +} +""" + +str_tex_wave = """ +float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) { + float n; + if(type == 0) n = (p.x + p.y + p.z) * 9.5; + else n = length(p) * 13.0; + if(dist != 0.0) n += dist * fractal_noise(p * detail_scale, detail) * 2.0 - 1.0; + if(profile == 0) { return 0.5 + 0.5 * sin(n - PI); } + else { + n /= 2.0 * PI; + return n - floor(n); + } +} +""" + +str_brightcontrast = """ +vec3 brightcontrast(const vec3 col, const float bright, const float contr) { + float a = 1.0 + contr; + float b = bright - contr * 0.5; + return max(a * col + b, 0.0); +} +""" + +# https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/ +# dielectric-dielectric +# approx pow(1.0 - dotNV, 7.25 / ior) +str_fresnel = """ +float fresnel(float eta, float c) { + float g = eta * eta - 1.0 + c * c; + if (g < 0.0) return 1.0; + g = sqrt(g); + float a = (g - c) / (g + c); + float b = ((g + c) * c - 1.0) / ((g - c) * c + 1.0); + return 0.5 * a * a * (1.0 + b * b); +} +""" + +# Save division like Blender does it. If dividing by 0, the result is 0. +# https://github.com/blender/blender/blob/df1e9b662bd6938f74579cea9d30341f3b6dd02b/intern/cycles/kernel/shaders/node_vector_math.osl +str_safe_divide = """ +vec3 safe_divide(const vec3 a, const vec3 b) { +\treturn vec3((b.x != 0.0) ? a.x / b.x : 0.0, +\t (b.y != 0.0) ? a.y / b.y : 0.0, +\t (b.z != 0.0) ? a.z / b.z : 0.0); +} +""" + +# https://github.com/blender/blender/blob/df1e9b662bd6938f74579cea9d30341f3b6dd02b/intern/cycles/kernel/shaders/node_vector_math.osl +str_project = """ +vec3 project(const vec3 v, const vec3 v_proj) { +\tfloat lenSquared = dot(v_proj, v_proj); +\treturn (lenSquared != 0.0) ? (dot(v, v_proj) / lenSquared) * v_proj : vec3(0); +} +""" + +# Adapted from godot engine math_funcs.h +str_wrap = """ +float wrap(const float value, const float max, const float min) { +\tfloat range = max - min; +\treturn (range != 0.0) ? value - (range * floor((value - min) / range)) : min; +} +vec3 wrap(const vec3 value, const vec3 max, const vec3 min) { +\treturn vec3(wrap(value.x, max.x, min.x), +\t wrap(value.y, max.y, min.y), +\t wrap(value.z, max.z, min.z)); +} +""" + +str_blackbody = """ +vec3 blackbody(const float temperature){ + + vec3 rgb = vec3(0.0, 0.0, 0.0); + + vec3 r = vec3(0.0, 0.0, 0.0); + vec3 g = vec3(0.0, 0.0, 0.0); + vec3 b = vec3(0.0, 0.0, 0.0); + + float t_inv = float(1.0 / temperature); + + if (temperature >= 12000.0) { + + rgb = vec3(0.826270103, 0.994478524, 1.56626022); + + } else if(temperature < 965.0) { + + rgb = vec3(4.70366907, 0.0, 0.0); + + } else { + + if (temperature >= 6365.0) { + vec3 r = vec3(3.78765709e+03, 9.36026367e-06, 3.98995841e-01); + vec3 g = vec3(-5.00279505e+02, -4.59745390e-06, 1.09090465e+00); + vec4 b = vec4(6.72595954e-13, -2.73059993e-08, 4.24068546e-04, -7.52204323e-01); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } else if (temperature >= 3315.0) { + vec3 r = vec3(4.60124770e+03, 2.89727618e-05, 1.48001316e-01); + vec3 g = vec3(-1.18134453e+03, -2.18913373e-05, 1.30656109e+00); + vec4 b = vec4(-2.22463426e-13, -1.55078698e-08, 3.81675160e-04, -7.30646033e-01); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } else if (temperature >= 1902.0) { + vec3 r = vec3(4.66849800e+03, 2.85655028e-05, 1.29075375e-01); + vec3 g = vec3(-1.42546105e+03, -4.01730887e-05, 1.44002695e+00); + vec4 b = vec4(-2.02524603e-11, 1.79435860e-07, -2.60561875e-04, -1.41761141e-02); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } else if (temperature >= 1449.0) { + vec3 r = vec3(4.10671449e+03, -8.61949938e-05, 6.41423749e-01); + vec3 g = vec3(-1.22075471e+03, 2.56245413e-05, 1.20753416e+00); + vec4 b = vec4(0.0, 0.0, 0.0, 0.0); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } else if (temperature >= 1167.0) { + vec3 r = vec3(3.37763626e+03, -4.34581697e-04, 1.64843306e+00); + vec3 g = vec3(-1.00402363e+03, 1.29189794e-04, 9.08181524e-01); + vec4 b = vec4(0.0, 0.0, 0.0, 0.0); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } else { + vec3 r = vec3(2.52432244e+03, -1.06185848e-03, 3.11067539e+00); + vec3 g = vec3(-7.50343014e+02, 3.15679613e-04, 4.73464526e-01); + vec4 b = vec4(0.0, 0.0, 0.0, 0.0); + + rgb = vec3(r.r * t_inv + r.g * temperature + r.b, g.r * t_inv + g.g * temperature + g.b, ((b.r * temperature + b.g) * temperature + b.b) * temperature + b.a ); + + } + } + + return rgb; + +} +""" + +# Adapted from https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/gpu/shaders/material/gpu_shader_material_map_range.glsl +str_map_range_linear = """ +float map_range_linear(const float value, const float fromMin, const float fromMax, const float toMin, const float toMax) { + if (fromMax != fromMin) { + return float(toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin)); + } + else { + return float(0.0); + } +} +""" + +str_map_range_stepped = """ +float map_range_stepped(const float value, const float fromMin, const float fromMax, const float toMin, const float toMax, const float steps) { + if (fromMax != fromMin) { + float factor = (value - fromMin) / (fromMax - fromMin); + factor = (steps > 0.0) ? floor(factor * (steps + 1.0)) / steps : 0.0; + return float(toMin + factor * (toMax - toMin)); + } + else { + return float(0.0); + } +} +""" + +str_map_range_smoothstep = """ +float map_range_smoothstep(const float value, const float fromMin, const float fromMax, const float toMin, const float toMax) +{ + if (fromMax != fromMin) { + float factor = (fromMin > fromMax) ? 1.0 - smoothstep(fromMax, fromMin, value) : + smoothstep(fromMin, fromMax, value); + return float(toMin + factor * (toMax - toMin)); + } + else { + return float(0.0); + } +} +""" + +str_map_range_smootherstep = """ +float safe_divide(float a, float b) +{ + return (b != 0.0) ? a / b : 0.0; +} + +float smootherstep(float edge0, float edge1, float x) +{ + x = clamp(safe_divide((x - edge0), (edge1 - edge0)), 0.0, 1.0); + return x * x * x * (x * (x * 6.0 - 15.0) + 10.0); +} + +float map_range_smootherstep(const float value, const float fromMin, const float fromMax, const float toMin, const float toMax) { + if (fromMax != fromMin) { + float factor = (fromMin > fromMax) ? 1.0 - smootherstep(fromMax, fromMin, value) : + smootherstep(fromMin, fromMax, value); + return float(toMin + factor * (toMax - toMin)); + } + else { + return float(0.0); + } +} +""" + +str_rotate_around_axis = """ +vec3 rotate_around_axis(const vec3 p, const vec3 axis, const float angle) +{ + float costheta = cos(angle); + float sintheta = sin(angle); + vec3 r; + + r.x = ((costheta + (1.0 - costheta) * axis.x * axis.x) * p.x) + + (((1.0 - costheta) * axis.x * axis.y - axis.z * sintheta) * p.y) + + (((1.0 - costheta) * axis.x * axis.z + axis.y * sintheta) * p.z); + + r.y = (((1.0 - costheta) * axis.x * axis.y + axis.z * sintheta) * p.x) + + ((costheta + (1.0 - costheta) * axis.y * axis.y) * p.y) + + (((1.0 - costheta) * axis.y * axis.z - axis.x * sintheta) * p.z); + + r.z = (((1.0 - costheta) * axis.x * axis.z - axis.y * sintheta) * p.x) + + (((1.0 - costheta) * axis.y * axis.z + axis.x * sintheta) * p.y) + + ((costheta + (1.0 - costheta) * axis.z * axis.z) * p.z); + + return r; +} +""" + +str_euler_to_mat3 = """ +mat3 euler_to_mat3(vec3 euler) +{ + float cx = cos(euler.x); + float cy = cos(euler.y); + float cz = cos(euler.z); + float sx = sin(euler.x); + float sy = sin(euler.y); + float sz = sin(euler.z); + + mat3 mat; + mat[0][0] = cy * cz; + mat[0][1] = cy * sz; + mat[0][2] = -sy; + + mat[1][0] = sy * sx * cz - cx * sz; + mat[1][1] = sy * sx * sz + cx * cz; + mat[1][2] = cy * sx; + + mat[2][0] = sy * cx * cz + sx * sz; + mat[2][1] = sy * cx * sz - sx * cz; + mat[2][2] = cy * cx; + return mat; +} +""" \ No newline at end of file diff --git a/blender/arm/material/cycles_nodes/__init__.py b/blender/arm/material/cycles_nodes/__init__.py new file mode 100644 index 0000000000..39f5ff1c50 --- /dev/null +++ b/blender/arm/material/cycles_nodes/__init__.py @@ -0,0 +1,5 @@ +import glob +from os.path import dirname, basename, isfile + +modules = glob.glob(dirname(__file__) + "/*.py") +__all__ = [basename(f)[:-3] for f in modules if isfile(f)] diff --git a/blender/arm/material/cycles_nodes/nodes_color.py b/blender/arm/material/cycles_nodes/nodes_color.py new file mode 100644 index 0000000000..bb29506cb1 --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_color.py @@ -0,0 +1,130 @@ +import bpy + +import arm +import arm.log as log +import arm.material.cycles as c +import arm.material.cycles_functions as c_functions +from arm.material.parser_state import ParserState +from arm.material.shader import floatstr, vec3str + +if arm.is_reload(__name__): + log = arm.reload_module(log) + c = arm.reload_module(c) + c_functions = arm.reload_module(c_functions) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import floatstr, vec3str +else: + arm.enable_reload(__name__) + + +def parse_brightcontrast(node: bpy.types.ShaderNodeBrightContrast, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + out_col = c.parse_vector_input(node.inputs[0]) + bright = c.parse_value_input(node.inputs[1]) + contr = c.parse_value_input(node.inputs[2]) + + state.curshader.add_function(c_functions.str_brightcontrast) + + return 'brightcontrast({0}, {1}, {2})'.format(out_col, bright, contr) + + +def parse_gamma(node: bpy.types.ShaderNodeGamma, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + out_col = c.parse_vector_input(node.inputs[0]) + gamma = c.parse_value_input(node.inputs[1]) + + return 'pow({0}, vec3({1}))'.format(out_col, gamma) + + +def parse_huesat(node: bpy.types.ShaderNodeHueSaturation, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + state.curshader.add_function(c_functions.str_hue_sat) + hue = c.parse_value_input(node.inputs[0]) + sat = c.parse_value_input(node.inputs[1]) + val = c.parse_value_input(node.inputs[2]) + fac = c.parse_value_input(node.inputs[3]) + col = c.parse_vector_input(node.inputs[4]) + + return f'hue_sat({col}, vec4({hue}-0.5, {sat}, {val}, 1.0-{fac}))' + + +def parse_invert(node: bpy.types.ShaderNodeInvert, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + fac = c.parse_value_input(node.inputs[0]) + out_col = c.parse_vector_input(node.inputs[1]) + + return f'mix({out_col}, vec3(1.0) - ({out_col}), {fac})' + + +def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + col1 = c.parse_vector_input(node.inputs[1]) + col2 = c.parse_vector_input(node.inputs[2]) + + # Store factor in variable for linked factor input + if node.inputs[0].is_linked: + fac = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() + state.curshader.write('float {0} = {1};'.format(fac, c.parse_value_input(node.inputs[0]))) + else: + fac = c.parse_value_input(node.inputs[0]) + + # TODO: Do not mix if factor is constant 0.0 or 1.0? + + blend = node.blend_type + if blend == 'MIX': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) + elif blend == 'ADD': + out_col = 'mix({0}, {0} + {1}, {2})'.format(col1, col2, fac) + elif blend == 'MULTIPLY': + out_col = 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac) + elif blend == 'SUBTRACT': + out_col = 'mix({0}, {0} - {1}, {2})'.format(col1, col2, fac) + elif blend == 'SCREEN': + out_col = '(vec3(1.0) - (vec3(1.0 - {2}) + {2} * (vec3(1.0) - {1})) * (vec3(1.0) - {0}))'.format(col1, col2, fac) + elif blend == 'DIVIDE': + out_col = '(vec3((1.0 - {2}) * {0} + {2} * {0} / {1}))'.format(col1, col2, fac) + elif blend == 'DIFFERENCE': + out_col = 'mix({0}, abs({0} - {1}), {2})'.format(col1, col2, fac) + elif blend == 'DARKEN': + out_col = 'min({0}, {1} * {2})'.format(col1, col2, fac) + elif blend == 'LIGHTEN': + out_col = 'max({0}, {1} * {2})'.format(col1, col2, fac) + elif blend == 'OVERLAY': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'DODGE': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'BURN': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'HUE': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'SATURATION': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'VALUE': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'COLOR': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + elif blend == 'SOFT_LIGHT': + out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))));'.format(col1, col2, fac) + elif blend == 'LINEAR_LIGHT': + out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix + # out_col = '({0} + {2} * (2.0 * ({1} - vec3(0.5))))'.format(col1, col2, fac_var) + else: + log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') + return col1 + + if node.use_clamp: + return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col) + return out_col + + +def parse_curvergb(node: bpy.types.ShaderNodeRGBCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + fac = c.parse_value_input(node.inputs[0]) + vec = c.parse_vector_input(node.inputs[1]) + curves = node.mapping.curves + name = c.node_name(node.name) + # mapping.curves[0].points[0].handle_type + return '(sqrt(vec3({0}, {1}, {2}) * vec3({4}, {5}, {6})) * {3})'.format( + c.vector_curve(name + '0', vec + '.x', curves[0].points), c.vector_curve(name + '1', vec + '.y', curves[1].points), c.vector_curve(name + '2', vec + '.z', curves[2].points), fac, + c.vector_curve(name + '3a', vec + '.x', curves[3].points), c.vector_curve(name + '3b', vec + '.y', curves[3].points), c.vector_curve(name + '3c', vec + '.z', curves[3].points)) + + +def parse_lightfalloff(node: bpy.types.ShaderNodeLightFalloff, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + # https://github.com/blender/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_light_falloff.glsl + return c.parse_value_input(node.inputs['Strength']) diff --git a/blender/arm/material/cycles_nodes/nodes_converter.py b/blender/arm/material/cycles_nodes/nodes_converter.py new file mode 100644 index 0000000000..044834ef5f --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_converter.py @@ -0,0 +1,397 @@ +from typing import Union + +import bpy + +import arm +import arm.log as log +import arm.material.cycles as c +import arm.material.cycles_functions as c_functions +from arm.material.parser_state import ParserPass, ParserState +from arm.material.shader import floatstr, vec3str + +if arm.is_reload(__name__): + log = arm.reload_module(log) + c = arm.reload_module(c) + c_functions = arm.reload_module(c_functions) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import floatstr, vec3str +else: + arm.enable_reload(__name__) + + +def parse_maprange(node: bpy.types.ShaderNodeMapRange, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + + interp = node.interpolation_type + + value: str = c.parse_value_input(node.inputs[0]) if node.inputs[0].is_linked else c.to_vec1(node.inputs[0].default_value) + fromMin = c.parse_value_input(node.inputs[1]) + fromMax = c.parse_value_input(node.inputs[2]) + toMin = c.parse_value_input(node.inputs[3]) + toMax = c.parse_value_input(node.inputs[4]) + + if interp == "LINEAR": + + state.curshader.add_function(c_functions.str_map_range_linear) + return f'map_range_linear({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + + elif interp == "STEPPED": + + steps = float(c.parse_value_input(node.inputs[5])) + state.curshader.add_function(c_functions.str_map_range_stepped) + return f'map_range_stepped({value}, {fromMin}, {fromMax}, {toMin}, {toMax}, {steps})' + + elif interp == "SMOOTHSTEP": + + state.curshader.add_function(c_functions.str_map_range_smoothstep) + return f'map_range_smoothstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + + elif interp == "SMOOTHERSTEP": + + state.curshader.add_function(c_functions.str_map_range_smootherstep) + return f'map_range_smootherstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + +def parse_blackbody(node: bpy.types.ShaderNodeBlackbody, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + + t = c.parse_value_input(node.inputs[0]) + + state.curshader.add_function(c_functions.str_blackbody) + return f'blackbody({t})' + +def parse_clamp(node: bpy.types.ShaderNodeClamp, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + value = c.parse_value_input(node.inputs['Value']) + minVal = c.parse_value_input(node.inputs['Min']) + maxVal = c.parse_value_input(node.inputs['Max']) + + if node.clamp_type == 'MINMAX': + # Condition is minVal < maxVal, otherwise use 'RANGE' type + return f'clamp({value}, {minVal}, {maxVal})' + + elif node.clamp_type == 'RANGE': + return f'{minVal} < {maxVal} ? clamp({value}, {minVal}, {maxVal}) : clamp({value}, {maxVal}, {minVal})' + + else: + log.warn(f'Clamp node: unsupported clamp type {node.clamp_type}.') + return value + + +def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Alpha (TODO: make ColorRamp calculation vec4-based and split afterwards) + if out_socket == node.outputs[1]: + return '1.0' + + input_fac: bpy.types.NodeSocket = node.inputs[0] + + fac: str = c.parse_value_input(input_fac) if input_fac.is_linked else c.to_vec1(input_fac.default_value) + interp = node.color_ramp.interpolation + elems = node.color_ramp.elements + + if len(elems) == 1: + return c.to_vec3(elems[0].color) + + # Write color array + # The last entry is included twice so that the interpolation + # between indices works (no out of bounds error) + cols_var = c.node_name(node.name).upper() + '_COLS' + + if state.current_pass == ParserPass.REGULAR: + cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems) + cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})' + state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1) + + fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() + state.curshader.write(f'float {fac_var} = {fac};') + + # Get index of the nearest left element relative to the factor + index = '0 + ' + index += ' + '.join([f'(({fac_var} > {elems[i].position}) ? 1 : 0)' for i in range(1, len(elems))]) + + # Write index + index_var = c.node_name(node.name) + '_i' + state.get_parser_pass_suffix() + state.curshader.write(f'int {index_var} = {index};') + + if interp == 'CONSTANT': + return f'{cols_var}[{index_var}]' + + # Linear interpolation + else: + # Write factor array + facs_var = c.node_name(node.name).upper() + '_FACS' + if state.current_pass == ParserPass.REGULAR: + facs_entries = ', '.join(str(elem.position) for elem in elems) + # Add one more entry at the rightmost position so that the + # interpolation between indices works (no out of bounds error) + facs_entries += ', 1.0' + state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1) + + # Mix color + prev_stop_fac = f'{facs_var}[{index_var}]' + next_stop_fac = f'{facs_var}[{index_var} + 1]' + prev_stop_col = f'{cols_var}[{index_var}]' + next_stop_col = f'{cols_var}[{index_var} + 1]' + rel_pos = f'({fac_var} - {prev_stop_fac}) * (1.0 / ({next_stop_fac} - {prev_stop_fac}))' + return f'mix({prev_stop_col}, {next_stop_col}, max({rel_pos}, 0.0))' + + +def parse_combine_color(node: bpy.types.ShaderNodeCombineColor, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + if node.mode == 'RGB': + return parse_combrgb(node, out_socket, state) + elif node.mode == 'HSV': + return parse_combhsv(node, out_socket, state) + elif node.mode == 'HSL': + log.warn('Combine Color node: HSL mode is not supported, using default value') + return c.to_vec3((0.0, 0.0, 0.0)) + + +def parse_combhsv(node: bpy.types.ShaderNodeCombineHSV, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + state.curshader.add_function(c_functions.str_hue_sat) + h = c.parse_value_input(node.inputs[0]) + s = c.parse_value_input(node.inputs[1]) + v = c.parse_value_input(node.inputs[2]) + return f'hsv_to_rgb(vec3({h}, {s}, {v}))' + + +def parse_combrgb(node: bpy.types.ShaderNodeCombineRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + r = c.parse_value_input(node.inputs[0]) + g = c.parse_value_input(node.inputs[1]) + b = c.parse_value_input(node.inputs[2]) + return f'vec3({r}, {g}, {b})' + + +def parse_combxyz(node: bpy.types.ShaderNodeCombineXYZ, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + x = c.parse_value_input(node.inputs[0]) + y = c.parse_value_input(node.inputs[1]) + z = c.parse_value_input(node.inputs[2]) + return f'vec3({x}, {y}, {z})' + + +def parse_wavelength(node: bpy.types.ShaderNodeWavelength, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + state.curshader.add_function(c_functions.str_wavelength_to_rgb) + wl = c.parse_value_input(node.inputs[0]) + # Roughly map to cycles - 450 to 600 nanometers + return f'wavelength_to_rgb(({wl} - 450.0) / 150.0)' + + +def parse_vectormath(node: bpy.types.ShaderNodeVectorMath, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + op = node.operation + + vec1 = c.parse_vector_input(node.inputs[0]) + vec2 = c.parse_vector_input(node.inputs[1]) + + if out_socket.type == 'VECTOR': + if op == 'ADD': + return f'({vec1} + {vec2})' + elif op == 'SUBTRACT': + return f'({vec1} - {vec2})' + elif op == 'MULTIPLY': + return f'({vec1} * {vec2})' + elif op == 'DIVIDE': + state.curshader.add_function(c_functions.str_safe_divide) + return f'safe_divide({vec1}, {vec2})' + + elif op == 'NORMALIZE': + return f'normalize({vec1})' + elif op == 'SCALE': + # Scale is input 3 despite being visually on another position (see the python tooltip in Blender) + scale = c.parse_value_input(node.inputs[3]) + return f'{vec1} * {scale}' + + elif op == 'REFLECT': + return f'reflect({vec1}, normalize({vec2}))' + elif op == 'PROJECT': + state.curshader.add_function(c_functions.str_project) + return f'project({vec1}, {vec2})' + elif op == 'CROSS_PRODUCT': + return f'cross({vec1}, {vec2})' + + elif op == 'SINE': + return f'sin({vec1})' + elif op == 'COSINE': + return f'cos({vec1})' + elif op == 'TANGENT': + return f'tan({vec1})' + + elif op == 'MODULO': + return f'mod({vec1}, {vec2})' + elif op == 'FRACTION': + return f'fract({vec1})' + + elif op == 'SNAP': + state.curshader.add_function(c_functions.str_safe_divide) + return f'floor(safe_divide({vec1}, {vec2})) * {vec2}' + elif op == 'WRAP': + vec3 = c.parse_vector_input(node.inputs[2]) + state.curshader.add_function(c_functions.str_wrap) + return f'wrap({vec1}, {vec2}, {vec3})' + elif op == 'CEIL': + return f'ceil({vec1})' + elif op == 'FLOOR': + return f'floor({vec1})' + elif op == 'MAXIMUM': + return f'max({vec1}, {vec2})' + elif op == 'MINIMUM': + return f'min({vec1}, {vec2})' + elif op == 'ABSOLUTE': + return f'abs({vec1})' + + log.warn(f'Vectormath node: unsupported operation {node.operation}.') + return vec1 + + # Float output + if op == 'DOT_PRODUCT': + return f'dot({vec1}, {vec2})' + elif op == 'DISTANCE': + return f'distance({vec1}, {vec2})' + elif op == 'LENGTH': + return f'length({vec1})' + + log.warn(f'Vectormath node: unsupported operation {node.operation}.') + return '0.0' + + +def parse_math(node: bpy.types.ShaderNodeMath, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + val1 = c.parse_value_input(node.inputs[0]) + val2 = c.parse_value_input(node.inputs[1]) + op = node.operation + if op == 'ADD': + out_val = '({0} + {1})'.format(val1, val2) + elif op == 'SUBTRACT': + out_val = '({0} - {1})'.format(val1, val2) + elif op == 'MULTIPLY': + out_val = '({0} * {1})'.format(val1, val2) + elif op == 'DIVIDE': + out_val = '({0} / {1})'.format(val1, val2) + elif op == 'MULTIPLY_ADD': + val3 = c.parse_value_input(node.inputs[2]) + out_val = '({0} * {1} + {2})'.format(val1, val2, val3) + elif op == 'POWER': + out_val = 'pow({0}, {1})'.format(val1, val2) + elif op == 'LOGARITHM': + out_val = 'log({0})'.format(val1) + elif op == 'SQRT': + out_val = 'sqrt({0})'.format(val1) + elif op == 'INVERSE_SQRT': + out_val = 'inversesqrt({0})'.format(val1) + elif op == 'ABSOLUTE': + out_val = 'abs({0})'.format(val1) + elif op == 'EXPONENT': + out_val = 'exp({0})'.format(val1) + elif op == 'MINIMUM': + out_val = 'min({0}, {1})'.format(val1, val2) + elif op == 'MAXIMUM': + out_val = 'max({0}, {1})'.format(val1, val2) + elif op == 'LESS_THAN': + out_val = 'float({0} < {1})'.format(val1, val2) + elif op == 'GREATER_THAN': + out_val = 'float({0} > {1})'.format(val1, val2) + elif op == 'SIGN': + out_val = 'sign({0})'.format(val1) + elif op == 'COMPARE': + val3 = c.parse_value_input(node.inputs[2]) + out_val = 'float((abs({0} - {1}) <= max({2}, 1e-5)) ? 1.0 : 0.0)'.format(val1, val2, val3) + elif op == 'SMOOTH_MIN': + val3 = c.parse_value_input(node.inputs[2]) + out_val = 'float(float({2} != 0.0 ? min({0},{1}) - (max({2} - abs({0} - {1}), 0.0) / {2}) * (max({2} - abs({0} - {1}), 0.0) / {2}) * (max({2} - abs({0} - {1}), 0.0) / {2}) * {2} * (1.0 / 6.0) : min({0}, {1})))'.format(val1, val2, val3) + elif op == 'SMOOTH_MAX': + val3 = c.parse_value_input(node.inputs[2]) + out_val = 'float(0-(float({2} != 0.0 ? min(-{0},-{1}) - (max({2} - abs(-{0} - (-{1})), 0.0) / {2}) * (max({2} - abs(-{0} - (-{1})), 0.0) / {2}) * (max({2} - abs(-{0} - (-{1})), 0.0) / {2}) * {2} * (1.0 / 6.0) : min(-{0}, (-{1})))))'.format(val1, val2, val3) + elif op == 'ROUND': + # out_val = 'round({0})'.format(val1) + out_val = 'floor({0} + 0.5)'.format(val1) + elif op == 'FLOOR': + out_val = 'floor({0})'.format(val1) + elif op == 'CEIL': + out_val = 'ceil({0})'.format(val1) + elif op == 'TRUNC': + out_val = 'trunc({0})'.format(val1) + elif op == 'FRACT': + out_val = 'fract({0})'.format(val1) + elif op == 'MODULO': + # out_val = 'float({0} % {1})'.format(val1, val2) + out_val = 'mod({0}, {1})'.format(val1, val2) + elif op == 'WRAP': + val3 = c.parse_value_input(node.inputs[2]) + out_val = 'float((({1}-{2}) != 0.0) ? {0} - (({1}-{2}) * floor(({0} - {2}) / ({1}-{2}))) : {2})'.format(val1, val2, val3) + elif op == 'SNAP': + out_val = 'floor(({1} != 0.0) ? {0} / {1} : 0.0) * {1}'.format(val1, val2) + elif op == 'PINGPONG': + out_val = 'float(({1} != 0.0) ? abs(fract(({0} - {1}) / ({1} * 2.0)) * {1} * 2.0 - {1}) : 0.0)'.format(val1, val2) + elif op == 'SINE': + out_val = 'sin({0})'.format(val1) + elif op == 'COSINE': + out_val = 'cos({0})'.format(val1) + elif op == 'TANGENT': + out_val = 'tan({0})'.format(val1) + elif op == 'ARCSINE': + out_val = 'asin({0})'.format(val1) + elif op == 'ARCCOSINE': + out_val = 'acos({0})'.format(val1) + elif op == 'ARCTANGENT': + out_val = 'atan({0})'.format(val1) + elif op == 'ARCTAN2': + out_val = 'atan({0}, {1})'.format(val1, val2) + elif op == 'SINH': + out_val = 'sinh({0})'.format(val1) + elif op == 'COSH': + out_val = 'cosh({0})'.format(val1) + elif op == 'TANH': + out_val = 'tanh({0})'.format(val1) + elif op == 'RADIANS': + out_val = 'radians({0})'.format(val1) + elif op == 'DEGREES': + out_val = 'degrees({0})'.format(val1) + + if node.use_clamp: + return 'clamp({0}, 0.0, 1.0)'.format(out_val) + else: + return out_val + + +def parse_rgbtobw(node: bpy.types.ShaderNodeRGBToBW, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + return c.rgb_to_bw(c.parse_vector_input(node.inputs[0])) + + +def parse_separate_color(node: bpy.types.ShaderNodeSeparateColor, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + if node.mode == 'RGB': + return parse_seprgb(node, out_socket, state) + elif node.mode == 'HSV': + return parse_sephsv(node, out_socket, state) + elif node.mode == 'HSL': + log.warn('Separate Color node: HSL mode is not supported, using default value') + return '0.0' + + +def parse_sephsv(node: bpy.types.ShaderNodeSeparateHSV, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + state.curshader.add_function(c_functions.str_hue_sat) + + hsv_var = c.node_name(node.name) + '_hsv' + state.get_parser_pass_suffix() + if not state.curshader.contains(hsv_var): # Already written if a second output is parsed + state.curshader.write(f'const vec3 {hsv_var} = rgb_to_hsv({c.parse_vector_input(node.inputs["Color"])}.rgb);') + + if out_socket == node.outputs[0]: + return f'{hsv_var}.x' + elif out_socket == node.outputs[1]: + return f'{hsv_var}.y' + elif out_socket == node.outputs[2]: + return f'{hsv_var}.z' + + +def parse_seprgb(node: bpy.types.ShaderNodeSeparateRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + col = c.parse_vector_input(node.inputs[0]) + if out_socket == node.outputs[0]: + return '{0}.r'.format(col) + elif out_socket == node.outputs[1]: + return '{0}.g'.format(col) + elif out_socket == node.outputs[2]: + return '{0}.b'.format(col) + + +def parse_sepxyz(node: bpy.types.ShaderNodeSeparateXYZ, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + vec = c.parse_vector_input(node.inputs[0]) + if out_socket == node.outputs[0]: + return '{0}.x'.format(vec) + elif out_socket == node.outputs[1]: + return '{0}.y'.format(vec) + elif out_socket == node.outputs[2]: + return '{0}.z'.format(vec) diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py new file mode 100644 index 0000000000..d1c502a88a --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -0,0 +1,426 @@ +from typing import Union + +import bpy +import mathutils + +import arm.log as log +import arm.material.cycles as c +import arm.material.cycles_functions as c_functions +import arm.material.mat_state as mat_state +from arm.material.parser_state import ParserState, ParserContext +from arm.material.shader import floatstr, vec3str +import arm.utils + +if arm.is_reload(__name__): + log = arm.reload_module(log) + c = arm.reload_module(c) + c_functions = arm.reload_module(c_functions) + mat_state = arm.reload_module(mat_state) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState, ParserContext + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import floatstr, vec3str + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + out_type = 'float' if out_socket.type == 'VALUE' else 'vec3' + + if node.attribute_name == 'time': + state.curshader.add_uniform('float time', link='_time') + + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('time', from_type='float', to_type=out_type) + + # UV maps (higher priority) and vertex colors + if node.attribute_type == 'GEOMETRY': + + # Alpha output. Armory doesn't support vertex colors with alpha + # values yet and UV maps don't have an alpha channel + if out_socket == node.outputs[3]: + return '1.0' + + # UV maps + mat = c.mat_get_material() + mat_users = c.mat_get_material_users() + + if mat_users is not None and mat in mat_users: + mat_user = mat_users[mat][0] + + # Curves don't have uv layers, so check that first + if hasattr(mat_user.data, 'uv_layers'): + lays = mat_user.data.uv_layers + + # First UV map referenced + if len(lays) > 0 and node.attribute_name == lays[0].name: + state.con.add_elem('tex', 'short2norm') + state.dxdy_varying_input_value = True + return c.cast_value('vec3(texCoord.x, 1.0 - texCoord.y, 0.0)', from_type='vec3', to_type=out_type) + + # Second UV map referenced + elif len(lays) > 1 and node.attribute_name == lays[1].name: + state.con.add_elem('tex1', 'short2norm') + state.dxdy_varying_input_value = True + return c.cast_value('vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)', from_type='vec3', to_type=out_type) + + # Vertex colors + # TODO: support multiple vertex color sets + state.con.add_elem('col', 'short4norm') + state.dxdy_varying_input_value = True + return c.cast_value('vcolor', from_type='vec3', to_type=out_type) + + # Check object properties + # see https://developer.blender.org/rB6fdcca8de6 for reference + mat = c.mat_get_material() + mat_users = c.mat_get_material_users() + if mat_users is not None and mat in mat_users: + # Use first material user for now... + mat_user = mat_users[mat][0] + + val = None + # Custom properties first + if node.attribute_name in mat_user: + val = mat_user[node.attribute_name] + # Blender properties + elif hasattr(mat_user, node.attribute_name): + val = getattr(mat_user, node.attribute_name) + + if val is not None: + if isinstance(val, float): + return c.cast_value(str(val), from_type='float', to_type=out_type) + elif isinstance(val, int): + return c.cast_value(str(val), from_type='int', to_type=out_type) + elif isinstance(val, mathutils.Vector) and len(val) <= 4: + out = val.to_4d() + + if out_socket == node.outputs[3]: + return c.to_vec1(out[3]) + return c.cast_value(c.to_vec3(out), from_type='vec3', to_type=out_type) + + # Default values, attribute name did not match + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('0.0', from_type='float', to_type=out_type) + + +def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if node.arm_material_param: + nn = 'param_' + c.node_name(node.name) + v = out_socket.default_value + value = [float(v[0]), float(v[1]), float(v[2])] + state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value=value, is_arm_mat_param=True) + return nn + else: + return c.to_vec3(out_socket.default_value) + + +def parse_vertex_color(node: bpy.types.ShaderNodeVertexColor, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + state.con.add_elem('col', 'short4norm') + return 'vcolor' + + +def parse_camera(node: bpy.types.ShaderNodeCameraData, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # View Vector in camera space + if out_socket == node.outputs[0]: + state.dxdy_varying_input_value = True + return 'vVecCam' + + # View Z Depth + elif out_socket == node.outputs[1]: + state.curshader.add_include('std/math.glsl') + state.curshader.add_uniform('vec2 cameraProj', link='_cameraPlaneProj') + state.dxdy_varying_input_value = True + return 'linearize(gl_FragCoord.z, cameraProj)' + + # View Distance + else: + state.curshader.add_uniform('vec3 eye', link='_cameraPosition') + state.dxdy_varying_input_value = True + return 'distance(eye, wposition)' + + +def parse_geometry(node: bpy.types.ShaderNodeNewGeometry, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Position + if out_socket == node.outputs[0]: + state.dxdy_varying_input_value = True + return 'wposition' + # Normal + elif out_socket == node.outputs[1]: + state.dxdy_varying_input_value = True + return 'n' if state.curshader.shader_type == 'frag' else 'wnormal' + # Tangent + elif out_socket == node.outputs[2]: + state.dxdy_varying_input_value = True + return 'wtangent' + # True Normal + elif out_socket == node.outputs[3]: + state.dxdy_varying_input_value = True + return 'n' if state.curshader.shader_type == 'frag' else 'wnormal' + # Incoming + elif out_socket == node.outputs[4]: + state.dxdy_varying_input_value = True + return 'vVec' + # Parametric + elif out_socket == node.outputs[5]: + state.dxdy_varying_input_value = True + return 'mposition' + # Backfacing + elif out_socket == node.outputs[6]: + return '(1.0 - float(gl_FrontFacing))' if state.context == ParserContext.OBJECT else '0.0' + # Pointiness + elif out_socket == node.outputs[7]: + return '0.0' + # Random Per Island + elif out_socket == node.outputs[8]: + return '0.0' + + +def parse_hairinfo(node: bpy.types.ShaderNodeHairInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Tangent Normal + if out_socket == node.outputs[3]: + return 'vec3(0.0)' + else: + # Is Strand + # Intercept + # Thickness + # Random + return '0.5' + + +def parse_objectinfo(node: bpy.types.ShaderNodeObjectInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Location + if out_socket == node.outputs[0]: + if state.context == ParserContext.WORLD: + return c.to_vec3((0.0, 0.0, 0.0)) + return 'wposition' + + # Color + elif out_socket == node.outputs[1]: + if state.context == ParserContext.WORLD: + # Use world strength like Blender + background_node = c.node_by_type(state.world.node_tree.nodes, 'BACKGROUND') + if background_node is None: + return c.to_vec3((0.0, 0.0, 0.0)) + return c.to_vec3([background_node.inputs[1].default_value] * 3) + + # TODO: Implement object color in Iron + # state.curshader.add_uniform('vec3 objectInfoColor', link='_objectInfoColor') + # return 'objectInfoColor' + return c.to_vec3((1.0, 1.0, 1.0)) + + # Alpha + elif out_socket == node.outputs[2]: + # TODO, see color output above + return '0.0' + + # Object Index + elif out_socket == node.outputs[3]: + if state.context == ParserContext.WORLD: + return '0.0' + state.curshader.add_uniform('float objectInfoIndex', link='_objectInfoIndex') + return 'objectInfoIndex' + + # Material Index + elif out_socket == node.outputs[4]: + if state.context == ParserContext.WORLD: + return '0.0' + state.curshader.add_uniform('float objectInfoMaterialIndex', link='_objectInfoMaterialIndex') + return 'objectInfoMaterialIndex' + + # Random + elif out_socket == node.outputs[5]: + if state.context == ParserContext.WORLD: + return '0.0' + + # Use random value per instance + if mat_state.uses_instancing: + state.vert.add_out(f'flat float irand') + state.frag.add_in(f'flat float irand') + state.vert.write(f'irand = fract(sin(gl_InstanceID) * 43758.5453);') + return 'irand' + + state.curshader.add_uniform('float objectInfoRandom', link='_objectInfoRandom') + return 'objectInfoRandom' + + +def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + particles_on = arm.utils.get_rp().arm_particles == 'On' + + # Index + if out_socket == node.outputs[0]: + c.particle_info['index'] = True + return 'p_index' if particles_on else '0.0' + + # TODO: Random + if out_socket == node.outputs[1]: + return '0.0' + + # Age + elif out_socket == node.outputs[2]: + c.particle_info['age'] = True + return 'p_age' if particles_on else '0.0' + + # Lifetime + elif out_socket == node.outputs[3]: + c.particle_info['lifetime'] = True + return 'p_lifetime' if particles_on else '0.0' + + # Location + if out_socket == node.outputs[4]: + c.particle_info['location'] = True + return 'p_location' if particles_on else 'vec3(0.0)' + + # Size + elif out_socket == node.outputs[5]: + c.particle_info['size'] = True + return '1.0' + + # Velocity + elif out_socket == node.outputs[6]: + c.particle_info['velocity'] = True + return 'p_velocity' if particles_on else 'vec3(0.0)' + + # Angular Velocity + elif out_socket == node.outputs[7]: + c.particle_info['angular_velocity'] = True + return 'vec3(0.0)' + + +def parse_tangent(node: bpy.types.ShaderNodeTangent, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + state.dxdy_varying_input_value = True + return 'wtangent' + + +def parse_texcoord(node: bpy.types.ShaderNodeTexCoord, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + #obj = node.object + #instance = node.from_instance + if out_socket == node.outputs[0]: # Generated - bounds + state.dxdy_varying_input_value = True + return 'bposition' + elif out_socket == node.outputs[1]: # Normal + state.dxdy_varying_input_value = True + return 'n' + elif out_socket == node.outputs[2]: # UV + if state.context == ParserContext.WORLD: + return 'vec3(0.0)' + state.con.add_elem('tex', 'short2norm') + state.dxdy_varying_input_value = True + return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' + elif out_socket == node.outputs[3]: # Object + state.dxdy_varying_input_value = True + return 'mposition' + elif out_socket == node.outputs[4]: # Camera + return 'vec3(0.0)' # 'vposition' + elif out_socket == node.outputs[5]: # Window + # TODO: Don't use gl_FragCoord here, it uses different axes on different graphics APIs + state.frag.add_uniform('vec2 screenSize', link='_screenSize') + state.dxdy_varying_input_value = True + return f'vec3(gl_FragCoord.xy / screenSize, 0.0)' + elif out_socket == node.outputs[6]: # Reflection + if state.context == ParserContext.WORLD: + state.dxdy_varying_input_value = True + return 'n' + return 'vec3(0.0)' + + +def parse_uvmap(node: bpy.types.ShaderNodeUVMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + # instance = node.from_instance + state.con.add_elem('tex', 'short2norm') + mat = c.mat_get_material() + mat_users = c.mat_get_material_users() + + state.dxdy_varying_input_value = True + + if mat_users is not None and mat in mat_users: + mat_user = mat_users[mat][0] + if hasattr(mat_user.data, 'uv_layers'): + layers = mat_user.data.uv_layers + # Second UV map referenced + if len(layers) > 1 and node.uv_map == layers[1].name: + state.con.add_elem('tex1', 'short2norm') + return 'vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)' + + return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' + + +def parse_fresnel(node: bpy.types.ShaderNodeFresnel, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + state.curshader.add_function(c_functions.str_fresnel) + ior = c.parse_value_input(node.inputs[0]) + if node.inputs[1].is_linked: + dotnv = 'dot({0}, vVec)'.format(c.parse_vector_input(node.inputs[1])) + else: + dotnv = 'dotNV' + + state.dxdy_varying_input_value = True + return 'fresnel({0}, {1})'.format(ior, dotnv) + + +def parse_layerweight(node: bpy.types.ShaderNodeLayerWeight, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + blend = c.parse_value_input(node.inputs[0]) + if node.inputs[1].is_linked: + dotnv = 'dot({0}, vVec)'.format(c.parse_vector_input(node.inputs[1])) + else: + dotnv = 'dotNV' + + state.dxdy_varying_input_value = True + + # Fresnel + if out_socket == node.outputs[0]: + state.curshader.add_function(c_functions.str_fresnel) + return 'fresnel(1.0 / (1.0 - {0}), {1})'.format(blend, dotnv) + # Facing + elif out_socket == node.outputs[1]: + return '(1.0 - pow({0}, ({1} < 0.5) ? 2.0 * {1} : 0.5 / (1.0 - {1})))'.format(dotnv, blend) + + +def parse_lightpath(node: bpy.types.ShaderNodeLightPath, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + # https://github.com/blender/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_light_path.glsl + if out_socket == node.outputs['Is Camera Ray']: + return '1.0' + elif out_socket == node.outputs['Is Shadow Ray']: + return '0.0' + elif out_socket == node.outputs['Is Diffuse Ray']: + return '1.0' + elif out_socket == node.outputs['Is Glossy Ray']: + return '1.0' + elif out_socket == node.outputs['Is Singular Ray']: + return '0.0' + elif out_socket == node.outputs['Is Reflection Ray']: + return '0.0' + elif out_socket == node.outputs['Is Transmission Ray']: + return '0.0' + elif out_socket == node.outputs['Ray Length']: + return '1.0' + elif out_socket == node.outputs['Ray Depth']: + return '0.0' + elif out_socket == node.outputs['Diffuse Depth']: + return '0.0' + elif out_socket == node.outputs['Glossy Depth']: + return '0.0' + elif out_socket == node.outputs['Transparent Depth']: + return '0.0' + elif out_socket == node.outputs['Transmission Depth']: + return '0.0' + + log.warn(f'Light Path node: unsupported output {out_socket.identifier}.') + return '0.0' + + +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) + value = 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) + + +def parse_wireframe(node: bpy.types.ShaderNodeWireframe, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + # node.use_pixel_size + # size = c.parse_value_input(node.inputs[0]) + return '0.0' diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py new file mode 100644 index 0000000000..674c6704ec --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -0,0 +1,212 @@ +import bpy +from bpy.types import NodeSocket + +import arm +import arm.material.cycles as c +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils +from arm.material.parser_state import ParserState + +if arm.is_reload(__name__): + c = arm.reload_module(c) + mat_state = arm.reload_module(mat_state) + mat_utils = arm.reload_module(mat_utils) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState +else: + arm.enable_reload(__name__) + + +def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state: ParserState) -> None: + # Skip mixing if only one input is effectively used + if not node.inputs[0].is_linked: + if node.inputs[0].default_value <= 0.0: + c.parse_shader_input(node.inputs[1]) + return + elif node.inputs[0].default_value >= 1.0: + c.parse_shader_input(node.inputs[2]) + return + + prefix = '' if node.inputs[0].is_linked else 'const ' + fac = c.parse_value_input(node.inputs[0]) + fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() + fac_inv_var = c.node_name(node.name) + '_fac_inv' + state.curshader.write('{0}float {1} = clamp({2}, 0.0, 1.0);'.format(prefix, fac_var, fac)) + state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var)) + + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + bc1, rough1, met1, occ1, spec1, opac1, rior1, emi1 = c.parse_shader_input(node.inputs[1]) + ek1 = mat_state.emission_type + + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + bc2, rough2, met2, occ2, spec2, opac2, rior2, emi2 = c.parse_shader_input(node.inputs[1]) + ek2 = mat_state.emission_type + + if state.parse_surface: + state.out_basecol = '({0} * {3} + {1} * {2})'.format(bc1, bc2, fac_var, fac_inv_var) + state.out_roughness = '({0} * {3} + {1} * {2})'.format(rough1, rough2, fac_var, fac_inv_var) + state.out_metallic = '({0} * {3} + {1} * {2})'.format(met1, met2, fac_var, fac_inv_var) + state.out_occlusion = '({0} * {3} + {1} * {2})'.format(occ1, occ2, fac_var, fac_inv_var) + state.out_specular = '({0} * {3} + {1} * {2})'.format(spec1, spec2, fac_var, fac_inv_var) + state.out_emission_col = '({0} * {3} + {1} * {2})'.format(emi1, emi2, fac_var, fac_inv_var) + mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2) + if state.parse_opacity: + state.out_opacity = '({0} * {3} + {1} * {2})'.format(opac1, opac2, fac_var, fac_inv_var) + state.out_rior = '({0} * {3} + {1} * {2})'.format(rior1, rior2, fac_var, fac_inv_var) + +def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state: ParserState) -> None: + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + bc1, rough1, met1, occ1, spec1, opac1, rior1, emi1 = c.parse_shader_input(node.inputs[0]) + ek1 = mat_state.emission_type + + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + bc2, rough2, met2, occ2, spec2, opac2, rior2, emi2 = c.parse_shader_input(node.inputs[1]) + ek2 = mat_state.emission_type + + if state.parse_surface: + state.out_basecol = '({0} + {1})'.format(bc1, bc2) + state.out_roughness = '({0} * 0.5 + {1} * 0.5)'.format(rough1, rough2) + state.out_metallic = '({0} * 0.5 + {1} * 0.5)'.format(met1, met2) + state.out_occlusion = '({0} * 0.5 + {1} * 0.5)'.format(occ1, occ2) + state.out_specular = '({0} * 0.5 + {1} * 0.5)'.format(spec1, spec2) + state.out_emission_col = '({0} + {1})'.format(emi1, emi2) + mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2) + if state.parse_opacity: + state.out_opacity = '({0} * 0.5 + {1} * 0.5)'.format(opac1, opac2) + state.out_rior = '({0} * 0.5 + {1} * 0.5)'.format(rior1, rior2) + +def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[22]) + state.out_basecol = c.parse_vector_input(node.inputs[0]) + # subsurface = c.parse_vector_input(node.inputs[1]) + # subsurface_radius = c.parse_vector_input(node.inputs[2]) + # subsurface_color = c.parse_vector_input(node.inputs[3]) + state.out_metallic = c.parse_value_input(node.inputs[6]) + state.out_specular = c.parse_value_input(node.inputs[7]) + # specular_tint = c.parse_vector_input(node.inputs[6]) + state.out_roughness = c.parse_value_input(node.inputs[9]) + # aniso = c.parse_vector_input(node.inputs[8]) + # aniso_rot = c.parse_vector_input(node.inputs[9]) + # sheen = c.parse_vector_input(node.inputs[10]) + # sheen_tint = c.parse_vector_input(node.inputs[11]) + # clearcoat = c.parse_vector_input(node.inputs[12]) + # clearcoat_rough = c.parse_vector_input(node.inputs[13]) + # ior = c.parse_vector_input(node.inputs[14]) + # transmission = c.parse_vector_input(node.inputs[15]) + # transmission_roughness = c.parse_vector_input(node.inputs[16]) + if (node.inputs['Emission Strength'].is_linked or node.inputs['Emission Strength'].default_value != 0.0)\ + and (node.inputs['Emission'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission'], (0.0, 0.0, 0.0), comp_alpha=False)): + emission_col = c.parse_vector_input(node.inputs[19]) + emission_strength = c.parse_value_input(node.inputs[20]) + state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) + mat_state.emission_type = mat_state.EmissionType.SHADED + else: + mat_state.emission_type = mat_state.EmissionType.NO_EMISSION + # clearcoar_normal = c.parse_vector_input(node.inputs[21]) + # tangent = c.parse_vector_input(node.inputs[22]) + if state.parse_opacity: + state.out_opacity = c.parse_value_input(node.inputs[21]) + state.out_rior = c.parse_value_input(node.inputs[16]); + + +def parse_bsdfdiffuse(node: bpy.types.ShaderNodeBsdfDiffuse, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[2]) + state.out_basecol = c.parse_vector_input(node.inputs[0]) + state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_specular = '0.0' + + +def parse_bsdfglossy(node: bpy.types.ShaderNodeBsdfGlossy, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[2]) + state.out_basecol = c.parse_vector_input(node.inputs[0]) + state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_metallic = '1.0' + + +def parse_ambientocclusion(node: bpy.types.ShaderNodeAmbientOcclusion, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + # Single channel + state.out_occlusion = c.parse_vector_input(node.inputs[0]) + '.r' + + +def parse_bsdfanisotropic(node: bpy.types.ShaderNodeBsdfAnisotropic, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[4]) + # Revert to glossy + state.out_basecol = c.parse_vector_input(node.inputs[0]) + state.out_roughness = c.parse_value_input(node.inputs[1]) + state.out_metallic = '1.0' + + +def parse_emission(node: bpy.types.ShaderNodeEmission, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + emission_col = c.parse_vector_input(node.inputs[0]) + emission_strength = c.parse_value_input(node.inputs[1]) + state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) + state.out_basecol = 'vec3(0.0)' + state.out_specular = '0.0' + state.out_metallic = '0.0' + mat_state.emission_type = mat_state.EmissionType.SHADELESS + + +def parse_bsdfglass(node: bpy.types.ShaderNodeBsdfGlass, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + state.out_basecol = c.parse_vector_input(node.inputs[0]) + c.write_normal(node.inputs[3]) + state.out_roughness = c.parse_value_input(node.inputs[1]) + if state.parse_opacity: + state.out_opacity = '0.0' + state.out_rior = c.parse_value_input(node.inputs[2]) + + +def parse_bsdfhair(node: bpy.types.ShaderNodeBsdfHair, out_socket: NodeSocket, state: ParserState) -> None: + pass + + +def parse_holdout(node: bpy.types.ShaderNodeHoldout, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + # Occlude + state.out_occlusion = '0.0' + + +def parse_bsdfrefraction(node: bpy.types.ShaderNodeBsdfRefraction, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + state.out_basecol = c.parse_vector_input(node.inputs[0]) + c.write_normal(node.inputs[3]) + state.out_roughness = c.parse_value_input(node.inputs[1]) + if state.parse_opacity: + state.out_opacity = '0.0' + state.out_rior = c.parse_value_input(node.inputs[2]) + +def parse_subsurfacescattering(node: bpy.types.ShaderNodeSubsurfaceScattering, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[4]) + state.out_basecol = c.parse_vector_input(node.inputs[0]) + + +def parse_bsdftoon(node: bpy.types.ShaderNodeBsdfToon, out_socket: NodeSocket, state: ParserState) -> None: + # c.write_normal(node.inputs[3]) + pass + + +def parse_bsdftranslucent(node: bpy.types.ShaderNodeBsdfTranslucent, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[1]) + if state.parse_opacity: + state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs[0])) + + +def parse_bsdftransparent(node: bpy.types.ShaderNodeBsdfTransparent, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_opacity: + state.out_opacity = '(1.0 - {0}.r)'.format(c.parse_vector_input(node.inputs[0])) + + +def parse_bsdfvelvet(node: bpy.types.ShaderNodeBsdfVelvet, out_socket: NodeSocket, state: ParserState) -> None: + if state.parse_surface: + c.write_normal(node.inputs[2]) + state.out_basecol = c.parse_vector_input(node.inputs[0]) + state.out_roughness = '1.0' + state.out_metallic = '1.0' diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py new file mode 100644 index 0000000000..de99c7fe58 --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -0,0 +1,584 @@ +import math +import os +from typing import Union + +import bpy + +import arm.assets as assets +import arm.log as log +import arm.material.cycles as c +import arm.material.cycles_functions as c_functions +from arm.material.parser_state import ParserState, ParserContext, ParserPass +from arm.material.shader import floatstr, vec3str +import arm.utils +import arm.write_probes as write_probes + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + log = arm.reload_module(log) + c = arm.reload_module(c) + c_functions = arm.reload_module(c_functions) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState, ParserContext, ParserPass + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import floatstr, vec3str + arm.utils = arm.reload_module(arm.utils) + write_probes = arm.reload_module(write_probes) +else: + arm.enable_reload(__name__) + + +def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + state.curshader.add_function(c_functions.str_tex_brick) + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + # Color + if out_socket == node.outputs[0]: + col1 = c.parse_vector_input(node.inputs[1]) + col2 = c.parse_vector_input(node.inputs[2]) + col3 = c.parse_vector_input(node.inputs[3]) + scale = c.parse_value_input(node.inputs[4]) + res = f'tex_brick({co} * {scale}, {col1}, {col2}, {col3})' + # Fac + else: + scale = c.parse_value_input(node.inputs[4]) + res = 'tex_brick_f({0} * {1})'.format(co, scale) + + return res + + +def parse_tex_checker(node: bpy.types.ShaderNodeTexChecker, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + state.curshader.add_function(c_functions.str_tex_checker) + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + # Color + if out_socket == node.outputs[0]: + col1 = c.parse_vector_input(node.inputs[1]) + col2 = c.parse_vector_input(node.inputs[2]) + scale = c.parse_value_input(node.inputs[3]) + res = f'tex_checker({co}, {col1}, {col2}, {scale})' + # Fac + else: + scale = c.parse_value_input(node.inputs[3]) + res = 'tex_checker_f({0}, {1})'.format(co, scale) + + return res + + +def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + grad = node.gradient_type + if grad == 'LINEAR': + f = f'{co}.x' + elif grad == 'QUADRATIC': + f = '0.0' + elif grad == 'EASING': + f = '0.0' + elif grad == 'DIAGONAL': + f = f'({co}.x + {co}.y) * 0.5' + elif grad == 'RADIAL': + f = f'atan({co}.y, {co}.x) / PI2 + 0.5' + elif grad == 'QUADRATIC_SPHERE': + f = '0.0' + else: # SPHERICAL + f = f'max(1.0 - sqrt({co}.x * {co}.x + {co}.y * {co}.y + {co}.z * {co}.z), 0.0)' + + # Color + if out_socket == node.outputs[0]: + res = f'vec3(clamp({f}, 0.0, 1.0))' + # Fac + else: + res = f'(clamp({f}, 0.0, 1.0))' + + return res + + +def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Color or Alpha output + use_color_out = out_socket == node.outputs[0] + + if state.context == ParserContext.OBJECT: + tex_store = c.store_var_name(node) + + if c.node_need_reevaluation_for_screenspace_derivative(node): + tex_store += state.get_parser_pass_suffix() + + # Already fetched + if c.is_parsed(tex_store): + if use_color_out: + return f'{tex_store}.rgb' + else: + return f'{tex_store}.a' + + tex_name = c.node_name(node.name) + tex = c.make_texture_from_image_node(node, tex_name) + 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 is not None: + state.curshader.write_textures += 1 + if node.arm_material_param and tex['file'] is not None: + tex_default_file = tex['file'] + 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, 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, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.a' + state.curshader.write_textures -= 1 + return res + + # Empty texture + elif node.image is None: + tex = { + 'name': tex_name, + 'file': '' + } + if use_color_out: + return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, tex_link=tex_link, is_arm_mat_param=is_arm_mat_param)) + return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, tex_link=tex_link, is_arm_mat_param=is_arm_mat_param)) + + # Pink color for missing texture + else: + if use_color_out: + state.parsed.add(tex_store) + state.curshader.write_textures += 1 + state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);') + state.curshader.write_textures -= 1 + return f'{tex_store}.rgb' + else: + state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);') + return f'{tex_store}.a' + + # World context + # TODO: Merge with above implementation to also allow mappings other than using view coordinates + else: + world = state.world + world.world_defs += '_EnvImg' + + # Background texture + state.curshader.add_uniform('sampler2D envmap', link='_envmap') + state.curshader.add_uniform('vec2 screenSize', link='_screenSize') + + image = node.image + if image is None: + log.warn(f'World "{world.name}": image texture node "{node.name}" is empty') + return 'vec3(0.0, 0.0, 0.0)' if use_color_out else '0.0' + + filepath = image.filepath + + if image.packed_file is not None: + # Extract packed data + filepath = arm.utils.build_dir() + '/compiled/Assets/unpacked' + unpack_path = arm.utils.get_fp() + filepath + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + unpack_filepath = unpack_path + '/' + image.name + if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size: + with open(unpack_filepath, 'wb') as f: + f.write(image.packed_file.data) + assets.add(unpack_filepath) + else: + # Link image path to assets + assets.add(arm.utils.asset_path(image.filepath)) + + # Reference image name + tex_file = arm.utils.extract_filename(image.filepath) + base = tex_file.rsplit('.', 1) + ext = base[1].lower() + + if ext == 'hdr': + target_format = 'HDR' + else: + target_format = 'JPEG' + + # Generate prefiltered envmaps + world.arm_envtex_name = tex_file + world.arm_envtex_irr_name = tex_file.rsplit('.', 1)[0] + + disable_hdr = target_format == 'JPEG' + from_srgb = image.colorspace_settings.name == "sRGB" + + rpdat = arm.utils.get_rp() + mip_count = world.arm_envtex_num_mips + mip_count = write_probes.write_probes(filepath, disable_hdr, from_srgb, mip_count, arm_radiance=rpdat.arm_radiance) + + world.arm_envtex_num_mips = mip_count + + # Will have to get rid of gl_FragCoord, pass texture coords from vertex shader + state.curshader.write_init('vec2 texco = gl_FragCoord.xy / screenSize;') + return 'texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength' + + +def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + state.curshader.add_function(c_functions.str_tex_magic) + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + scale = c.parse_value_input(node.inputs[1]) + + # Color + if out_socket == node.outputs[0]: + res = f'tex_magic({co} * {scale} * 4.0)' + # Fac + else: + res = f'tex_magic_f({co} * {scale} * 4.0)' + + return res + + +def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + state.curshader.add_function(c_functions.str_tex_musgrave) + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + scale = c.parse_value_input(node.inputs['Scale']) + # detail = c.parse_value_input(node.inputs[2]) + # distortion = c.parse_value_input(node.inputs[3]) + + res = f'tex_musgrave_f({co} * {scale} * 0.5)' + + return res + + +def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + c.write_procedurals() + state.curshader.add_function(c_functions.str_tex_noise) + c.assets_add(os.path.join(arm.utils.get_sdk_path(), 'armory', 'Assets', 'noise256.png')) + c.assets_add_embedded_data('noise256.png') + state.curshader.add_uniform('sampler2D snoise256', link='$noise256.png') + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + scale = c.parse_value_input(node.inputs[2]) + detail = c.parse_value_input(node.inputs[3]) + roughness = c.parse_value_input(node.inputs[4]) + distortion = c.parse_value_input(node.inputs[5]) + + # Color + if out_socket == node.outputs[1]: + res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion) + # Fac + else: + res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion) + + return res + + +def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Pass through + + # Color + if out_socket == node.outputs[0]: + return c.to_vec3([0.0, 0.0, 0.0]) + # Density + else: + return '0.0' + + +def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if state.context == ParserContext.OBJECT: + # Pass through + return c.to_vec3([0.0, 0.0, 0.0]) + + state.world.world_defs += '_EnvSky' + + if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE': + if node.sky_type == 'PREETHAM': + log.info('Info: Preetham sky model is not supported, using Hosek Wilkie sky model instead') + + return parse_sky_hosekwilkie(node, state) + + elif node.sky_type == 'NISHITA': + return parse_sky_nishita(node, state) + + else: + log.error(f'Unsupported sky model: {node.sky_type}!') + return c.to_vec3([0.0, 0.0, 0.0]) + + +def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: + world = state.world + curshader = state.curshader + + # Match to cycles + world.arm_envtex_strength *= 0.1 + + assets.add_khafile_def('arm_hosek') + curshader.add_uniform('vec3 A', link="_hosekA") + curshader.add_uniform('vec3 B', link="_hosekB") + curshader.add_uniform('vec3 C', link="_hosekC") + curshader.add_uniform('vec3 D', link="_hosekD") + curshader.add_uniform('vec3 E', link="_hosekE") + curshader.add_uniform('vec3 F', link="_hosekF") + curshader.add_uniform('vec3 G', link="_hosekG") + curshader.add_uniform('vec3 H', link="_hosekH") + curshader.add_uniform('vec3 I', link="_hosekI") + curshader.add_uniform('vec3 Z', link="_hosekZ") + curshader.add_uniform('vec3 hosekSunDirection', link="_hosekSunDirection") + curshader.add_function("""vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) { +\tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5)); +\treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta)); +}""") + + world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] + world.arm_envtex_turbidity = node.turbidity + world.arm_envtex_ground_albedo = node.ground_albedo + + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' + + if not state.radiance_written: + # Irradiance json file name + wname = arm.utils.safestr(world.name) + world.arm_envtex_irr_name = wname + write_probes.write_sky_irradiance(wname) + + # Radiance + if rpdat.arm_radiance and rpdat.arm_irradiance and not mobile_mat: + wrd.world_defs += '_Rad' + hosek_path = 'armory/Assets/hosek/' + sdk_path = arm.utils.get_sdk_path() + # Use fake maps for now + assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance.hdr') + for i in range(0, 8): + assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance_' + str(i) + '.hdr') + + world.arm_envtex_name = 'hosek' + world.arm_envtex_num_mips = 8 + + state.radiance_written = True + + curshader.write('float cos_theta = clamp(pos.z, 0.0, 1.0);') + curshader.write('float cos_gamma = dot(pos, hosekSunDirection);') + curshader.write('float gamma_val = acos(cos_gamma);') + + return 'Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;' + + +def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: + curshader = state.curshader + curshader.add_include('std/sky.glsl') + curshader.add_uniform('vec3 sunDir', link='_sunDirection') + curshader.add_uniform('sampler2D nishitaLUT', link='_nishitaLUT', included=True, + tex_addr_u='clamp', tex_addr_v='clamp') + curshader.add_uniform('vec2 nishitaDensity', link='_nishitaDensity', included=True) + + planet_radius = 6360e3 # Earth radius used in Blender + ray_origin_z = planet_radius + node.altitude + + state.world.arm_nishita_density = [node.air_density, node.dust_density, node.ozone_density] + + sun = '' + if node.sun_disc: + # The sun size is calculated relative in terms of the distance + # between the sun position and the sky dome normal at every + # pixel (see sun_disk() in sky.glsl). + # + # An isosceles triangle is created with the camera at the + # opposite side of the base with node.sun_size being the vertex + # angle from which the base angle theta is calculated. Iron's + # skydome geometry roughly resembles a unit sphere, so the leg + # size is set to 1. The base size is the doubled normal-relative + # target size. + + # sun_size is already in radians despite being degrees in the UI + theta = 0.5 * (math.pi - node.sun_size) + size = math.cos(theta) + sun = f'* sun_disk(pos, sunDir, {size}, {node.sun_intensity})' + + return f'nishita_atmosphere(pos, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}' + + +def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if state.context == ParserContext.OBJECT: + log.warn('Environment Texture node is not supported for object node trees, using default value') + return c.to_vec3([0.0, 0.0, 0.0]) + + if node.image is None: + return c.to_vec3([1.0, 0.0, 1.0]) + + world = state.world + world.world_defs += '_EnvTex' + + curshader = state.curshader + + curshader.add_include('std/math.glsl') + curshader.add_uniform('sampler2D envmap', link='_envmap') + + image = node.image + filepath = image.filepath + + if image.packed_file is None and not os.path.isfile(arm.utils.asset_path(filepath)): + log.warn(world.name + ' - unable to open ' + image.filepath) + return c.to_vec3([1.0, 0.0, 1.0]) + + # Reference image name + tex_file = arm.utils.extract_filename(image.filepath) + base = tex_file.rsplit('.', 1) + ext = base[1].lower() + + if ext == 'hdr': + target_format = 'HDR' + else: + target_format = 'JPEG' + do_convert = ext != 'hdr' and ext != 'jpg' + if do_convert: + if ext == 'exr': + tex_file = base[0] + '.hdr' + target_format = 'HDR' + else: + tex_file = base[0] + '.jpg' + target_format = 'JPEG' + + if image.packed_file is not None: + # Extract packed data + unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + unpack_filepath = unpack_path + '/' + tex_file + filepath = unpack_filepath + + if do_convert: + if not os.path.isfile(unpack_filepath): + arm.utils.unpack_image(image, unpack_filepath, file_format=target_format) + + elif not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size: + with open(unpack_filepath, 'wb') as f: + f.write(image.packed_file.data) + + assets.add(unpack_filepath) + else: + if do_convert: + unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' + if not os.path.exists(unpack_path): + os.makedirs(unpack_path) + converted_path = unpack_path + '/' + tex_file + filepath = converted_path + # TODO: delete cache when file changes + if not os.path.isfile(converted_path): + arm.utils.convert_image(image, converted_path, file_format=target_format) + assets.add(converted_path) + else: + # Link image path to assets + assets.add(arm.utils.asset_path(image.filepath)) + + rpdat = arm.utils.get_rp() + + if not state.radiance_written: + # Generate prefiltered envmaps + world.arm_envtex_name = tex_file + world.arm_envtex_irr_name = tex_file.rsplit('.', 1)[0] + disable_hdr = target_format == 'JPEG' + from_srgb = image.colorspace_settings.name == "sRGB" + + mip_count = world.arm_envtex_num_mips + mip_count = write_probes.write_probes(filepath, disable_hdr, from_srgb, mip_count, arm_radiance=rpdat.arm_radiance) + + world.arm_envtex_num_mips = mip_count + + state.radiance_written = True + + # Append LDR define + if disable_hdr: + world.world_defs += '_EnvLDR' + + wrd = bpy.data.worlds['Arm'] + mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' + + # Append radiance define + if rpdat.arm_irradiance and rpdat.arm_radiance and not mobile_mat: + wrd.world_defs += '_Rad' + + return 'texture(envmap, envMapEquirect(pos)).rgb * envmapStrength' + + +def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + outp = 0 + if out_socket.type == 'RGBA': + outp = 1 + elif out_socket.type == 'VECTOR': + outp = 2 + m = 0 + if node.distance == 'MANHATTAN': + m = 1 + elif node.distance == 'CHEBYCHEV': + m = 2 + elif node.distance == 'MINKOWSKI': + m = 3 + + c.write_procedurals() + state.curshader.add_function(c_functions.str_tex_voronoi) + + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + + scale = c.parse_value_input(node.inputs[2]) + exp = c.parse_value_input(node.inputs[4]) + randomness = c.parse_value_input(node.inputs[5]) + + # Color or Position + if out_socket == node.outputs[1] or out_socket == node.outputs[2]: + res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5})'.format(co, randomness, m, outp, scale, exp) + # Distance + else: + res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5}).x'.format(co, randomness, m, outp, scale, exp) + + return res + + +def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + c.write_procedurals() + state.curshader.add_function(c_functions.str_tex_wave) + if node.inputs[0].is_linked: + co = c.parse_vector_input(node.inputs[0]) + else: + co = 'bposition' + scale = c.parse_value_input(node.inputs[1]) + distortion = c.parse_value_input(node.inputs[2]) + detail = c.parse_value_input(node.inputs[3]) + detail_scale = c.parse_value_input(node.inputs[4]) + if node.wave_profile == 'SIN': + wave_profile = 0 + else: + wave_profile = 1 + if node.wave_type == 'BANDS': + wave_type = 0 + else: + wave_type = 1 + + # Color + if out_socket == node.outputs[0]: + res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) + # Fac + else: + res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) + + return res diff --git a/blender/arm/material/cycles_nodes/nodes_vector.py b/blender/arm/material/cycles_nodes/nodes_vector.py new file mode 100644 index 0000000000..16d273e4be --- /dev/null +++ b/blender/arm/material/cycles_nodes/nodes_vector.py @@ -0,0 +1,205 @@ +from typing import Union + +import bpy +from mathutils import Euler, Vector + +import arm.log +import arm.material.cycles as c +import arm.material.cycles_functions as c_functions +from arm.material.parser_state import ParserState, ParserPass +from arm.material.shader import floatstr, vec3str +import arm.utils as utils + +if arm.is_reload(__name__): + arm.log = arm.reload_module(arm.log) + c = arm.reload_module(c) + c_functions = arm.reload_module(c_functions) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) + from arm.material.parser_state import ParserState, ParserPass + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import floatstr, vec3str + utils = arm.reload_module(utils) +else: + arm.enable_reload(__name__) + + +def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + fac = c.parse_value_input(node.inputs[0]) + vec = c.parse_vector_input(node.inputs[1]) + curves = node.mapping.curves + name = c.node_name(node.name) + # mapping.curves[0].points[0].handle_type # bezier curve + return '(vec3({0}, {1}, {2}) * {3})'.format( + c.vector_curve(name + '0', vec + '.x', curves[0].points), + c.vector_curve(name + '1', vec + '.y', curves[1].points), + c.vector_curve(name + '2', vec + '.z', curves[2].points), fac) + + +def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if state.curshader.shader_type != 'frag': + arm.log.warn("Bump node not supported outside of fragment shaders") + return 'vec3(0.0)' + + # Interpolation strength + strength = c.parse_value_input(node.inputs[0]) + # Height multiplier + # distance = c.parse_value_input(node.inputs[1]) + height = c.parse_value_input(node.inputs[2]) + + state.current_pass = ParserPass.DX_SCREEN_SPACE + height_dx = c.parse_value_input(node.inputs[2]) + state.current_pass = ParserPass.DY_SCREEN_SPACE + height_dy = c.parse_value_input(node.inputs[2]) + state.current_pass = ParserPass.REGULAR + + # nor = c.parse_vector_input(node.inputs[3]) + + if height_dx != height or height_dy != height: + tangent = f'{c.dfdx_fine("wposition")} + n * ({height_dx} - {height})' + bitangent = f'{c.dfdy_fine("wposition")} + n * ({height_dy} - {height})' + + # Cross-product operand order, dFdy is flipped on d3d11 + bitangent_first = utils.get_gapi() == 'direct3d11' + + if node.invert: + bitangent_first = not bitangent_first + + if bitangent_first: + # We need to normalize twice, once for the correct "weight" of the strength, + # once for having a normalized output vector (lerping vectors does not preserve magnitude) + res = f'normalize(mix(n, normalize(cross({bitangent}, {tangent})), {strength}))' + else: + res = f'normalize(mix(n, normalize(cross({tangent}, {bitangent})), {strength}))' + + else: + res = 'n' + + return res + + +def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + # Only "Point", "Texture" and "Vector" types supported for now.. + # More information about the order of operations for this node: + # https://docs.blender.org/manual/en/latest/render/shader_nodes/vector/mapping.html#properties + + input_vector: bpy.types.NodeSocket = node.inputs[0] + input_location: bpy.types.NodeSocket = node.inputs['Location'] + input_rotation: bpy.types.NodeSocket = node.inputs['Rotation'] + input_scale: bpy.types.NodeSocket = node.inputs['Scale'] + out = c.parse_vector_input(input_vector) if input_vector.is_linked else c.to_vec3(input_vector.default_value) + location = c.parse_vector_input(input_location) if input_location.is_linked else c.to_vec3(input_location.default_value) + rotation = c.parse_vector_input(input_rotation) if input_rotation.is_linked else c.to_vec3(input_rotation.default_value) + scale = c.parse_vector_input(input_scale) if input_scale.is_linked else c.to_vec3(input_scale.default_value) + + # Use inner functions because the order of operations varies between + # mapping node vector types. This adds a slight overhead but makes + # the code much more readable. + # - "Point" and "Vector" use Scale -> Rotate -> Translate + # - "Texture" uses Translate -> Rotate -> Scale + def calc_location(output: str) -> str: + # Vectors and Eulers support the "!=" operator + if input_scale.is_linked or input_scale.default_value != Vector((1, 1, 1)): + if node.vector_type == 'TEXTURE': + output = f'({output} / {scale})' + else: + output = f'({output} * {scale})' + + return output + + def calc_scale(output: str) -> str: + if input_location.is_linked or input_location.default_value != Vector((0, 0, 0)): + # z location is a little off sometimes?... + if node.vector_type == 'TEXTURE': + output = f'({output} - {location})' + else: + output = f'({output} + {location})' + return output + + out = calc_location(out) if node.vector_type == 'TEXTURE' else calc_scale(out) + + if input_rotation.is_linked or input_rotation.default_value != Euler((0, 0, 0)): + var_name = c.node_name(node.name) + "_rotation" + state.get_parser_pass_suffix() + if node.vector_type == 'TEXTURE': + state.curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));') + state.curshader.write(f'mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));') + state.curshader.write(f'mat3 {var_name}Z = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);') + else: + # A little bit redundant, but faster than 12 more multiplications to make it work dynamically + state.curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos(-{rotation}.x), sin(-{rotation}.x), 0.0, -sin(-{rotation}.x), cos(-{rotation}.x));') + state.curshader.write(f'mat3 {var_name}Y = mat3(cos(-{rotation}.y), 0.0, -sin(-{rotation}.y), 0.0, 1.0, 0.0, sin(-{rotation}.y), 0.0, cos(-{rotation}.y));') + state.curshader.write(f'mat3 {var_name}Z = mat3(cos(-{rotation}.z), sin(-{rotation}.z), 0.0, -sin(-{rotation}.z), cos(-{rotation}.z), 0.0, 0.0, 0.0, 1.0);') + + # XYZ-order euler rotation + out = f'{out} * {var_name}X * {var_name}Y * {var_name}Z' + + out = calc_scale(out) if node.vector_type == 'TEXTURE' else calc_location(out) + + return out + + +def parse_normal(node: bpy.types.ShaderNodeNormal, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + nor1 = c.to_vec3(node.outputs['Normal'].default_value) + + if out_socket == node.outputs['Normal']: + return nor1 + + elif out_socket == node.outputs['Dot']: + nor2 = c.parse_vector_input(node.inputs["Normal"]) + return f'dot({nor1}, {nor2})' + + +def parse_normalmap(node: bpy.types.ShaderNodeNormalMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if state.curshader == state.tese: + return c.parse_vector_input(node.inputs[1]) + else: + # space = node.space + # map = node.uv_map + # Color + c.parse_normal_map_color_input(node.inputs[1], node.inputs[0]) + return 'n' + + +def parse_vectortransform(node: bpy.types.ShaderNodeVectorTransform, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + # type = node.vector_type + # conv_from = node.convert_from + # conv_to = node.convert_to + # Pass through + return c.parse_vector_input(node.inputs[0]) + + +def parse_displacement(node: bpy.types.ShaderNodeDisplacement, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + height = c.parse_value_input(node.inputs[0]) + midlevel = c.parse_value_input(node.inputs[1]) + scale = c.parse_value_input(node.inputs[2]) + nor = c.parse_vector_input(node.inputs[3]) + return f'(vec3({height}) * {scale})' + +def parse_vectorrotate(node: bpy.types.ShaderNodeVectorRotate, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + + type = node.rotation_type + input_vector: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[0]) + input_center: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[1]) + input_axis: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[2]) + input_angle: bpy.types.NodeSocket = c.parse_value_input(node.inputs[3]) + input_rotation: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[4]) + + if node.invert: + input_invert = "0" + else: + input_invert = "1" + + state.curshader.add_function(c_functions.str_rotate_around_axis) + + if type == 'AXIS_ANGLE': + return f'vec3( (length({input_axis}) != 0.0) ? rotate_around_axis({input_vector} - {input_center}, normalize({input_axis}), {input_angle} * {input_invert}) + {input_center} : {input_vector} )' + elif type == 'X_AXIS': + return f'vec3( rotate_around_axis({input_vector} - {input_center}, vec3(1.0, 0.0, 0.0), {input_angle} * {input_invert}) + {input_center} )' + elif type == 'Y_AXIS': + return f'vec3( rotate_around_axis({input_vector} - {input_center}, vec3(0.0, 1.0, 0.0), {input_angle} * {input_invert}) + {input_center} )' + elif type == 'Z_AXIS': + return f'vec3( rotate_around_axis({input_vector} - {input_center}, vec3(0.0, 0.0, 1.0), {input_angle} * {input_invert}) + {input_center} )' + elif type == 'EULER_XYZ': + state.curshader.add_function(c_functions.str_euler_to_mat3) + return f'vec3( mat3(({input_invert} < 0.0) ? transpose(euler_to_mat3({input_rotation})) : euler_to_mat3({input_rotation})) * ({input_vector} - {input_center}) + {input_center})' + + return f'(vec3(1.0, 0.0, 0.0))' diff --git a/blender/arm/material/make.py b/blender/arm/material/make.py new file mode 100644 index 0000000000..ac1ab0ff9e --- /dev/null +++ b/blender/arm/material/make.py @@ -0,0 +1,170 @@ +from typing import Dict, List + +import bpy +from bpy.types import Material +from bpy.types import Object + +import arm.log as log +import arm.material.cycles as cycles +import arm.material.make_shader as make_shader +import arm.material.mat_batch as mat_batch +import arm.material.mat_utils as mat_utils +import arm.node_utils +import arm.utils + +if arm.is_reload(__name__): + log = arm.reload_module(log) + cycles = arm.reload_module(cycles) + make_shader = arm.reload_module(make_shader) + mat_batch = arm.reload_module(mat_batch) + mat_utils = arm.reload_module(mat_utils) + arm.node_utils = arm.reload_module(arm.node_utils) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def glsl_value(val): + if str(type(val)) == "": + res = [] + for v in val: + res.append(v) + return res + else: + return val + + +def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], mat_armusers) -> tuple: + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + # Texture caching for material batching + batch_cached_textures = [] + + needs_sss = material_needs_sss(material) + if needs_sss and rpdat.rp_sss_state != 'Off' and '_SSS' not in wrd.world_defs: + # Must be set before calling make_shader.build() + wrd.world_defs += '_SSS' + + # No batch - shader data per material + if material.arm_custom_material != '': + rpasses = ['mesh'] + + con = {'vertex_elements': []} + con['vertex_elements'].append({'name': 'pos', 'data': 'short4norm'}) + con['vertex_elements'].append({'name': 'nor', 'data': 'short2norm'}) + con['vertex_elements'].append({'name': 'tex', 'data': 'short2norm'}) + con['vertex_elements'].append({'name': 'tex1', 'data': 'short2norm'}) + + sd = {'contexts': [con]} + shader_data_name = material.arm_custom_material + bind_constants = {'mesh': []} + bind_textures = {'mesh': []} + + make_shader.make_instancing_and_skinning(material, mat_users) + + for idx, item in enumerate(material.arm_bind_textures_list): + if item.uniform_name == '': + log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} with empty uniform name') + continue + + if item.image is not None: + tex = cycles.make_texture(item.image, item.uniform_name, material.name, 'Linear', 'REPEAT') + if tex is None: + continue + bind_textures['mesh'].append(tex) + else: + log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} ("{item.uniform_name}") with no image selected') + + elif not wrd.arm_batch_materials or material.name.startswith('armdefault'): + rpasses, shader_data, shader_data_name, bind_constants, bind_textures = make_shader.build(material, mat_users, mat_armusers) + sd = shader_data.sd + else: + rpasses, shader_data, shader_data_name, bind_constants, bind_textures = mat_batch.get(material) + sd = shader_data.sd + + sss_used = False + + # Material + for rp in rpasses: + c = { + 'name': rp, + 'bind_constants': [] + bind_constants[rp], + 'bind_textures': [] + bind_textures[rp], + 'depth_read': material.arm_depth_read, + } + mat_data['contexts'].append(c) + + if rp == 'mesh': + c['bind_constants'].append({'name': 'receiveShadow', 'bool': material.arm_receive_shadow}) + + if material.arm_material_id != 0: + c['bind_constants'].append({'name': 'materialID', 'int': material.arm_material_id}) + + if material.arm_material_id == 2: + wrd.world_defs += '_Hair' + + elif rpdat.rp_sss_state != 'Off': + const = {'name': 'materialID'} + if needs_sss: + const['int'] = 2 + sss_used = True + else: + const['int'] = 0 + c['bind_constants'].append(const) + + # TODO: Mesh only material batching + if wrd.arm_batch_materials: + # Set textures uniforms + if len(c['bind_textures']) > 0: + c['bind_textures'] = [] + for node in material.node_tree.nodes: + if node.type == 'TEX_IMAGE': + tex_name = arm.utils.safesrc(node.name) + tex = cycles.make_texture_from_image_node(node, tex_name) + # Empty texture + if tex is None: + tex = {'name': tex_name, 'file': ''} + c['bind_textures'].append(tex) + batch_cached_textures = c['bind_textures'] + + # Set marked inputs as uniforms + for node in material.node_tree.nodes: + for inp in node.inputs: + if inp.is_uniform: + uname = arm.utils.safesrc(inp.node.name) + arm.utils.safesrc(inp.name) # Merge with cycles module + c['bind_constants'].append({'name': uname, cycles.glsl_type(inp.type): glsl_value(inp.default_value)}) + + elif rp == 'translucent' or rp == 'refraction': + c['bind_constants'].append({'name': 'receiveShadow', 'bool': material.arm_receive_shadow}) + + elif rp == 'shadowmap': + if wrd.arm_batch_materials: + if len(c['bind_textures']) > 0: + c['bind_textures'] = batch_cached_textures + + if wrd.arm_single_data_file: + mat_data['shader'] = shader_data_name + else: + # Make sure that custom materials are not expected to be in .arm format + ext = '' if wrd.arm_minimize and material.arm_custom_material == "" else '.json' + mat_data['shader'] = shader_data_name + ext + '/' + shader_data_name + + return sd, rpasses, sss_used + + +def material_needs_sss(material: Material) -> bool: + """Check whether the given material requires SSS.""" + for sss_node in arm.node_utils.iter_nodes_by_type(material.node_tree, 'SUBSURFACE_SCATTERING'): + if sss_node is not None and sss_node.outputs[0].is_linked: + return True + + for sss_node in arm.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'): + if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[1].is_linked or sss_node.inputs[1].default_value != 0.0): + return True + + for sss_node in mat_utils.iter_nodes_armorypbr(material.node_tree): + if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0): + return True + + return False diff --git a/blender/arm/material/make_attrib.py b/blender/arm/material/make_attrib.py new file mode 100644 index 0000000000..8aaaaf2486 --- /dev/null +++ b/blender/arm/material/make_attrib.py @@ -0,0 +1,100 @@ +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.make_tess as make_tess +import arm.material.make_morph_target as make_morph_target +from arm.material.shader import Shader, ShaderContext +import arm.utils + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + mat_state = arm.reload_module(mat_state) + make_skin = arm.reload_module(make_skin) + make_particle = arm.reload_module(make_particle) + make_inst = arm.reload_module(make_inst) + make_tess = arm.reload_module(make_tess) + make_morph_target = arm.reload_module(make_morph_target) + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader, ShaderContext + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def write_vertpos(vert): + billboard = mat_state.material.arm_billboard + particle = mat_state.material.arm_particle_flag + # Particles + if particle: + if arm.utils.get_rp().arm_particles == 'On': + make_particle.write(vert, particle_info=cycles.particle_info) + # Billboards + if billboard == 'spherical': + vert.add_uniform('mat4 WV', '_worldViewMatrix') + vert.add_uniform('mat4 P', '_projectionMatrix') + vert.write('gl_Position = P * (WV * vec4(0.0, 0.0, spos.z, 1.0) + vec4(spos.x, spos.y, 0.0, 0.0));') + else: + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.write('gl_Position = WVP * spos;') + else: + # Billboards + if billboard == 'spherical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixCylinder') + else: # off + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.write('gl_Position = WVP * spos;') + + +def write_norpos(con_mesh: ShaderContext, vert: Shader, declare=False, write_nor=True): + is_bone = con_mesh.is_elem('bone') + is_morph = con_mesh.is_elem('morph') + if is_morph: + make_morph_target.morph_pos(vert) + if is_bone: + make_skin.skin_pos(vert) + if write_nor: + prep = 'vec3 ' if declare else '' + if is_morph: + make_morph_target.morph_nor(vert, is_bone, prep) + if is_bone: + make_skin.skin_nor(vert, is_morph, prep) + if not is_morph and not is_bone: + 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_cluster.py b/blender/arm/material/make_cluster.py new file mode 100644 index 0000000000..31fcb1b06e --- /dev/null +++ b/blender/arm/material/make_cluster.py @@ -0,0 +1,92 @@ +import bpy + +import arm.material.shader as shader +import arm.utils + +if arm.is_reload(__name__): + shader = arm.reload_module(shader) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def write(vert: shader.Shader, frag: shader.Shader): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + is_mobile = rpdat.arm_material_model == 'Mobile' + is_shadows = '_ShadowMap' in wrd.world_defs + is_shadows_atlas = '_ShadowMapAtlas' in wrd.world_defs + is_single_atlas = '_SingleAtlas' in wrd.world_defs + + frag.add_include_front('std/clusters.glsl') + frag.add_uniform('vec2 cameraProj', link='_cameraPlaneProj') + frag.add_uniform('vec2 cameraPlane', link='_cameraPlane') + frag.add_uniform('vec4 lightsArray[maxLights * 3]', link='_lightsArray') + frag.add_uniform('sampler2D clustersData', link='_clustersData') + if is_shadows: + frag.add_uniform('bool receiveShadow') + frag.add_uniform('vec2 lightProj', link='_lightPlaneProj', included=True) + if is_shadows_atlas: + if not is_single_atlas: + frag.add_uniform('sampler2DShadow shadowMapAtlasPoint', included=True) + else: + frag.add_uniform('sampler2DShadow shadowMapAtlas', top=True) + frag.add_uniform('vec4 pointLightDataArray[maxLightsCluster]', link='_pointLightsAtlasArray', included=True) + else: + frag.add_uniform('samplerCubeShadow shadowMapPoint[4]', included=True) + vert.add_out('vec4 wvpposition') + vert.write('wvpposition = gl_Position;') + # wvpposition.z / wvpposition.w + frag.write('float viewz = linearize(gl_FragCoord.z, cameraProj);') + frag.write('int clusterI = getClusterI((wvpposition.xy / wvpposition.w) * 0.5 + 0.5, viewz, cameraPlane);') + frag.write('int numLights = int(texelFetch(clustersData, ivec2(clusterI, 0), 0).r * 255);') + + frag.write('#ifdef HLSL') + frag.write('viewz += texture(clustersData, vec2(0.0)).r * 1e-9;') # TODO: krafix bug, needs to generate sampler + frag.write('#endif') + + if '_Spot' in wrd.world_defs: + frag.add_uniform('vec4 lightsArraySpot[maxLights * 2]', link='_lightsArraySpot') + frag.write('int numSpots = int(texelFetch(clustersData, ivec2(clusterI, 1 + maxLightsCluster), 0).r * 255);') + frag.write('int numPoints = numLights - numSpots;') + if is_shadows: + if is_shadows_atlas: + if not is_single_atlas: + frag.add_uniform('sampler2DShadow shadowMapAtlasSpot', included=True) + else: + frag.add_uniform('sampler2DShadow shadowMapAtlas', top=True) + else: + frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) + # FIXME: type is actually mat4, but otherwise it will not be set as floats when writing the shaders' json files + frag.add_uniform('vec4 LWVPSpotArray[maxLightsCluster]', link='_biasLightWorldViewProjectionMatrixSpotArray', included=True) + + frag.write('for (int i = 0; i < min(numLights, maxLightsCluster); i++) {') + frag.write('int li = int(texelFetch(clustersData, ivec2(clusterI, i + 1), 0).r * 255);') + + frag.write('direct += sampleLight(') + frag.write(' wposition,') + frag.write(' n,') + frag.write(' vVec,') + frag.write(' dotNV,') + frag.write(' lightsArray[li * 3].xyz,') # lp + frag.write(' lightsArray[li * 3 + 1].xyz,') # lightCol + frag.write(' albedo,') + frag.write(' roughness,') + frag.write(' specular,') + frag.write(' f0') + if is_shadows: + frag.write('\t, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0') # bias + if '_Spot' in wrd.world_defs: + frag.write('\t, lightsArray[li * 3 + 2].y != 0.0') + frag.write('\t, lightsArray[li * 3 + 2].y') # spot size (cutoff) + frag.write('\t, lightsArraySpot[li].w') # spot blend (exponent) + frag.write('\t, lightsArraySpot[li].xyz') # spotDir + frag.write('\t, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w)') # scale + frag.write('\t, lightsArraySpot[li * 2 + 1].xyz') # right + if '_VoxelShadow' in wrd.world_defs and '_VoxelAOvar' in wrd.world_defs: + frag.write(' , voxels, voxpos') + if '_MicroShadowing' in wrd.world_defs and not is_mobile: + frag.write('\t, occlusion') + frag.write(');') + + frag.write('}') # for numLights diff --git a/blender/arm/material/make_decal.py b/blender/arm/material/make_decal.py new file mode 100644 index 0000000000..cb72b876aa --- /dev/null +++ b/blender/arm/material/make_decal.py @@ -0,0 +1,83 @@ +import bpy + +import arm.material.cycles as cycles +import arm.material.mat_state as mat_state +import arm.material.make_finalize as make_finalize +import arm.utils + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + mat_state = arm.reload_module(mat_state) + make_finalize = arm.reload_module(make_finalize) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def make(context_id): + wrd = bpy.data.worlds['Arm'] + + vs = [{'name': 'pos', 'data': 'float3'}] + con_decal = mat_state.data.add_context({ 'name': context_id, 'vertex_elements': vs, 'depth_write': False, 'compare_mode': 'less', 'cull_mode': 'clockwise', + 'blend_source': 'source_alpha', + 'blend_destination': 'inverse_source_alpha', + 'blend_operation': 'add', + 'color_writes_alpha': [False, False] + }) + + vert = con_decal.make_vert() + frag = con_decal.make_frag() + geom = None + tesc = None + tese = None + + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.add_uniform('mat3 N', '_normalMatrix') + vert.add_out('vec4 wvpposition') + vert.add_out('vec3 wnormal') + + vert.write('wnormal = N * vec3(0.0, 0.0, 1.0);') + vert.write('wvpposition = WVP * vec4(pos.xyz, 1.0);') + vert.write('gl_Position = wvpposition;') + + frag.add_include('compiled.inc') + frag.add_include('std/gbuffer.glsl') + frag.ins = vert.outs + frag.add_uniform('sampler2D gbufferD') + frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix') + frag.add_uniform('mat4 invW', '_inverseWorldMatrix') + frag.add_out('vec4 fragColor[2]') + + frag.write_attrib(' vec3 n = normalize(wnormal);') + + frag.write_attrib(' vec2 screenPosition = wvpposition.xy / wvpposition.w;') + frag.write_attrib(' vec2 depthCoord = screenPosition * 0.5 + 0.5;') + frag.write_attrib('#ifdef _InvY') + frag.write_attrib(' depthCoord.y = 1.0 - depthCoord.y;') + frag.write_attrib('#endif') + frag.write_attrib(' float depth = texture(gbufferD, depthCoord).r * 2.0 - 1.0;') + + frag.write_attrib(' vec3 wpos = getPos2(invVP, depth, depthCoord);') + frag.write_attrib(' vec4 mpos = invW * vec4(wpos, 1.0);') + frag.write_attrib(' if (abs(mpos.x) > 1.0) discard;') + frag.write_attrib(' if (abs(mpos.y) > 1.0) discard;') + frag.write_attrib(' if (abs(mpos.z) > 1.0) discard;') + frag.write_attrib(' vec2 texCoord = mpos.xy * 0.5 + 0.5;') + + frag.write('vec3 basecol;') + frag.write('float roughness;') + frag.write('float metallic;') + frag.write('float occlusion;') + frag.write('float specular;') + frag.write('float opacity;') + frag.write('vec3 emissionCol;') # Declared to prevent compiler errors, but decals currently don't output any emission + cycles.parse(mat_state.nodes, con_decal, vert, frag, geom, tesc, tese) + + frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') + frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') + frag.write('fragColor[0] = vec4(n.xy, roughness, opacity);') + frag.write('fragColor[1] = vec4(basecol.rgb, opacity);') + + make_finalize.make(con_decal) + + return con_decal diff --git a/blender/arm/material/make_depth.py b/blender/arm/material/make_depth.py new file mode 100644 index 0000000000..f81ca5800b --- /dev/null +++ b/blender/arm/material/make_depth.py @@ -0,0 +1,203 @@ +import bpy + +import arm.material.cycles as cycles +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils +import arm.material.make_skin as make_skin +import arm.material.make_inst as make_inst +import arm.material.make_tess as make_tess +import arm.material.make_particle as make_particle +import arm.material.make_finalize as make_finalize +import arm.material.make_morph_target as make_morph_target +import arm.assets as assets +import arm.utils + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + mat_state = arm.reload_module(mat_state) + mat_utils = arm.reload_module(mat_utils) + make_skin = arm.reload_module(make_skin) + make_inst = arm.reload_module(make_inst) + make_tess = arm.reload_module(make_tess) + make_particle = arm.reload_module(make_particle) + make_finalize = arm.reload_module(make_finalize) + assets = arm.reload_module(assets) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def make(context_id, rpasses, shadowmap=False): + + is_disp = mat_utils.disp_linked(mat_state.output_node) + + vs = [{'name': 'pos', 'data': 'short4norm'}] + if is_disp: + vs.append({'name': 'nor', 'data': 'short2norm'}) + + con_depth = mat_state.data.add_context({ 'name': context_id, 'vertex_elements': vs, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise', 'color_writes_red': [False], 'color_writes_green': [False], 'color_writes_blue': [False], 'color_writes_alpha': [False] }) + + vert = con_depth.make_vert() + frag = con_depth.make_frag() + geom = None + tesc = None + tese = None + + vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') + vert.add_include('compiled.inc') + + parse_opacity = 'translucent' in rpasses or mat_state.material.arm_discard or 'refraction' in rpasses + + parse_custom_particle = (cycles.node_by_name(mat_state.nodes, 'ArmCustomParticleNode') is not None) + + if parse_opacity: + frag.write('float opacity;') + #frag.write('float rior;') + + if(con_depth).is_elem('morph'): + make_morph_target.morph_pos(vert) + + if con_depth.is_elem('bone'): + make_skin.skin_pos(vert) + + if (not is_disp and parse_custom_particle): + cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=parse_opacity) + + if con_depth.is_elem('ipos'): + make_inst.inst_pos(con_depth, vert) + + rpdat = arm.utils.get_rp() + if mat_state.material.arm_particle_flag and rpdat.arm_particles == 'On': + make_particle.write(vert, shadowmap=shadowmap) + + if is_disp: + if rpdat.arm_rp_displacement == 'Vertex': + frag.ins = vert.outs + vert.add_uniform('mat3 N', '_normalMatrix') + vert.write('vec3 wnormal = normalize(N * vec3(nor.xy, pos.w));') + if(con_depth.is_elem('ipos')): + vert.write('wposition = vec4(W * spos).xyz;') + if(con_depth.is_elem('irot')): + vert.write('wnormal = normalize(N * mirot * vec3(nor.xy, pos.w));') + cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=parse_opacity) + if con_depth.is_elem('tex'): + vert.add_out('vec2 texCoord') ## vs only, remove out + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.write_attrib('texCoord = tex * texUnpack;') + if con_depth.is_elem('tex1'): + vert.add_out('vec2 texCoord1') ## vs only, remove out + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.write_attrib('texCoord1 = tex1 * texUnpack;') + if con_depth.is_elem('col'): + vert.add_out('vec3 vcolor') + vert.write_attrib('vcolor = col.rgb;') + vert.write('wposition += wnormal * disp;') + if shadowmap: + vert.add_uniform('mat4 LVP', '_lightViewProjectionMatrix') + vert.write('gl_Position = LVP * vec4(wposition, 1.0);') + else: + vert.add_uniform('mat4 VP', '_viewProjectionMatrix') + vert.write('gl_Position = VP * vec4(wposition, 1.0);') + + else: # Tessellation + tesc = con_depth.make_tesc() + tese = con_depth.make_tese() + tesc.ins = vert.outs + tese.ins = tesc.outs + frag.ins = tese.outs + + vert.add_out('vec3 wnormal') + vert.add_uniform('mat3 N', '_normalMatrix') + vert.write('wnormal = normalize(N * vec3(nor.xy, pos.w));') + + make_tess.tesc_levels(tesc, rpdat.arm_tess_shadows_inner, rpdat.arm_tess_shadows_outer) + make_tess.interpolate(tese, 'wposition', 3) + make_tess.interpolate(tese, 'wnormal', 3, normalize=True) + + cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=parse_opacity) + + if con_depth.is_elem('tex'): + vert.add_out('vec2 texCoord') + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.write('texCoord = tex * texUnpack;') + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord', 2, declare_out=frag.contains('texCoord')) + tese.write_pre = False + + if con_depth.is_elem('tex1'): + vert.add_out('vec2 texCoord1') + vert.write('texCoord1 = tex1;') + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord1', 2, declare_out=frag.contains('texCoord1')) + tese.write_pre = False + + if con_depth.is_elem('col'): + vert.add_out('vec3 vcolor') + vert.write('vcolor = col.rgb;') + tese.write_pre = True + make_tess.interpolate(tese, 'vcolor', 3, declare_out=frag.contains('vcolor')) + tese.write_pre = False + + if shadowmap: + tese.add_uniform('mat4 LVP', '_lightViewProjectionMatrix') + tese.write('wposition += wnormal * disp;') + tese.write('gl_Position = LVP * vec4(wposition, 1.0);') + else: + tese.add_uniform('mat4 VP', '_viewProjectionMatrix') + tese.write('wposition += wnormal * disp;') + tese.write('gl_Position = VP * vec4(wposition, 1.0);') + # No displacement + else: + frag.ins = vert.outs + billboard = mat_state.material.arm_billboard + if shadowmap: + if billboard == 'spherical': + vert.add_uniform('mat4 LWVP', '_lightWorldViewProjectionMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat4 LWVP', '_lightWorldViewProjectionMatrixCylinder') + else: # off + vert.add_uniform('mat4 LWVP', '_lightWorldViewProjectionMatrix') + vert.write('gl_Position = LWVP * spos;') + else: + if billboard == 'spherical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixCylinder') + else: # off + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.write('gl_Position = WVP * spos;') + + if parse_opacity: + if (not parse_custom_particle): + cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=True) + + if con_depth.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;') + + if con_depth.is_elem('tex1'): + vert.add_out('vec2 texCoord1') + vert.write('texCoord1 = tex1;') + + if con_depth.is_elem('col'): + vert.add_out('vec3 vcolor') + vert.write('vcolor = col.rgb;') + + if parse_opacity: + if mat_state.material.arm_discard: + opac = mat_state.material.arm_discard_opacity_shadows + else: + opac = '1.0' + frag.write('if (opacity < {0}) discard;'.format(opac)) + + make_finalize.make(con_depth) + + assets.vs_equal(con_depth, assets.shader_cons['depth_vert']) + assets.fs_equal(con_depth, assets.shader_cons['depth_frag']) + + return con_depth diff --git a/blender/arm/material/make_finalize.py b/blender/arm/material/make_finalize.py new file mode 100644 index 0000000000..7525b517cf --- /dev/null +++ b/blender/arm/material/make_finalize.py @@ -0,0 +1,147 @@ +import bpy + +import arm +import arm.material.make_tess as make_tess +from arm.material.shader import ShaderContext + +if arm.is_reload(__name__): + make_tess = arm.reload_module(make_tess) + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import ShaderContext +else: + arm.enable_reload(__name__) + + +def make(con_mesh: ShaderContext): + vert = con_mesh.vert + frag = con_mesh.frag + geom = con_mesh.geom + tesc = con_mesh.tesc + tese = con_mesh.tese + + # Additional values referenced in cycles + # TODO: enable from cycles.py + 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 is not None: + tese.add_out('vec3 eyeDir') + tese.add_uniform('vec3 eye', '_cameraPosition') + tese.write('eyeDir = eye - wposition;') + + else: + if not vert.contains('wposition'): + write_wpos = True + vert.add_out('vec3 eyeDir') + vert.add_uniform('vec3 eye', '_cameraPosition') + vert.write('eyeDir = eye - wposition;') + frag.write_attrib('vec3 vVec = normalize(eyeDir);') + + export_wpos = False + if frag.contains('wposition') and not frag.contains('vec3 wposition'): + export_wpos = True + if tese is not None: + export_wpos = True + if vert.contains('wposition'): + write_wpos = True + + if export_wpos: + vert.add_uniform('mat4 W', '_worldMatrix') + vert.add_out('vec3 wposition') + vert.write_attrib('wposition = vec4(W * spos).xyz;') + elif write_wpos: + vert.add_uniform('mat4 W', '_worldMatrix') + vert.write_attrib('vec3 wposition = vec4(W * spos).xyz;') + + frag_mpos = (frag.contains('mposition') and not frag.contains('vec3 mposition')) or vert.contains('mposition') + if frag_mpos: + vert.add_out('vec3 mposition') + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write_attrib('mposition = spos.xyz * posUnpack;') + + 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'): + vert.add_out('vec3 mposition') + vert.write_pre = True + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write('mposition = spos.xyz * posUnpack;') + vert.write_pre = False + make_tess.interpolate(tese, 'mposition', 3, declare_out=False) + + frag_bpos = (frag.contains('bposition') and not frag.contains('vec3 bposition')) or vert.contains('bposition') + if frag_bpos: + vert.add_out('vec3 bposition') + vert.add_uniform('vec3 dim', link='_dim') + vert.add_uniform('vec3 hdim', link='_halfDim') + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write_attrib('bposition = (spos.xyz * posUnpack + hdim) / dim;') + vert.write_attrib('if (dim.z == 0) bposition.z = 0;') + vert.write_attrib('if (dim.y == 0) bposition.y = 0;') + vert.write_attrib('if (dim.x == 0) bposition.x = 0;') + + 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'): + vert.add_out('vec3 bposition') + vert.add_uniform('vec3 dim', link='_dim') + vert.add_uniform('vec3 hdim', link='_halfDim') + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write_attrib('bposition = (spos.xyz * posUnpack + hdim) / dim;') + make_tess.interpolate(tese, 'bposition', 3, declare_out=False) + + frag_wtan = (frag.contains('wtangent') and not frag.contains('vec3 wtangent')) or vert.contains('wtangent') + if frag_wtan: + # Indicate we want tang attrib in finalizer to prevent TBN generation + con_mesh.add_elem('tex', 'short2norm') + con_mesh.add_elem('tang', 'short4norm') + vert.add_out('vec3 wtangent') + vert.write_pre = True + vert.write('wtangent = normalize(N * tang.xyz);') + vert.write_pre = False + + 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'): + vert.add_out('vec3 wtangent') + vert.write_pre = True + vert.write('wtangent = normalize(N * tang.xyz);') + vert.write_pre = False + make_tess.interpolate(tese, 'wtangent', 3, declare_out=False) + + if frag.contains('vVecCam'): + vert.add_out('vec3 eyeDirCam') + vert.add_uniform('mat4 WV', '_worldViewMatrix') + vert.write('eyeDirCam = vec4(WV * spos).xyz; eyeDirCam.z *= -1;') + frag.write_attrib('vec3 vVecCam = normalize(eyeDirCam);') + + if frag.contains('nAttr'): + vert.add_out('vec3 nAttr') + vert.write_attrib('nAttr = vec3(nor.xy, pos.w);') + + wrd = bpy.data.worlds['Arm'] + if '_Legacy' in wrd.world_defs: + frag.replace('sampler2DShadow', 'sampler2D') + frag.replace('samplerCubeShadow', 'samplerCube') diff --git a/blender/arm/material/make_inst.py b/blender/arm/material/make_inst.py new file mode 100644 index 0000000000..a3d77f2d60 --- /dev/null +++ b/blender/arm/material/make_inst.py @@ -0,0 +1,24 @@ + +def inst_pos(con, vert): + if con.is_elem('irot'): + # http://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToMatrix/index.htm + vert.write('float srotx = sin(irot.x);') + vert.write('float crotx = cos(irot.x);') + vert.write('float sroty = sin(irot.y);') + vert.write('float croty = cos(irot.y);') + vert.write('float srotz = sin(irot.z);') + vert.write('float crotz = cos(irot.z);') + vert.write('mat3 mirot = mat3(') + vert.write(' croty * crotz, srotz, -sroty * crotz,') + vert.write(' -croty * srotz * crotx + sroty * srotx, crotz * crotx, sroty * srotz * crotx + croty * srotx,') + vert.write(' croty * srotz * srotx + sroty * crotx, -crotz * srotx, -sroty * srotz * srotx + croty * crotx') + vert.write(');') + vert.write('spos.xyz = mirot * spos.xyz;') + if (con.data['name'] == 'mesh' or con.data['name'] == 'translucent' or con.data['name'] == 'refraction') and vert.contains('wnormal'): + vert.write('wnormal = normalize(N * mirot * vec3(nor.xy, pos.w));') + + + if con.is_elem('iscl'): + vert.write('spos.xyz *= iscl;') + + vert.write('spos.xyz += ipos;') diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py new file mode 100644 index 0000000000..70e3fe0ce7 --- /dev/null +++ b/blender/arm/material/make_mesh.py @@ -0,0 +1,755 @@ +from typing import Any, Callable, Optional + +import bpy + +import arm.assets as assets +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils +import arm.material.cycles as cycles +import arm.material.make_tess as make_tess +import arm.material.make_particle as make_particle +import arm.material.make_cluster as make_cluster +import arm.material.make_finalize as make_finalize +import arm.material.make_attrib as make_attrib +import arm.material.shader as shader +import arm.utils + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + mat_state = arm.reload_module(mat_state) + mat_utils = arm.reload_module(mat_utils) + cycles = arm.reload_module(cycles) + make_tess = arm.reload_module(make_tess) + make_particle = arm.reload_module(make_particle) + make_cluster = arm.reload_module(make_cluster) + make_finalize = arm.reload_module(make_finalize) + make_attrib = arm.reload_module(make_attrib) + shader = arm.reload_module(shader) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +is_displacement = False + +# User callbacks +write_material_attribs: Optional[Callable[[dict[str, Any], shader.Shader], bool]] = None +write_material_attribs_post: Optional[Callable[[dict[str, Any], shader.Shader], None]] = None +write_vertex_attribs: Optional[Callable[[shader.Shader], bool]] = None + + +def make(context_id, rpasses): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + rid = rpdat.rp_renderer + + con = { 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' } + + # Blend context + mat = mat_state.material + blend = mat.arm_blending + particle = mat.arm_particle_flag + dprepass = rid == 'Forward' and rpdat.rp_depthprepass + if blend: + con['name'] = 'blend' + con['blend_source'] = mat.arm_blending_source + con['blend_destination'] = mat.arm_blending_destination + con['blend_operation'] = mat.arm_blending_operation + con['alpha_blend_source'] = mat.arm_blending_source_alpha + con['alpha_blend_destination'] = mat.arm_blending_destination_alpha + con['alpha_blend_operation'] = mat.arm_blending_operation_alpha + con['depth_write'] = False + con['compare_mode'] = 'less' + elif particle: + pass + # Depth prepass was performed, exclude mat with depth read that + # isn't part of depth prepass + elif dprepass and not (rpdat.rp_depth_texture and mat.arm_depth_read): + con['depth_write'] = False + con['compare_mode'] = 'equal' + + attachment_format = 'RGBA32' if '_LDR' in wrd.world_defs else 'RGBA64' + con['color_attachments'] = [attachment_format, attachment_format] + if '_gbuffer2' in wrd.world_defs: + con['color_attachments'].append(attachment_format) + + con_mesh = mat_state.data.add_context(con) + mat_state.con_mesh = con_mesh + + if rid == 'Forward' or blend: + if rpdat.arm_material_model == 'Mobile': + make_forward_mobile(con_mesh) + elif rpdat.arm_material_model == 'Solid': + make_forward_solid(con_mesh) + else: + make_forward(con_mesh, rpasses) + elif rid == 'Deferred': + make_deferred(con_mesh, rpasses) + elif rid == 'Raytracer': + make_raytracer(con_mesh) + + make_finalize.make(con_mesh) + + assets.vs_equal(con_mesh, assets.shader_cons['mesh_vert']) + + return con_mesh + + +def make_base(con_mesh, parse_opacity): + global is_displacement + global write_vertex_attribs + + vert = con_mesh.make_vert() + frag = con_mesh.make_frag() + geom = None + tesc = None + tese = None + + vert.add_uniform('mat3 N', '_normalMatrix') + vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') + + vattr_written = False + rpdat = arm.utils.get_rp() + is_displacement = mat_utils.disp_linked(mat_state.output_node) + if is_displacement: + if rpdat.arm_rp_displacement == 'Vertex': + frag.ins = vert.outs + else: # Tessellation + tesc = con_mesh.make_tesc() + tese = con_mesh.make_tese() + tesc.ins = vert.outs + tese.ins = tesc.outs + frag.ins = tese.outs + make_tess.tesc_levels(tesc, rpdat.arm_tess_mesh_inner, rpdat.arm_tess_mesh_outer) + make_tess.interpolate(tese, 'wposition', 3, declare_out=True) + make_tess.interpolate(tese, 'wnormal', 3, declare_out=True, normalize=True) + + # No displacement + else: + frag.ins = vert.outs + if write_vertex_attribs is not None: + vattr_written = write_vertex_attribs(vert) + + vert.add_include('compiled.inc') + frag.add_include('compiled.inc') + + attribs_written = False + if write_material_attribs is not None: + attribs_written = write_material_attribs(con_mesh, frag) + if not attribs_written: + _write_material_attribs_default(frag, parse_opacity) + cycles.parse(mat_state.nodes, con_mesh, vert, frag, geom, tesc, tese, parse_opacity=parse_opacity) + if write_material_attribs_post is not None: + write_material_attribs_post(con_mesh, frag) + + vert.add_out('vec3 wnormal') + make_attrib.write_norpos(con_mesh, vert) + frag.write_attrib('vec3 n = normalize(wnormal);') + + if not is_displacement and not vattr_written: + make_attrib.write_vertpos(vert) + + make_attrib.write_tex_coords(con_mesh, vert, frag, tese) + + if con_mesh.is_elem('col'): + vert.add_out('vec3 vcolor') + vert.write_attrib('vcolor = col.rgb;') + if tese is not None: + tese.write_pre = True + make_tess.interpolate(tese, 'vcolor', 3, declare_out=frag.contains('vcolor')) + tese.write_pre = False + + if con_mesh.is_elem('tang'): + if tese is not None: + tese.add_out('mat3 TBN') + tese.write_attrib('vec3 wbitangent = normalize(cross(wnormal, wtangent));') + tese.write_attrib('TBN = mat3(wtangent, wbitangent, wnormal);') + else: + vert.add_out('mat3 TBN') + vert.write_attrib('vec3 tangent = normalize(N * tang.xyz);') + vert.write_attrib('vec3 bitangent = normalize(cross(wnormal, tangent));') + vert.write_attrib('TBN = mat3(tangent, bitangent, wnormal);') + + if is_displacement: + if rpdat.arm_rp_displacement == 'Vertex': + sh = vert + else: + sh = tese + if(con_mesh.is_elem('ipos')): + vert.write('wposition = vec4(W * spos).xyz;') + sh.add_uniform('mat4 VP', '_viewProjectionMatrix') + sh.write('wposition += wnormal * disp;') + sh.write('gl_Position = VP * vec4(wposition, 1.0);') + + +def make_deferred(con_mesh, rpasses): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + arm_discard = mat_state.material.arm_discard + parse_opacity = arm_discard or 'translucent' in rpasses or 'refraction' in rpasses + make_base(con_mesh, parse_opacity=parse_opacity) + + frag = con_mesh.frag + vert = con_mesh.vert + tese = con_mesh.tese + + if parse_opacity and not 'refraction' in rpasses: + if arm_discard: + opac = mat_state.material.arm_discard_opacity + else: + opac = '0.9999' # 1.0 - eps + frag.write('if (opacity < {0}) discard;'.format(opac)) + + frag.add_out(f'vec4 fragColor[GBUF_SIZE]') + + if '_gbuffer2' in wrd.world_defs: + if '_Veloc' in wrd.world_defs: + if tese is None: + vert.add_uniform('mat4 prevWVP', link='_prevWorldViewProjectionMatrix') + vert.add_out('vec4 wvpposition') + vert.add_out('vec4 prevwvpposition') + vert.write('wvpposition = gl_Position;') + if is_displacement: + vert.add_uniform('mat4 invW', link='_inverseWorldMatrix') + vert.write('prevwvpposition = prevWVP * (invW * vec4(wposition, 1.0));') + else: + vert.write('prevwvpposition = prevWVP * spos;') + else: + tese.add_out('vec4 wvpposition') + tese.add_out('vec4 prevwvpposition') + tese.write('wvpposition = gl_Position;') + if is_displacement: + tese.add_uniform('mat4 invW', link='_inverseWorldMatrix') + tese.add_uniform('mat4 prevWVP', '_prevWorldViewProjectionMatrix') + tese.write('prevwvpposition = prevWVP * (invW * vec4(wposition, 1.0));') + else: + vert.add_uniform('mat4 prevW', link='_prevWorldMatrix') + vert.add_out('vec3 prevwposition') + vert.write('prevwposition = vec4(prevW * spos).xyz;') + tese.add_uniform('mat4 prevVP', '_prevViewProjectionMatrix') + make_tess.interpolate(tese, 'prevwposition', 3) + tese.write('prevwvpposition = prevVP * vec4(prevwposition, 1.0);') + + # Pack gbuffer + frag.add_include('std/gbuffer.glsl') + + if mat_state.material.arm_two_sided: + frag.write('if (!gl_FrontFacing) n *= -1;') # Flip normal when drawing back-face + + frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') + frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') + + is_shadeless = mat_state.emission_type == mat_state.EmissionType.SHADELESS + if is_shadeless or '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs: + frag.write('uint matid = 0;') + if is_shadeless: + frag.write('matid = 1;') + frag.write('basecol = emissionCol;') + if '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs: + frag.add_uniform('int materialID') + frag.write('if (materialID == 2) matid = 2;') + else: + frag.write('const uint matid = 0;') + + frag.write('fragColor[GBUF_IDX_0] = vec4(n.xy, roughness, packFloatInt16(metallic, matid));') + frag.write('fragColor[GBUF_IDX_1] = vec4(basecol, packFloat2(occlusion, specular));') + + if '_gbuffer2' in wrd.world_defs: + if '_Veloc' in wrd.world_defs: + frag.write('vec2 posa = (wvpposition.xy / wvpposition.w) * 0.5 + 0.5;') + frag.write('vec2 posb = (prevwvpposition.xy / prevwvpposition.w) * 0.5 + 0.5;') + frag.write('fragColor[GBUF_IDX_2].rg = vec2(posa - posb);') + + if mat_state.material.arm_ignore_irradiance: + frag.write('fragColor[GBUF_IDX_2].b = 1.0;') + + # Even if the material doesn't use emission we need to write to the + # emission buffer (if used) to prevent undefined behaviour + frag.write('#ifdef _EmissionShaded') + frag.write('fragColor[GBUF_IDX_EMISSION] = vec4(emissionCol, 0.0);') #Alpha channel is unused at the moment + frag.write('#endif') + + if '_SSRefraction' in wrd.world_defs: + if 'refraction' in rpasses: + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(rior, opacity, 0.0, 0.0);') + else: + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 0.0);') + + return con_mesh + + +def make_raytracer(con_mesh): + con_mesh.data['vertex_elements'] = [{'name': 'pos', 'data': 'float3'}, {'name': 'nor', 'data': 'float3'}, {'name': 'tex', 'data': 'float2'}] + wrd = bpy.data.worlds['Arm'] + vert = con_mesh.make_vert() + frag = con_mesh.make_frag() + vert.add_out('vec3 n') + vert.add_out('vec2 uv') + vert.write('n = nor;') + vert.write('uv = tex;') + vert.write('gl_Position = vec4(pos.xyz, 1.0);') + + +def make_forward_mobile(con_mesh): + wrd = bpy.data.worlds['Arm'] + vert = con_mesh.make_vert() + frag = con_mesh.make_frag() + geom = None + tesc = None + tese = None + + vert.add_uniform('mat3 N', '_normalMatrix') + vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') + frag.ins = vert.outs + + vert.add_include('compiled.inc') + frag.add_include('compiled.inc') + + arm_discard = mat_state.material.arm_discard + blend = mat_state.material.arm_blending + is_transluc = mat_utils.is_transluc(mat_state.material) + parse_opacity = (blend and is_transluc) or arm_discard + + _write_material_attribs_default(frag, parse_opacity) + cycles.parse(mat_state.nodes, con_mesh, vert, frag, geom, tesc, tese, parse_opacity=parse_opacity, parse_displacement=False) + + if arm_discard: + opac = mat_state.material.arm_discard_opacity + frag.write('if (opacity < {0}) discard;'.format(opac)) + + make_attrib.write_tex_coords(con_mesh, vert, frag, tese) + + if con_mesh.is_elem('col'): + vert.add_out('vec3 vcolor') + vert.write('vcolor = col.rgb;') + + if con_mesh.is_elem('tang'): + vert.add_out('mat3 TBN') + make_attrib.write_norpos(con_mesh, vert, declare=True) + vert.write('vec3 tangent = normalize(N * tang.xyz);') + vert.write('vec3 bitangent = normalize(cross(wnormal, tangent));') + vert.write('TBN = mat3(tangent, bitangent, wnormal);') + else: + vert.add_out('vec3 wnormal') + 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') + + frag.add_out('vec4 fragColor') + blend = mat_state.material.arm_blending + if blend: + if parse_opacity: + frag.write('fragColor = vec4(basecol, opacity);') + else: + frag.write('fragColor = vec4(basecol, 1.0);') + return + + is_shadows = '_ShadowMap' in wrd.world_defs + is_shadows_atlas = '_ShadowMapAtlas' in wrd.world_defs + shadowmap_sun = 'shadowMap' + if is_shadows_atlas: + is_single_atlas = '_SingleAtlas' in wrd.world_defs + shadowmap_sun = 'shadowMapAtlasSun' if not is_single_atlas else 'shadowMapAtlas' + frag.add_uniform('vec2 smSizeUniform', '_shadowMapSize', included=True) + frag.write('vec3 direct = vec3(0.0);') + + if '_Sun' in wrd.world_defs: + frag.add_uniform('vec3 sunCol', '_sunColor') + frag.add_uniform('vec3 sunDir', '_sunDirection') + frag.write('float svisibility = 1.0;') + frag.write('float sdotNL = max(dot(n, sunDir), 0.0);') + if is_shadows: + vert.add_out('vec4 lightPosition') + vert.add_uniform('mat4 LWVP', '_biasLightWorldViewProjectionMatrixSun') + vert.write('lightPosition = LWVP * spos;') + frag.add_uniform('bool receiveShadow') + frag.add_uniform(f'sampler2DShadow {shadowmap_sun}') + frag.add_uniform('float shadowsBias', '_sunShadowsBias') + + frag.write('if (receiveShadow) {') + if '_CSM' in wrd.world_defs: + frag.add_include('std/shadows.glsl') + frag.add_uniform('vec4 casData[shadowmapCascades * 4 + 4]', '_cascadeData', included=True) + frag.add_uniform('vec3 eye', '_cameraPosition') + frag.write(f'svisibility = shadowTestCascade({shadowmap_sun}, eye, wposition + n * shadowsBias * 10, shadowsBias);') + else: + frag.write('if (lightPosition.w > 0.0) {') + frag.write(' vec3 lPos = lightPosition.xyz / lightPosition.w;') + if '_Legacy' in wrd.world_defs: + frag.write(f'svisibility = float(texture({shadowmap_sun}, vec2(lPos.xy)).r > lPos.z - shadowsBias);') + else: + frag.write(f'svisibility = texture({shadowmap_sun}, vec3(lPos.xy, lPos.z - shadowsBias)).r;') + frag.write('}') + frag.write('}') # receiveShadow + frag.write('direct += basecol * sdotNL * sunCol * svisibility;') + + if '_SinglePoint' in wrd.world_defs: + frag.add_uniform('vec3 pointPos', '_pointPosition') + frag.add_uniform('vec3 pointCol', '_pointColor') + if '_Spot' in wrd.world_defs: + frag.add_uniform('vec3 spotDir', link='_spotDirection') + frag.add_uniform('vec3 spotRight', link='_spotRight') + frag.add_uniform('vec4 spotData', link='_spotData') + frag.write('float visibility = 1.0;') + frag.write('vec3 ld = pointPos - wposition;') + frag.write('vec3 l = normalize(ld);') + frag.write('float dotNL = max(dot(n, l), 0.0);') + if is_shadows: + frag.add_uniform('bool receiveShadow') + frag.add_uniform('float pointBias', link='_pointShadowsBias') + frag.add_include('std/shadows.glsl') + + frag.write('if (receiveShadow) {') + if '_Spot' in wrd.world_defs: + vert.add_out('vec4 spotPosition') + vert.add_uniform('mat4 LWVPSpotArray[1]', link='_biasLightWorldViewProjectionMatrixSpotArray') + vert.write('spotPosition = LWVPSpotArray[0] * spos;') + frag.add_uniform('sampler2DShadow shadowMapSpot[1]') + frag.write('if (spotPosition.w > 0.0) {') + frag.write(' vec3 lPos = spotPosition.xyz / spotPosition.w;') + if '_Legacy' in wrd.world_defs: + frag.write(' visibility = float(texture(shadowMapSpot[0], vec2(lPos.xy)).r > lPos.z - pointBias);') + else: + frag.write(' visibility = texture(shadowMapSpot[0], vec3(lPos.xy, lPos.z - pointBias)).r;') + frag.write('}') + else: + frag.add_uniform('vec2 lightProj', link='_lightPlaneProj') + frag.add_uniform('samplerCubeShadow shadowMapPoint[1]') + frag.write('const float s = shadowmapCubePcfSize;') # TODO: incorrect... + frag.write('float compare = lpToDepth(ld, lightProj) - pointBias * 1.5;') + frag.write('#ifdef _InvY') + frag.write('l.y = -l.y;') + frag.write('#endif') + if '_Legacy' in wrd.world_defs: + frag.write('visibility = float(texture(shadowMapPoint[0], vec3(-l + n * pointBias * 20)).r > compare);') + else: + frag.write('visibility = texture(shadowMapPoint[0], vec4(-l + n * pointBias * 20, compare)).r;') + frag.write('}') # receiveShadow + + frag.write('direct += basecol * dotNL * pointCol * attenuate(distance(wposition, pointPos)) * visibility;') + + if '_Clusters' in wrd.world_defs: + frag.add_include('std/light_mobile.glsl') + frag.write('vec3 albedo = basecol;') + frag.write('vec3 f0 = surfaceF0(basecol, metallic);') + make_cluster.write(vert, frag) + + if '_Irr' in wrd.world_defs: + frag.add_include('std/shirr.glsl') + frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance') + env_str = 'shIrradiance(n, shirr)' + else: + env_str = '0.5' + + frag.add_uniform('float envmapStrength', link='_envmapStrength') + frag.write('fragColor = vec4(direct + basecol * {0} * envmapStrength, 1.0);'.format(env_str)) + + if '_LDR' in wrd.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));') + + +def make_forward_solid(con_mesh): + wrd = bpy.data.worlds['Arm'] + vert = con_mesh.make_vert() + frag = con_mesh.make_frag() + geom = None + tesc = None + tese = None + + for e in con_mesh.data['vertex_elements']: + if e['name'] == 'nor': + con_mesh.data['vertex_elements'].remove(e) + break + + vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') + frag.ins = vert.outs + + vert.add_include('compiled.inc') + frag.add_include('compiled.inc') + + arm_discard = mat_state.material.arm_discard + blend = mat_state.material.arm_blending + is_transluc = mat_utils.is_transluc(mat_state.material) + parse_opacity = (blend and is_transluc) or arm_discard + + _write_material_attribs_default(frag, parse_opacity) + cycles.parse(mat_state.nodes, con_mesh, vert, frag, geom, tesc, tese, parse_opacity=parse_opacity, parse_displacement=False, basecol_only=True) + + if arm_discard: + 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;') + + if con_mesh.is_elem('col'): + vert.add_out('vec3 vcolor') + 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: + frag.write('fragColor = vec4(basecol, opacity);') + else: + frag.write('fragColor = vec4(basecol, 1.0);') + + if '_LDR' in wrd.world_defs: + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));') + + +def make_forward(con_mesh, rpasses): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + blend = mat_state.material.arm_blending + parse_opacity = blend or mat_utils.is_transluc(mat_state.material) + + make_forward_base(con_mesh, parse_opacity=parse_opacity) + frag = con_mesh.frag + + if '_LTC' in wrd.world_defs: + frag.add_uniform('vec3 lightArea0', '_lightArea0', included=True) + frag.add_uniform('vec3 lightArea1', '_lightArea1', included=True) + frag.add_uniform('vec3 lightArea2', '_lightArea2', included=True) + frag.add_uniform('vec3 lightArea3', '_lightArea3', included=True) + frag.add_uniform('sampler2D sltcMat', '_ltcMat', included=True) + frag.add_uniform('sampler2D sltcMag', '_ltcMag', included=True) + if '_ShadowMap' in wrd.world_defs: + if '_SinglePoint' in wrd.world_defs: + frag.add_uniform('mat4 LWVPSpot[0]', link='_biasLightViewProjectionMatrixSpot0', included=True) + frag.add_uniform('sampler2DShadow shadowMapSpot[1]', included=True) + if '_Clusters' in wrd.world_defs: + frag.add_uniform('mat4 LWVPSpotArray[4]', link='_biasLightWorldViewProjectionMatrixSpotArray', included=True) + frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) + + if not blend: + mrt = 1 + if rpdat.rp_ssr: + mrt += 1 + if rpdat.rp_ss_refraction: + mrt += 1 + if mrt > 1: + # Store light gbuffer for post-processing + frag.add_out('vec4 fragColor[{0}]'.format(mrt)) + frag.add_include('std/gbuffer.glsl') + frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') + frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') + frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') + frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') + + if mrt > 2 in wrd.world_defs: + if 'refraction' in rpasses: + frag.write('fragColor[{0}] = vec4(rior, opacity, 0.0, 0.0);'.format(mrt-1)) + else: + frag.write('fragColor[{0}] = vec4(1.0, 1.0, 0.0, 0.0);'.format(mrt-1)) + else: + frag.write('fragColor[0] = vec4(direct + indirect, 1.0);') + + if '_LDR' in wrd.world_defs: + frag.add_include('std/tonemap.glsl') + frag.write('fragColor[0].rgb = tonemapFilmic(fragColor[0].rgb);') + + # Particle opacity + if mat_state.material.arm_particle_flag and arm.utils.get_rp().arm_particles == 'On' and mat_state.material.arm_particle_fade: + frag.write('fragColor[0].rgb *= p_fade;') + +def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): + global is_displacement + wrd = bpy.data.worlds['Arm'] + + arm_discard = mat_state.material.arm_discard + make_base(con_mesh, parse_opacity=(parse_opacity or arm_discard)) + + blend = mat_state.material.arm_blending + + vert = con_mesh.vert + frag = con_mesh.frag + tese = con_mesh.tese + + if (parse_opacity or arm_discard) and not '_SSRefraction' in wrd.world_defs: + if arm_discard or blend: + opac = mat_state.material.arm_discard_opacity + frag.write('if (opacity < {0}) discard;'.format(opac)) + elif transluc_pass: + frag.write('if (opacity == 1.0) discard;') + else: + opac = '0.9999' # 1.0 - eps + frag.write('if (opacity < {0}) discard;'.format(opac)) + + if blend: + if parse_opacity: + frag.write('fragColor[0] = vec4(basecol, opacity);') + else: + #frag.write('fragColor[0] = vec4(basecol * lightCol * visibility, 1.0);') + frag.write('fragColor[0] = vec4(basecol, 1.0);') + # TODO: Fade out fragments near depth buffer here + return + + # Pack gbuffer + frag.add_include('std/gbuffer.glsl') + + frag.write_attrib('vec3 vVec = normalize(eyeDir);') + frag.write_attrib('float dotNV = max(dot(n, vVec), 0.0);') + + sh = tese if tese is not None else vert + sh.add_out('vec3 eyeDir') + sh.add_uniform('vec3 eye', '_cameraPosition') + sh.write('eyeDir = eye - wposition;') + + frag.add_include('std/light.glsl') + is_shadows = '_ShadowMap' in wrd.world_defs + is_shadows_atlas = '_ShadowMapAtlas' in wrd.world_defs + is_single_atlas = is_shadows_atlas and '_SingleAtlas' in wrd.world_defs + shadowmap_sun = 'shadowMap' + if is_shadows_atlas: + shadowmap_sun = 'shadowMapAtlasSun' if not is_single_atlas else 'shadowMapAtlas' + frag.add_uniform('vec2 smSizeUniform', '_shadowMapSize', included=True) + + frag.write('vec3 albedo = surfaceAlbedo(basecol, metallic);') + frag.write('vec3 f0 = surfaceF0(basecol, metallic);') + + if '_Brdf' in wrd.world_defs: + frag.add_uniform('sampler2D senvmapBrdf', link='$brdf.png') + frag.write('vec2 envBRDF = texelFetch(senvmapBrdf, ivec2(vec2(dotNV, 1.0 - roughness) * 256.0), 0).xy;') + + if '_Irr' in wrd.world_defs: + frag.add_include('std/shirr.glsl') + frag.add_uniform('vec4 shirr[7]', link='_envmapIrradiance') + frag.write('vec3 indirect = shIrradiance(n, shirr);') + if '_EnvTex' in wrd.world_defs: + frag.write('indirect /= PI;') + frag.write('indirect *= albedo;') + if '_Rad' in wrd.world_defs: + frag.add_uniform('sampler2D senvmapRadiance', link='_envmapRadiance') + frag.add_uniform('int envmapNumMipmaps', link='_envmapNumMipmaps') + frag.write('vec3 reflectionWorld = reflect(-vVec, n);') + frag.write('float lod = getMipFromRoughness(roughness, envmapNumMipmaps);') + frag.write('vec3 prefilteredColor = textureLod(senvmapRadiance, envMapEquirect(reflectionWorld), lod).rgb;') + if '_EnvLDR' in wrd.world_defs: + frag.write('prefilteredColor = pow(prefilteredColor, vec3(2.2));') + frag.write('indirect += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5;') + elif '_EnvCol' in wrd.world_defs: + frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') + frag.write('indirect += backgroundCol * f0;') + else: + frag.write('vec3 indirect = albedo;') + frag.write('indirect *= occlusion;') + + frag.add_uniform('float envmapStrength', link='_envmapStrength') + frag.write('indirect *= envmapStrength;') + + if '_VoxelAOvar' in wrd.world_defs: + frag.add_include('std/conetrace.glsl') + frag.add_uniform('sampler3D voxels') + if '_VoxelGICam' in wrd.world_defs: + frag.add_uniform('vec3 eyeSnap', link='_cameraPositionSnap') + frag.write('vec3 voxpos = (wposition - eyeSnap) / voxelgiHalfExtents;') + else: + frag.write('vec3 voxpos = wposition / voxelgiHalfExtents;') + frag.write('indirect *= vec3(1.0 - traceAO(voxpos, n, voxels));') + + frag.write('vec3 direct = vec3(0.0);') + + if '_Sun' in wrd.world_defs: + frag.add_uniform('vec3 sunCol', '_sunColor') + frag.add_uniform('vec3 sunDir', '_sunDirection') + frag.write('float svisibility = 1.0;') + frag.write('vec3 sh = normalize(vVec + sunDir);') + frag.write('float sdotNL = dot(n, sunDir);') + frag.write('float sdotNH = dot(n, sh);') + frag.write('float sdotVH = dot(vVec, sh);') + if is_shadows: + frag.add_uniform('bool receiveShadow') + frag.add_uniform(f'sampler2DShadow {shadowmap_sun}', top=True) + frag.add_uniform('float shadowsBias', '_sunShadowsBias') + frag.write('if (receiveShadow) {') + if '_CSM' in wrd.world_defs: + frag.add_include('std/shadows.glsl') + frag.add_uniform('vec4 casData[shadowmapCascades * 4 + 4]', '_cascadeData', included=True) + frag.add_uniform('vec3 eye', '_cameraPosition') + frag.write(f'svisibility = shadowTestCascade({shadowmap_sun}, eye, wposition + n * shadowsBias * 10, shadowsBias);') + else: + if tese is not None: + tese.add_out('vec4 lightPosition') + tese.add_uniform('mat4 LVP', '_biasLightViewProjectionMatrix') + tese.write('lightPosition = LVP * vec4(wposition, 1.0);') + else: + if is_displacement: + vert.add_out('vec4 lightPosition') + vert.add_uniform('mat4 LVP', '_biasLightViewProjectionMatrix') + vert.write('lightPosition = LVP * vec4(wposition, 1.0);') + else: + vert.add_out('vec4 lightPosition') + vert.add_uniform('mat4 LWVP', '_biasLightWorldViewProjectionMatrixSun') + vert.write('lightPosition = LWVP * spos;') + frag.write('vec3 lPos = lightPosition.xyz / lightPosition.w;') + frag.write('const vec2 smSize = shadowmapSize;') + frag.write(f'svisibility = PCF({shadowmap_sun}, lPos.xy, lPos.z - shadowsBias, smSize);') + frag.write('}') # receiveShadow + if '_VoxelShadow' in wrd.world_defs and '_VoxelAOvar' in wrd.world_defs: + frag.write('svisibility *= 1.0 - traceShadow(voxels, voxpos, sunDir);') + if '_VoxelGIShadow' in wrd.world_defs: + frag.write('svisibility *= 1.0 - traceShadow(voxels, voxpos, sunDir);') + frag.write('direct += (lambertDiffuseBRDF(albedo, sdotNL) + specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * specular) * sunCol * svisibility;') + # sun + + if '_SinglePoint' in wrd.world_defs: + frag.add_uniform('vec3 pointPos', link='_pointPosition') + frag.add_uniform('vec3 pointCol', link='_pointColor') + if '_Spot' in wrd.world_defs: + frag.add_uniform('vec3 spotDir', link='_spotDirection') + frag.add_uniform('vec3 spotRight', link='_spotRight') + frag.add_uniform('vec4 spotData', link='_spotData') + if is_shadows: + frag.add_uniform('bool receiveShadow') + frag.add_uniform('float pointBias', link='_pointShadowsBias') + if '_Spot' in wrd.world_defs: + # Skip world matrix, already in world-space + frag.add_uniform('mat4 LWVPSpot[1]', link='_biasLightViewProjectionMatrixSpotArray', included=True) + frag.add_uniform('sampler2DShadow shadowMapSpot[1]', included=True) + else: + frag.add_uniform('vec2 lightProj', link='_lightPlaneProj', included=True) + frag.add_uniform('samplerCubeShadow shadowMapPoint[1]', included=True) + frag.write('direct += sampleLight(') + frag.write(' wposition, n, vVec, dotNV, pointPos, pointCol, albedo, roughness, specular, f0') + if is_shadows: + frag.write(' , 0, pointBias, receiveShadow') + if '_Spot' in wrd.world_defs: + frag.write(' , true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight') + if '_VoxelShadow' in wrd.world_defs and '_VoxelAOvar' in wrd.world_defs: + frag.write(' , voxels, voxpos') + if '_MicroShadowing' in wrd.world_defs: + frag.write(' , occlusion') + frag.write(');') + + if '_Clusters' in wrd.world_defs: + make_cluster.write(vert, frag) + + if mat_state.emission_type != mat_state.EmissionType.NO_EMISSION: + if mat_state.emission_type == mat_state.EmissionType.SHADELESS: + frag.write('direct = vec3(0.0);') + frag.write('indirect += emissionCol;') + +def _write_material_attribs_default(frag: shader.Shader, parse_opacity: bool): + frag.write('vec3 basecol;') + frag.write('float roughness;') + frag.write('float metallic;') + frag.write('float occlusion;') + frag.write('float specular;') + # We may not use emission, but the attribute will then be removed + # by the shader compiler + frag.write('vec3 emissionCol;') + if parse_opacity: + frag.write('float opacity;') + frag.write('float rior = 1.450;')#case shader is arm we don't get an ior diff --git a/blender/arm/material/make_morph_target.py b/blender/arm/material/make_morph_target.py new file mode 100644 index 0000000000..5fdbd951a3 --- /dev/null +++ b/blender/arm/material/make_morph_target.py @@ -0,0 +1,28 @@ +import arm.utils + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +def morph_pos(vert): + rpdat = arm.utils.get_rp() + vert.add_include('compiled.inc') + vert.add_include('std/morph_target.glsl') + vert.add_uniform('sampler2D morphDataPos', link='_morphDataPos', included=True) + vert.add_uniform('sampler2D morphDataNor', link='_morphDataNor', included=True) + vert.add_uniform('vec4 morphWeights[8]', link='_morphWeights', included=True) + vert.add_uniform('vec2 morphScaleOffset', link='_morphScaleOffset', included=True) + vert.add_uniform('vec2 morphDataDim', link='_morphDataDim', included=True) + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write_attrib('vec2 texCoordMorph = morph * texUnpack;') + vert.write_attrib('spos.xyz *= posUnpack;') + vert.write_attrib('getMorphedVertex(texCoordMorph, spos.xyz);') + vert.write_attrib('spos.xyz /= posUnpack;') + +def morph_nor(vert, is_bone, prep): + vert.write_attrib('vec3 morphNor = vec3(0, 0, 0);') + vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);') + if not is_bone: + vert.write_attrib(prep + 'wnormal = normalize(N * morphNor);') diff --git a/blender/arm/material/make_overlay.py b/blender/arm/material/make_overlay.py new file mode 100644 index 0000000000..c244939f91 --- /dev/null +++ b/blender/arm/material/make_overlay.py @@ -0,0 +1,50 @@ +import arm +import arm.material.make_finalize as make_finalize +import arm.material.make_mesh as make_mesh +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils + +if arm.is_reload(__name__): + make_finalize = arm.reload_module(make_finalize) + make_mesh = arm.reload_module(make_mesh) + mat_state = arm.reload_module(mat_state) + mat_utils = arm.reload_module(mat_utils) +else: + arm.enable_reload(__name__) + + +def make(context_id): + con = { 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' } + mat = mat_state.material + blend = mat.arm_blending + if blend: + con['blend_source'] = mat.arm_blending_source + con['blend_destination'] = mat.arm_blending_destination + con['blend_operation'] = mat.arm_blending_operation + con['alpha_blend_source'] = mat.arm_blending_source_alpha + con['alpha_blend_destination'] = mat.arm_blending_destination_alpha + con['alpha_blend_operation'] = mat.arm_blending_operation_alpha + + con_overlay = mat_state.data.add_context(con) + + arm_discard = mat.arm_discard + is_transluc = mat_utils.is_transluc(mat) + parse_opacity = (blend and is_transluc) or arm_discard + make_mesh.make_base(con_overlay, parse_opacity=parse_opacity) + frag = con_overlay.frag + + if arm_discard: + opac = mat.arm_discard_opacity + frag.write('if (opacity < {0}) discard;'.format(opac)) + + frag.add_out('vec4 fragColor') + if blend and parse_opacity: + frag.write('fragColor = vec4(basecol + emissionCol, opacity);') + else: + frag.write('fragColor = vec4(basecol + emissionCol, 1.0);') + + frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));') + + make_finalize.make(con_overlay) + + return con_overlay diff --git a/blender/arm/material/make_particle.py b/blender/arm/material/make_particle.py new file mode 100644 index 0000000000..8c71e13645 --- /dev/null +++ b/blender/arm/material/make_particle.py @@ -0,0 +1,99 @@ +import arm.utils +import arm.material.mat_state as mat_state + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) + mat_state = arm.reload_module(mat_state) +else: + arm.enable_reload(__name__) + + +def write(vert, particle_info=None, shadowmap=False): + + # Outs + out_index = True if particle_info != None and particle_info['index'] else False + out_age = True if particle_info != None and particle_info['age'] else False + out_lifetime = True if particle_info != None and particle_info['lifetime'] else False + out_location = True if particle_info != None and particle_info['location'] else False + out_size = True if particle_info != None and particle_info['size'] else False + out_velocity = True if particle_info != None and particle_info['velocity'] else False + out_angular_velocity = True if particle_info != None and particle_info['angular_velocity'] else False + + vert.add_uniform('mat4 pd', '_particleData') + + str_tex_hash = "float fhash(float n) { return fract(sin(n) * 43758.5453); }\n" + vert.add_function(str_tex_hash) + + prep = 'float ' + if out_age: + prep = '' + vert.add_out('float p_age') + # var p_age = lapTime - p.i * spawnRate + vert.write(prep + 'p_age = pd[3][3] - gl_InstanceID * pd[0][1];') + # p_age -= p_age * fhash(i) * r.lifetime_random; + vert.write('p_age -= p_age * fhash(gl_InstanceID) * pd[2][3];') + + # Loop + # pd[0][0] - animtime, loop stored in sign + # vert.write('while (p_age < 0) p_age += pd[0][0];') + vert.write('if (pd[0][0] > 0 && p_age < 0) p_age += (int(-p_age / pd[0][0]) + 1) * pd[0][0];') + + # lifetime + prep = 'float ' + if out_lifetime: + prep = '' + vert.add_out('float p_lifetime') + vert.write(prep + 'p_lifetime = pd[0][2];') + # clip with nan + vert.write('if (p_age < 0 || p_age > p_lifetime) {') + vert.write(' gl_Position /= 0.0;') + vert.write(' return;') + vert.write('}') + + # vert.write('p_age /= 2;') # Match + + # object_align_factor / 2 + gxyz + prep = 'vec3 ' + if out_velocity: + prep = '' + vert.add_out('vec3 p_velocity') + vert.write(prep + 'p_velocity = vec3(pd[1][0], pd[1][1], pd[1][2]);') + + vert.write('p_velocity.x += fhash(gl_InstanceID) * pd[1][3] - pd[1][3] / 2;') + vert.write('p_velocity.y += fhash(gl_InstanceID + pd[0][3]) * pd[1][3] - pd[1][3] / 2;') + vert.write('p_velocity.z += fhash(gl_InstanceID + 2 * pd[0][3]) * pd[1][3] - pd[1][3] / 2;') + + # factor_random = pd[1][3] + # p.i = gl_InstanceID + # particles.length = pd[0][3] + + # gxyz + vert.write('p_velocity.x += (pd[2][0] * p_age) / 5;') + vert.write('p_velocity.y += (pd[2][1] * p_age) / 5;') + vert.write('p_velocity.z += (pd[2][2] * p_age) / 5;') + + prep = 'vec3 ' + if out_location: + prep = '' + vert.add_out('vec3 p_location') + vert.write(prep + 'p_location = p_velocity * p_age;') + + vert.write('spos.xyz += p_location;') + + # Particle fade + if mat_state.material.arm_particle_flag and arm.utils.get_rp().arm_particles == 'On' and mat_state.material.arm_particle_fade: + vert.add_out('float p_fade') + vert.write('p_fade = sin(min((p_age / 2) * 3.141592, 3.141592));') + + if out_index: + vert.add_out('float p_index'); + vert.write('p_index = gl_InstanceID;') + +def write_tilesheet(vert): + # tilesx, tilesy, framerate - pd[3][0], pd[3][1], pd[3][2] + vert.write('int frame = int((p_age) / pd[3][2]);') + vert.write('int tx = frame % int(pd[3][0]);') + vert.write('int ty = int(frame / pd[3][0]);') + vert.write('vec2 tilesheetOffset = vec2(tx * (1 / pd[3][0]), ty * (1 / pd[3][1]));') + vert.write('texCoord = tex * texUnpack + tilesheetOffset;') + # vert.write('texCoord = tex;') diff --git a/blender/arm/material/make_shader.py b/blender/arm/material/make_shader.py new file mode 100644 index 0000000000..115b3b0127 --- /dev/null +++ b/blender/arm/material/make_shader.py @@ -0,0 +1,221 @@ +import os +import subprocess +from typing import Dict, List, Tuple + +import bpy +from bpy.types import Material +from bpy.types import Object + +import arm.api +import arm.assets as assets +import arm.exporter +import arm.log as log +import arm.material.cycles as cycles +import arm.material.make_decal as make_decal +import arm.material.make_depth as make_depth +import arm.material.make_mesh as make_mesh +import arm.material.make_overlay as make_overlay +import arm.material.make_transluc as make_transluc +import arm.material.make_voxel as make_voxel +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils +from arm.material.shader import Shader, ShaderContext, ShaderData +import arm.utils + +if arm.is_reload(__name__): + arm.api = arm.reload_module(arm.api) + assets = arm.reload_module(assets) + arm.exporter = arm.reload_module(arm.exporter) + log = arm.reload_module(log) + cycles = arm.reload_module(cycles) + make_decal = arm.reload_module(make_decal) + make_depth = arm.reload_module(make_depth) + make_mesh = arm.reload_module(make_mesh) + make_overlay = arm.reload_module(make_overlay) + make_transluc = arm.reload_module(make_transluc) + make_voxel = arm.reload_module(make_voxel) + mat_state = arm.reload_module(mat_state) + mat_utils = arm.reload_module(mat_utils) + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader, ShaderContext, ShaderData + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +rpass_hook = None + + +def build(material: Material, mat_users: Dict[Material, List[Object]], mat_armusers) -> Tuple: + mat_state.mat_users = mat_users + mat_state.mat_armusers = mat_armusers + mat_state.material = material + mat_state.nodes = material.node_tree.nodes + mat_state.data = ShaderData(material) + mat_state.output_node = cycles.node_by_type(mat_state.nodes, 'OUTPUT_MATERIAL') + if mat_state.output_node is None: + # Place empty material output to keep compiler happy.. + mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial') + + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + rpasses = mat_utils.get_rpasses(material) + matname = arm.utils.safesrc(arm.utils.asset_name(material)) + rel_path = arm.utils.build_dir() + '/compiled/Shaders/' + full_path = arm.utils.get_fp() + '/' + rel_path + if not os.path.exists(full_path): + os.makedirs(full_path) + + make_instancing_and_skinning(material, mat_users) + + bind_constants = dict() + bind_textures = dict() + + for rp in rpasses: + car = [] + bind_constants[rp] = car + mat_state.bind_constants = car + tar = [] + bind_textures[rp] = tar + mat_state.bind_textures = tar + + con = None + + if rpdat.rp_driver != 'Armory' and arm.api.drivers[rpdat.rp_driver]['make_rpass'] is not None: + con = arm.api.drivers[rpdat.rp_driver]['make_rpass'](rp) + + if con is not None: + pass + + elif rp == 'mesh': + con = make_mesh.make(rp, rpasses) + + elif rp == 'shadowmap': + con = make_depth.make(rp, rpasses, shadowmap=True) + + elif rp == 'translucent': + con = make_transluc.make(rp) + + elif rp == 'refraction': + con = make_mesh.make(rp, rpasses) + + elif rp == 'overlay': + con = make_overlay.make(rp) + + elif rp == 'decal': + con = make_decal.make(rp) + + elif rp == 'depth': + con = make_depth.make(rp, rpasses) + + elif rp == 'voxel': + con = make_voxel.make(rp) + + elif rpass_hook is not None: + con = rpass_hook(rp) + + write_shaders(rel_path, con, rp, matname) + + shader_data_name = matname + '_data' + + if wrd.arm_single_data_file: + if 'shader_datas' not in arm.exporter.current_output: + arm.exporter.current_output['shader_datas'] = [] + arm.exporter.current_output['shader_datas'].append(mat_state.data.get()['shader_datas'][0]) + else: + arm.utils.write_arm(full_path + '/' + matname + '_data.arm', mat_state.data.get()) + shader_data_path = arm.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.arm' + assets.add_shader_data(shader_data_path) + + return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures + + +def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None: + keep_cache = mat_state.material.arm_cached + write_shader(rel_path, con.vert, 'vert', rpass, matname, keep_cache=keep_cache) + write_shader(rel_path, con.frag, 'frag', rpass, matname, keep_cache=keep_cache) + write_shader(rel_path, con.geom, 'geom', rpass, matname, keep_cache=keep_cache) + write_shader(rel_path, con.tesc, 'tesc', rpass, matname, keep_cache=keep_cache) + write_shader(rel_path, con.tese, 'tese', rpass, matname, keep_cache=keep_cache) + + +def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: str, keep_cache=True) -> None: + if shader is None or shader.is_linked: + return + + # TODO: blend context + if rpass == 'mesh' and mat_state.material.arm_blending: + rpass = 'blend' + + file_ext = '.glsl' + if shader.noprocessing: + # Use hlsl directly + hlsl_dir = arm.utils.build_dir() + '/compiled/Hlsl/' + if not os.path.exists(hlsl_dir): + os.makedirs(hlsl_dir) + file_ext = '.hlsl' + rel_path = rel_path.replace('/compiled/Shaders/', '/compiled/Hlsl/') + + shader_file = matname + '_' + rpass + '.' + ext + file_ext + shader_path = arm.utils.get_fp() + '/' + rel_path + '/' + shader_file + assets.add_shader(shader_path) + if not os.path.isfile(shader_path) or not keep_cache: + with open(shader_path, 'w') as f: + f.write(shader.get()) + + if shader.noprocessing: + cwd = os.getcwd() + os.chdir(arm.utils.get_fp() + '/' + rel_path) + hlslbin_path = arm.utils.get_sdk_path() + '/lib/armory_tools/hlslbin/hlslbin.exe' + prof = 'vs_5_0' if ext == 'vert' else 'ps_5_0' if ext == 'frag' else 'gs_5_0' + # noprocessing flag - gets renamed to .d3d11 + args = [hlslbin_path.replace('/', '\\').replace('\\\\', '\\'), shader_file, shader_file[:-4] + 'glsl', prof] + if ext == 'vert': + args.append('-i') + args.append('pos') + proc = subprocess.call(args) + os.chdir(cwd) + + +def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[Object]]) -> None: + """Build material with instancing or skinning if enabled. + If the material is a custom material, only validation checks for instancing are performed.""" + global_elems = [] + if mat_users is not None and mat in mat_users: + # Whether there are both an instanced object and a not instanced object with this material + instancing_usage = [False, False] + mat_state.uses_instancing = False + + for bo in mat_users[mat]: + if mat.arm_custom_material == '': + # Morph Targets + if arm.utils.export_morph_targets(bo): + global_elems.append({'name': 'morph', 'data': 'short2norm'}) + # GPU Skinning + if arm.utils.export_bone_data(bo): + global_elems.append({'name': 'bone', 'data': 'short4norm'}) + global_elems.append({'name': 'weight', 'data': 'short4norm'}) + + # Instancing + inst = bo.arm_instanced + if inst != 'Off' or mat.arm_particle_flag: + instancing_usage[0] = True + mat_state.uses_instancing = True + + if mat.arm_custom_material == '': + global_elems.append({'name': 'ipos', 'data': 'float3'}) + if 'Rot' in inst: + global_elems.append({'name': 'irot', 'data': 'float3'}) + if 'Scale' in inst: + global_elems.append({'name': 'iscl', 'data': 'float3'}) + + elif inst == 'Off': + # Ignore children of instanced objects, they are instanced even when set to 'Off' + instancing_usage[1] = bo.parent is None or bo.parent.arm_instanced == 'Off' + + if instancing_usage[0] and instancing_usage[1]: + # Display a warning for invalid instancing configurations + # See https://github.com/armory3d/armory/issues/2072 + log.warn(f'Material "{mat.name}" has both instanced and not instanced objects, objects might flicker!') + + if mat.arm_custom_material == '': + mat_state.data.global_elems = global_elems diff --git a/blender/arm/material/make_skin.py b/blender/arm/material/make_skin.py new file mode 100644 index 0000000000..9df79c852c --- /dev/null +++ b/blender/arm/material/make_skin.py @@ -0,0 +1,30 @@ +import arm.utils + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def skin_pos(vert): + vert.add_include('compiled.inc') + + rpdat = arm.utils.get_rp() + vert.add_include('std/skinning.glsl') + vert.add_uniform('vec4 skinBones[skinMaxBones * 2]', link='_skinBones', included=True) + vert.add_uniform('float posUnpack', link='_posUnpack') + vert.write_attrib('vec4 skinA;') + vert.write_attrib('vec4 skinB;') + vert.write_attrib('getSkinningDualQuat(ivec4(bone * 32767), weight, skinA, skinB);') + vert.write_attrib('spos.xyz *= posUnpack;') + vert.write_attrib('spos.xyz += 2.0 * cross(skinA.xyz, cross(skinA.xyz, spos.xyz) + skinA.w * spos.xyz); // Rotate') + vert.write_attrib('spos.xyz += 2.0 * (skinA.w * skinB.xyz - skinB.w * skinA.xyz + cross(skinA.xyz, skinB.xyz)); // Translate') + vert.write_attrib('spos.xyz /= posUnpack;') + + +def skin_nor(vert, is_morph, prep): + rpdat = arm.utils.get_rp() + if(is_morph): + vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));') + else: + vert.write_attrib(prep + 'wnormal = normalize(N * (vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz, cross(skinA.xyz, vec3(nor.xy, pos.w)) + skinA.w * vec3(nor.xy, pos.w))));') diff --git a/blender/arm/material/make_tess.py b/blender/arm/material/make_tess.py new file mode 100644 index 0000000000..ade6e3a5b5 --- /dev/null +++ b/blender/arm/material/make_tess.py @@ -0,0 +1,32 @@ + +def tesc_levels(tesc, innerLevel, outerLevel): + tesc.write('if (gl_InvocationID == 0) {') + tesc.write(' gl_TessLevelInner[0] = {0}; // inner level'.format(innerLevel)) + tesc.write(' gl_TessLevelInner[1] = {0};'.format(innerLevel)) + tesc.write(' gl_TessLevelOuter[0] = {0}; // outer level'.format(outerLevel)) + tesc.write(' gl_TessLevelOuter[1] = {0};'.format(outerLevel)) + tesc.write(' gl_TessLevelOuter[2] = {0};'.format(outerLevel)) + tesc.write(' gl_TessLevelOuter[3] = {0};'.format(outerLevel)) + tesc.write('}') + +def interpolate(tese, var, size, normalize=False, declare_out=False): + tese.add_include('compiled.inc') + vec = 'vec{0}'.format(size) + if declare_out: + tese.add_out('{0} {1}'.format(vec, var)) + + s = '{0} {1}_0 = gl_TessCoord.x * tc_{1}[0];\n'.format(vec, var) + s += '{0} {1}_1 = gl_TessCoord.y * tc_{1}[1];\n'.format(vec, var) + s += '{0} {1}_2 = gl_TessCoord.z * tc_{1}[2];\n'.format(vec, var) + + prep = '' + if not declare_out: + prep = vec + ' ' + + if normalize: + s += '{0}{1} = normalize({1}_0 + {1}_1 + {1}_2);\n'.format(prep, var) + s += 'vec3 n = {0};\n'.format(var) + else: + s += '{0}{1} = {1}_0 + {1}_1 + {1}_2;\n'.format(prep, var) + + tese.write_attrib(s) diff --git a/blender/arm/material/make_transluc.py b/blender/arm/material/make_transluc.py new file mode 100644 index 0000000000..010c89a3e4 --- /dev/null +++ b/blender/arm/material/make_transluc.py @@ -0,0 +1,51 @@ +import bpy + +import arm +import arm.material.cycles as cycles +import arm.material.mat_state as mat_state +import arm.material.make_mesh as make_mesh +import arm.material.make_finalize as make_finalize +import arm.assets as assets + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + mat_state = arm.reload_module(mat_state) + make_mesh = arm.reload_module(make_mesh) + make_finalize = arm.reload_module(make_finalize) + assets = arm.reload_module(assets) +else: + arm.enable_reload(__name__) + + +def make(context_id): + con_transluc = mat_state.data.add_context({ 'name': context_id, 'depth_write': False, 'compare_mode': 'less', 'cull_mode': 'clockwise', \ + 'blend_source': 'blend_one', 'blend_destination': 'blend_one', 'blend_operation': 'add', \ + 'alpha_blend_source': 'blend_zero', 'alpha_blend_destination': 'inverse_source_alpha', 'alpha_blend_operation': 'add' }) + + make_mesh.make_forward_base(con_transluc, parse_opacity=True, transluc_pass=True) + + vert = con_transluc.vert + frag = con_transluc.frag + tese = con_transluc.tese + + frag.add_out('vec4 fragColor[2]') + + # Remove fragColor = ...; + frag.main = frag.main[:frag.main.rfind('fragColor')] + frag.write('\n') + + wrd = bpy.data.worlds['Arm'] + if '_VoxelAOvar' in wrd.world_defs: + frag.write('indirect *= 0.25;') + frag.write('vec4 premultipliedReflect = vec4(vec3(direct + indirect * 0.5) * opacity, opacity);') + + frag.write('float w = clamp(pow(min(1.0, premultipliedReflect.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - (gl_FragCoord.z) * 0.9, 3.0), 1e-2, 3e3);') + frag.write('fragColor[0] = vec4(premultipliedReflect.rgb * w, premultipliedReflect.a);') + frag.write('fragColor[1] = vec4(premultipliedReflect.a * w, 0.0, 0.0, 1.0);') + + make_finalize.make(con_transluc) + + # assets.vs_equal(con_transluc, assets.shader_cons['transluc_vert']) # shader_cons has no transluc yet + # assets.fs_equal(con_transluc, assets.shader_cons['transluc_frag']) + + return con_transluc diff --git a/blender/arm/material/make_voxel.py b/blender/arm/material/make_voxel.py new file mode 100644 index 0000000000..74e9276e52 --- /dev/null +++ b/blender/arm/material/make_voxel.py @@ -0,0 +1,144 @@ +import bpy +import arm.utils +import arm.assets as assets +import arm.material.cycles as cycles +import arm.material.mat_state as mat_state +import arm.material.mat_utils as mat_utils +import arm.material.make_particle as make_particle +import arm.make_state as state + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) + assets = arm.reload_module(assets) + mat_state = arm.reload_module(mat_state) +else: + arm.enable_reload(__name__) + +def make(context_id): + rpdat = arm.utils.get_rp() + con = make_ao(context_id) + + assets.vs_equal(con, assets.shader_cons['voxel_vert']) + assets.fs_equal(con, assets.shader_cons['voxel_frag']) + assets.gs_equal(con, assets.shader_cons['voxel_geom']) + + return con + +def make_ao(context_id): + con_voxel = mat_state.data.add_context({ 'name': context_id, 'depth_write': False, 'compare_mode': 'always', 'cull_mode': 'none', 'color_writes_red': [False], 'color_writes_green': [False], 'color_writes_blue': [False], 'color_write_alpha': [False], 'conservative_raster': False }) + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + vert = con_voxel.make_vert() + frag = con_voxel.make_frag() + geom = con_voxel.make_geom() + tesc = None + tese = None + + if arm.utils.get_gapi() == 'direct3d11': + for e in con_voxel.data['vertex_elements']: + if e['name'] == 'nor': + con_voxel.data['vertex_elements'].remove(e) + break + + # No geom shader compiler for hlsl yet + vert.noprocessing = True + frag.noprocessing = True + geom.noprocessing = True + + vert.add_uniform('mat4 W', '_worldMatrix') + vert.write('uniform float4x4 W;') + if rpdat.arm_voxelgi_revoxelize and rpdat.arm_voxelgi_camera: + vert.add_uniform('vec3 eyeSnap', '_cameraPositionSnap') + vert.write('uniform float3 eyeSnap;') + vert.write('struct SPIRV_Cross_Input { float4 pos : TEXCOORD0; };') + vert.write('struct SPIRV_Cross_Output { float4 svpos : SV_POSITION; };') + vert.write('SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) {') + vert.write(' SPIRV_Cross_Output stage_output;') + voxHalfExt = str(round(rpdat.arm_voxelgi_dimensions / 2.0)) + if rpdat.arm_voxelgi_revoxelize and rpdat.arm_voxelgi_camera: + vert.write(' stage_output.svpos.xyz = (mul(float4(stage_input.pos.xyz, 1.0), W).xyz - eyeSnap) / float3(' + voxHalfExt + ', ' + voxHalfExt + ', ' + voxHalfExt + ');') + else: + vert.write(' stage_output.svpos.xyz = mul(float4(stage_input.pos.xyz, 1.0), W).xyz / float3(' + voxHalfExt + ', ' + voxHalfExt + ', ' + voxHalfExt + ');') + vert.write(' stage_output.svpos.w = 1.0;') + vert.write(' return stage_output;') + vert.write('}') + + geom.write('struct SPIRV_Cross_Input { float4 svpos : SV_POSITION; };') + geom.write('struct SPIRV_Cross_Output { float3 wpos : TEXCOORD0; float4 svpos : SV_POSITION; };') + geom.write('[maxvertexcount(3)]') + geom.write('void main(triangle SPIRV_Cross_Input stage_input[3], inout TriangleStream output) {') + geom.write(' float3 p1 = stage_input[1].svpos.xyz - stage_input[0].svpos.xyz;') + geom.write(' float3 p2 = stage_input[2].svpos.xyz - stage_input[0].svpos.xyz;') + geom.write(' float3 p = abs(cross(p1, p2));') + geom.write(' for (int i = 0; i < 3; ++i) {') + geom.write(' SPIRV_Cross_Output stage_output;') + geom.write(' stage_output.wpos = stage_input[i].svpos.xyz;') + geom.write(' if (p.z > p.x && p.z > p.y) {') + geom.write(' stage_output.svpos = float4(stage_input[i].svpos.x, stage_input[i].svpos.y, 0.0, 1.0);') + geom.write(' }') + geom.write(' else if (p.x > p.y && p.x > p.z) {') + geom.write(' stage_output.svpos = float4(stage_input[i].svpos.y, stage_input[i].svpos.z, 0.0, 1.0);') + geom.write(' }') + geom.write(' else {') + geom.write(' stage_output.svpos = float4(stage_input[i].svpos.x, stage_input[i].svpos.z, 0.0, 1.0);') + geom.write(' }') + geom.write(' output.Append(stage_output);') + geom.write(' }') + geom.write('}') + + frag.add_uniform('layout(r8) writeonly image3D voxels') + frag.write('RWTexture3D voxels;') + frag.write('struct SPIRV_Cross_Input { float3 wpos : TEXCOORD0; };') + frag.write('struct SPIRV_Cross_Output { float4 FragColor : SV_TARGET0; };') + frag.write('void main(SPIRV_Cross_Input stage_input) {') + frag.write(' if (abs(stage_input.wpos.z) > ' + rpdat.rp_voxelgi_resolution_z + ' || abs(stage_input.wpos.x) > 1 || abs(stage_input.wpos.y) > 1) return;') + voxRes = str(rpdat.rp_voxelgi_resolution) + voxResZ = str(int(int(rpdat.rp_voxelgi_resolution) * float(rpdat.rp_voxelgi_resolution_z))) + frag.write(' voxels[int3(' + voxRes + ', ' + voxRes + ', ' + voxResZ + ') * (stage_input.wpos * 0.5 + 0.5)] = 1.0;') + frag.write('') + frag.write('}') + else: + geom.ins = vert.outs + frag.ins = geom.outs + + frag.add_include('compiled.inc') + frag.add_include('std/math.glsl') + frag.add_include('std/imageatomic.glsl') + frag.write_header('#extension GL_ARB_shader_image_load_store : enable') + + frag.add_uniform('layout(r8) writeonly image3D voxels') + + vert.add_include('compiled.inc') + vert.add_uniform('mat4 W', '_worldMatrix') + vert.add_out('vec3 voxpositionGeom') + + if rpdat.arm_voxelgi_revoxelize and rpdat.arm_voxelgi_camera: + vert.add_uniform('vec3 eyeSnap', '_cameraPositionSnap') + vert.write('voxpositionGeom = (vec3(W * vec4(pos.xyz, 1.0)) - eyeSnap) / voxelgiHalfExtents;') + else: + vert.write('voxpositionGeom = vec3(W * vec4(pos.xyz, 1.0)) / voxelgiHalfExtents;') + + geom.add_out('vec3 voxposition') + geom.write('vec3 p1 = voxpositionGeom[1] - voxpositionGeom[0];') + geom.write('vec3 p2 = voxpositionGeom[2] - voxpositionGeom[0];') + geom.write('vec3 p = abs(cross(p1, p2));') + geom.write('for (uint i = 0; i < 3; ++i) {') + geom.write(' voxposition = voxpositionGeom[i];') + geom.write(' if (p.z > p.x && p.z > p.y) {') + geom.write(' gl_Position = vec4(voxposition.x, voxposition.y, 0.0, 1.0);') + geom.write(' }') + geom.write(' else if (p.x > p.y && p.x > p.z) {') + geom.write(' gl_Position = vec4(voxposition.y, voxposition.z, 0.0, 1.0);') + geom.write(' }') + geom.write(' else {') + geom.write(' gl_Position = vec4(voxposition.x, voxposition.z, 0.0, 1.0);') + geom.write(' }') + geom.write(' EmitVertex();') + geom.write('}') + geom.write('EndPrimitive();') + + frag.write('if (abs(voxposition.z) > ' + rpdat.rp_voxelgi_resolution_z + ' || abs(voxposition.x) > 1 || abs(voxposition.y) > 1) return;') + frag.write('imageStore(voxels, ivec3(voxelgiResolution * (voxposition * 0.5 + 0.5)), vec4(1.0));') + + return con_voxel diff --git a/blender/arm/material/mat_batch.py b/blender/arm/material/mat_batch.py new file mode 100644 index 0000000000..3d2746c31b --- /dev/null +++ b/blender/arm/material/mat_batch.py @@ -0,0 +1,139 @@ +import arm +import arm.material.cycles as cycles +import arm.material.make_shader as make_shader +import arm.material.mat_state as mat_state + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + make_shader = arm.reload_module(make_shader) + mat_state = arm.reload_module(mat_state) +else: + arm.enable_reload(__name__) + +# TODO: handle groups +# TODO: handle cached shaders + +batchDict = None +signatureDict = None + +def traverse_tree(node, sign): + sign += node.type + '-' + for inp in node.inputs: + if inp.is_linked: + sign = traverse_tree(inp.links[0].from_node, sign) + else: + sign += 'o' # Unconnected socket + return sign + +def get_signature(mat): + nodes = mat.node_tree.nodes + output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') + + if output_node != None: + sign = traverse_tree(output_node, '') + # Append flags + sign += '1' if mat.arm_cast_shadow else '0' + sign += '1' if mat.arm_ignore_irradiance else '0' + if mat.arm_two_sided: + sign += '2' + elif mat.arm_cull_mode == 'Clockwise': + sign += '1' + else: + sign += '0' + sign += str(mat.arm_material_id) + sign += '1' if mat.arm_depth_read else '0' + sign += '1' if mat.arm_overlay else '0' + sign += '1' if mat.arm_decal else '0' + if mat.arm_discard: + sign += '1' + sign += str(round(mat.arm_discard_opacity, 2)) + sign += str(round(mat.arm_discard_opacity_shadows, 2)) + else: + sign += '000' + sign += mat.arm_custom_material if mat.arm_custom_material != '' else '0' + sign += mat.arm_skip_context if mat.arm_skip_context != '' else '0' + sign += '1' if mat.arm_particle_fade else '0' + sign += mat.arm_billboard + return sign + +def traverse_tree2(node, ar): + ar.append(node) + for inp in node.inputs: + inp.is_uniform = False + if inp.is_linked: + traverse_tree2(inp.links[0].from_node, ar) + +def get_sorted(mat): + nodes = mat.node_tree.nodes + output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') + + if output_node != None: + ar = [] + traverse_tree2(output_node, ar) + return ar + +def mark_uniforms(mats): + ars = [] + for m in mats: + ars.append(get_sorted(m)) + + # Buckle up.. + for i in range(0, len(ars[0])): # Traverse nodes + for j in range(0, len(ars[0][i].inputs)): # Traverse inputs + inp = ars[0][i].inputs[j] + if not inp.is_linked and hasattr(inp, 'default_value'): + for k in range(1, len(ars)): # Compare default values + inp2 = ars[k][i].inputs[j] + diff = False + if str(type(inp.default_value)) == "": + for l in range(0, len(inp.default_value)): + if inp.default_value[l] != inp2.default_value[l]: + diff = True + break + elif inp.default_value != inp2.default_value: + diff = True + if diff: # Diff found + for ar in ars: + ar[i].inputs[j].is_uniform = True + break + +def build(materialArray, mat_users, mat_armusers): + global batchDict + batchDict = dict() # Stores shader data for given material + signatureDict = dict() # Stores materials for given signature + + # Update signatures + for mat in materialArray: + if mat.signature == '' or not mat.arm_cached: + mat.signature = get_signature(mat) + # Group signatures + if mat.signature in signatureDict: + signatureDict[mat.signature].append(mat) + else: + signatureDict[mat.signature] = [mat] + + # Mark different inputs + for ref in signatureDict: + mats = signatureDict[ref] + if len(mats) > 1: + mark_uniforms(mats) + + mat_state.batch = True + + # Build unique shaders + for mat in materialArray: + for mat2 in materialArray: + # Signature not found - build it + if mat == mat2: + batchDict[mat] = make_shader.build(mat, mat_users, mat_armusers) + break + + # Already batched + if mat.signature == mat2.signature: + batchDict[mat] = batchDict[mat2] + break + + mat_state.batch = False + +def get(mat): + return batchDict[mat] diff --git a/blender/arm/material/mat_state.py b/blender/arm/material/mat_state.py new file mode 100644 index 0000000000..c78d3675ef --- /dev/null +++ b/blender/arm/material/mat_state.py @@ -0,0 +1,40 @@ +from enum import IntEnum + + +class EmissionType(IntEnum): + NO_EMISSION = 0 + """The material has no emission at all.""" + + SHADELESS = 1 + """The material is emissive and does not interact with lights/shadows.""" + + SHADED = 2 + """The material is emissive and interacts with lights/shadows.""" + + @staticmethod + def get_effective_combination(a: 'EmissionType', b: 'EmissionType') -> 'EmissionType': + # Shaded emission always has precedence over shadeless emission + if a == EmissionType.SHADED or b == EmissionType.SHADED: + return EmissionType.SHADED + + if a == EmissionType.SHADELESS and b == EmissionType.SHADELESS: + return EmissionType.SHADELESS + + # If only one input is shadeless we still need shaded emission + if a == EmissionType.SHADELESS or b == EmissionType.SHADELESS: + return EmissionType.SHADED + + return EmissionType.NO_EMISSION + + +data = None # ShaderData +material = None +nodes = None +mat_users = None +bind_constants = None # Merged with mat_context bind constants +bind_textures = None # Merged with mat_context bind textures +batch = False +texture_grad = False # Sample textures using textureGrad() +con_mesh = None # Mesh context +uses_instancing = False # Whether the current material has at least one user with instancing enabled +emission_type = EmissionType.NO_EMISSION diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py new file mode 100644 index 0000000000..0f92412987 --- /dev/null +++ b/blender/arm/material/mat_utils.py @@ -0,0 +1,107 @@ +from typing import Generator + +import bpy + +import arm.utils +import arm.make_state as make_state +import arm.material.cycles as cycles +import arm.log as log + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) + make_state = arm.reload_module(make_state) + cycles = arm.reload_module(cycles) + log = arm.reload_module(log) +else: + arm.enable_reload(__name__) + +add_mesh_contexts = [] + +def disp_linked(output_node): + linked = output_node.inputs[2].is_linked + if not linked: + return False + # Armory PBR with unlinked height socket + l = output_node.inputs[2].links[0] + if l.from_node.type == 'GROUP' and l.from_node.node_tree.name.startswith('Armory PBR') and \ + l.from_node.inputs[7].is_linked == False: + return False + disp_enabled = arm.utils.disp_enabled(make_state.target) + rpdat = arm.utils.get_rp() + if not disp_enabled and rpdat.arm_rp_displacement == 'Tessellation': + log.warn('Tessellation not available on ' + make_state.target) + return disp_enabled + +def get_rpasses(material): + ar = [] + rpdat = arm.utils.get_rp() + has_voxels = arm.utils.voxel_support() + wrd = bpy.data.worlds['Arm'] + + if material.arm_decal: + ar.append('decal') + elif material.arm_overlay: + ar.append('overlay') + elif is_transluc(material) and not material.arm_discard and not material.arm_blending and rpdat.rp_ss_refraction: + ar.append('refraction') + else: + ar.append('mesh') + for con in add_mesh_contexts: + ar.append(con) + if is_transluc(material) and not material.arm_discard and rpdat.rp_translucency_state != 'Off' and not material.arm_blending: + ar.append('translucent') + if rpdat.rp_voxels and has_voxels: + ar.append('voxel') + if rpdat.rp_renderer == 'Forward' and rpdat.rp_depthprepass and not material.arm_blending and not material.arm_particle_flag: + ar.append('depth') + + if material.arm_cast_shadow and rpdat.rp_shadows and ('mesh' in ar): + ar.append('shadowmap') + return ar + +def is_transluc(material): + nodes = material.node_tree.nodes + output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') + if output_node == None or output_node.inputs[0].is_linked == False: + return False + + surface_node = output_node.inputs[0].links[0].from_node + return is_transluc_traverse(surface_node) + +def is_transluc_traverse(node): + # TODO: traverse groups + if is_transluc_type(node): + return True + for inp in node.inputs: + if inp.is_linked: + res = is_transluc_traverse(inp.links[0].from_node) + if res: + return True + return False + + +def is_transluc_type(node: bpy.types.ShaderNode) -> bool: + return node.type in ('BSDF_GLASS', 'BSDF_TRANSPARENT', 'BSDF_TRANSLUCENT') \ + or (is_armory_pbr_node(node) and (node.inputs['Opacity'].is_linked or node.inputs['Opacity'].default_value != 1.0)) \ + or (node.type == 'BSDF_PRINCIPLED' and (node.inputs['Alpha'].is_linked or node.inputs['Alpha'].default_value != 1.0)) + + +def is_armory_pbr_node(node: bpy.types.ShaderNode) -> bool: + return node.type == 'GROUP' and node.node_tree.name.startswith('Armory PBR') + + +def iter_nodes_armorypbr(node_group: bpy.types.NodeTree) -> Generator[bpy.types.Node, None, None]: + for node in node_group.nodes: + if is_armory_pbr_node(node): + yield node + + +def equals_color_socket(socket: bpy.types.NodeSocketColor, value: tuple[float, ...], *, comp_alpha=True) -> bool: + # NodeSocketColor.default_value is of bpy_prop_array type that doesn't + # support direct comparison + return ( + socket.default_value[0] == value[0] + and socket.default_value[1] == value[1] + and socket.default_value[2] == value[2] + and (socket.default_value[3] == value[3] if comp_alpha else True) + ) diff --git a/blender/arm/material/node_meta.py b/blender/arm/material/node_meta.py new file mode 100644 index 0000000000..4860df4f62 --- /dev/null +++ b/blender/arm/material/node_meta.py @@ -0,0 +1,209 @@ +""" +This module contains a list of all material nodes that Armory supports +(excluding output nodes), as well as Armory-related metadata. +""" +from enum import IntEnum, unique +from dataclasses import dataclass +from typing import Any, Callable, Optional + +import bpy + +import arm.material.arm_nodes.shader_data_node as shader_data_node +import arm.material.cycles_nodes.nodes_color as nodes_color +import arm.material.cycles_nodes.nodes_converter as nodes_converter +import arm.material.cycles_nodes.nodes_input as nodes_input +import arm.material.cycles_nodes.nodes_shader as nodes_shader +import arm.material.cycles_nodes.nodes_texture as nodes_texture +import arm.material.cycles_nodes.nodes_vector as nodes_vector +import arm.material.parser_state + +if arm.is_reload(__name__): + shader_data_node = arm.reload_module(shader_data_node) + nodes_color = arm.reload_module(nodes_color) + nodes_converter = arm.reload_module(nodes_converter) + nodes_input = arm.reload_module(nodes_input) + nodes_shader = arm.reload_module(nodes_shader) + nodes_texture = arm.reload_module(nodes_texture) + nodes_vector = arm.reload_module(nodes_vector) + arm.material.parser_state = arm.reload_module(arm.material.parser_state) +else: + arm.enable_reload(__name__) + + +@unique +class ComputeDXDYVariant(IntEnum): + ALWAYS = 0 + """Always compute dx/dy variants of the corresponding node. + Use this for input nodes that represent leafs of the node graph + if some of their output values vary between fragments. + """ + + NEVER = 1 + """Never compute dx/dy variants of the corresponding node. + Use this for nodes whose output values do not change with respect + to fragment positions. + """ + + DYNAMIC = 2 + """Compute dx/dy variants if any input socket of the corresponding node + is connected to a node that requires dx/dy variants. + """ + + +@dataclass +class MaterialNodeMeta: + # Use Any here due to contravariance + parse_func: Callable[[Any, bpy.types.NodeSocket, arm.material.parser_state.ParserState], Optional[str]] + """The function used to parse this node and to translate it to GLSL output code.""" + + compute_dxdy_variants: ComputeDXDYVariant = ComputeDXDYVariant.DYNAMIC + """Specifies when this node should compute dx/dy variants + if the ParserState is in the dx/dy offset pass. + """ + + +ALL_NODES: dict[str, MaterialNodeMeta] = { + # --- nodes_color + 'BRIGHTCONTRAST': MaterialNodeMeta(parse_func=nodes_color.parse_brightcontrast), + 'CURVE_RGB': MaterialNodeMeta(parse_func=nodes_color.parse_curvergb), + 'GAMMA': MaterialNodeMeta(parse_func=nodes_color.parse_gamma), + 'HUE_SAT': MaterialNodeMeta(parse_func=nodes_color.parse_huesat), + 'INVERT': MaterialNodeMeta(parse_func=nodes_color.parse_invert), + 'LIGHT_FALLOFF': MaterialNodeMeta(parse_func=nodes_color.parse_lightfalloff), + 'MIX_RGB': MaterialNodeMeta(parse_func=nodes_color.parse_mixrgb), + + # --- nodes_converter + 'BLACKBODY': MaterialNodeMeta(parse_func=nodes_converter.parse_blackbody), + 'CLAMP': MaterialNodeMeta(parse_func=nodes_converter.parse_clamp), + 'COMBHSV': MaterialNodeMeta(parse_func=nodes_converter.parse_combhsv), + 'COMBINE_COLOR': MaterialNodeMeta(parse_func=nodes_converter.parse_combine_color), + 'COMBRGB': MaterialNodeMeta(parse_func=nodes_converter.parse_combrgb), + 'COMBXYZ': MaterialNodeMeta(parse_func=nodes_converter.parse_combxyz), + 'MAP_RANGE': MaterialNodeMeta(parse_func=nodes_converter.parse_maprange), + 'MATH': MaterialNodeMeta(parse_func=nodes_converter.parse_math), + 'RGBTOBW': MaterialNodeMeta(parse_func=nodes_converter.parse_rgbtobw), + 'SEPARATE_COLOR': MaterialNodeMeta(parse_func=nodes_converter.parse_separate_color), + 'SEPHSV': MaterialNodeMeta(parse_func=nodes_converter.parse_sephsv), + 'SEPRGB': MaterialNodeMeta(parse_func=nodes_converter.parse_seprgb), + 'SEPXYZ': MaterialNodeMeta(parse_func=nodes_converter.parse_sepxyz), + 'VALTORGB': MaterialNodeMeta(parse_func=nodes_converter.parse_valtorgb), # ColorRamp + 'VECT_MATH': MaterialNodeMeta(parse_func=nodes_converter.parse_vectormath), + 'WAVELENGTH': MaterialNodeMeta(parse_func=nodes_converter.parse_wavelength), + + # --- nodes_input + 'ATTRIBUTE': MaterialNodeMeta( + parse_func=nodes_input.parse_attribute, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'CAMERA': MaterialNodeMeta( + parse_func=nodes_input.parse_camera, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'FRESNEL': MaterialNodeMeta( + parse_func=nodes_input.parse_fresnel, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'HAIR_INFO': MaterialNodeMeta(parse_func=nodes_input.parse_hairinfo), + 'LAYER_WEIGHT': MaterialNodeMeta( + parse_func=nodes_input.parse_layerweight, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'LIGHT_PATH': MaterialNodeMeta( + parse_func=nodes_input.parse_lightpath, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'NEW_GEOMETRY': MaterialNodeMeta( + parse_func=nodes_input.parse_geometry, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'OBJECT_INFO': MaterialNodeMeta( + parse_func=nodes_input.parse_objectinfo, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'PARTICLE_INFO': MaterialNodeMeta( + parse_func=nodes_input.parse_particleinfo, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'RGB': MaterialNodeMeta( + parse_func=nodes_input.parse_rgb, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'TANGENT': MaterialNodeMeta( + parse_func=nodes_input.parse_tangent, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'TEX_COORD': MaterialNodeMeta( + parse_func=nodes_input.parse_texcoord, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'UVMAP': MaterialNodeMeta( + parse_func=nodes_input.parse_uvmap, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ), + 'VALUE': MaterialNodeMeta( + parse_func=nodes_input.parse_value, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'VERTEX_COLOR': MaterialNodeMeta(parse_func=nodes_input.parse_vertex_color), + 'WIREFRAME': MaterialNodeMeta( + parse_func=nodes_input.parse_wireframe, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + + # --- nodes_shader + 'ADD_SHADER': MaterialNodeMeta(parse_func=nodes_shader.parse_addshader), + 'AMBIENT_OCCLUSION': MaterialNodeMeta(parse_func=nodes_shader.parse_ambientocclusion), + 'BSDF_ANISOTROPIC': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfanisotropic), + 'BSDF_DIFFUSE': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfdiffuse), + 'BSDF_GLASS': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfglass), + 'BSDF_GLOSSY': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfglossy), + 'BSDF_PRINCIPLED': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfprincipled), + 'BSDF_TRANSLUCENT': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdftranslucent), + 'BSDF_TRANSPARENT': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdftransparent), + 'BSDF_VELVET': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfvelvet), + 'EMISSION': MaterialNodeMeta(parse_func=nodes_shader.parse_emission), + 'HOLDOUT': MaterialNodeMeta( + parse_func=nodes_shader.parse_holdout, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'MIX_SHADER': MaterialNodeMeta(parse_func=nodes_shader.parse_mixshader), + 'SUBSURFACE_SCATTERING': MaterialNodeMeta(parse_func=nodes_shader.parse_subsurfacescattering), + + # --- nodes_texture + 'TEX_BRICK': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_brick), + 'TEX_CHECKER': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_checker), + 'TEX_ENVIRONMENT': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_environment), + 'TEX_GRADIENT': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_gradient), + 'TEX_IMAGE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_image), + 'TEX_MAGIC': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_magic), + 'TEX_MUSGRAVE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_musgrave), + 'TEX_NOISE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_noise), + 'TEX_POINTDENSITY': MaterialNodeMeta( + parse_func=nodes_texture.parse_tex_pointdensity, + compute_dxdy_variants=ComputeDXDYVariant.NEVER + ), + 'TEX_SKY': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_sky), + 'TEX_VORONOI': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_voronoi), + 'TEX_WAVE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_wave), + + # --- nodes_vector + 'BUMP': MaterialNodeMeta(parse_func=nodes_vector.parse_bump), + 'CURVE_VEC': MaterialNodeMeta(parse_func=nodes_vector.parse_curvevec), + 'DISPLACEMENT': MaterialNodeMeta(parse_func=nodes_vector.parse_displacement), + 'MAPPING': MaterialNodeMeta(parse_func=nodes_vector.parse_mapping), + 'NORMAL': MaterialNodeMeta(parse_func=nodes_vector.parse_normal), + 'NORMAL_MAP': MaterialNodeMeta(parse_func=nodes_vector.parse_normalmap), + 'VECTOR_ROTATE': MaterialNodeMeta(parse_func=nodes_vector.parse_vectorrotate), + 'VECT_TRANSFORM': MaterialNodeMeta(parse_func=nodes_vector.parse_vectortransform), + + # --- arm_nodes + 'ArmShaderDataNode': MaterialNodeMeta( + parse_func=shader_data_node.ShaderDataNode.parse, + compute_dxdy_variants=ComputeDXDYVariant.ALWAYS + ) +} + + +def get_node_meta(node: bpy.types.Node) -> MaterialNodeMeta: + type_identifier = node.type if node.type != 'CUSTOM' else node.bl_idname + return ALL_NODES[type_identifier] diff --git a/blender/arm/material/parser_state.py b/blender/arm/material/parser_state.py new file mode 100644 index 0000000000..baccdc0ff8 --- /dev/null +++ b/blender/arm/material/parser_state.py @@ -0,0 +1,127 @@ +from enum import IntEnum, unique +from typing import List, Set, Tuple, Union, Optional + +import bpy + +import arm +from arm.material.shader import Shader, ShaderContext, vec3str, floatstr + +if arm.is_reload(__name__): + arm.material.shader = arm.reload_module(arm.material.shader) + from arm.material.shader import Shader, ShaderContext, vec3str, floatstr +else: + arm.enable_reload(__name__) + + + @unique + class ParserContext(IntEnum): + """Describes which kind of node tree is parsed.""" + OBJECT = 0 + # Texture node trees are not supported yet + # TEXTURE = 1 + WORLD = 2 + + + @unique + class ParserPass(IntEnum): + """In some situations, a node tree (or a subtree of that) needs + to be parsed multiple times in different contexts called _passes_. + Nodes can output different code in reaction to the parser state's + current pass; for more information on the individual passes + please refer to below enum items. + """ + REGULAR = 0 + """The tree is parsed to generate regular shader code.""" + + DX_SCREEN_SPACE = 1 + """The tree is parsed to output shader code to compute + the derivative of a value with respect to the screen's x coordinate.""" + + DY_SCREEN_SPACE = 2 + """The tree is parsed to output shader code to compute + the derivative of a value with respect to the screen's y coordinate.""" + + +class ParserState: + """Dataclass to keep track of the current state while parsing a shader tree.""" + + def __init__(self, context: ParserContext, tree_name: str, world: Optional[bpy.types.World] = None): + self.context = context + self.tree_name = tree_name + + self.current_pass = ParserPass.REGULAR + + # The current world, if parsing a world node tree + self.world = world + + # Active shader - frag for surface / tese for displacement + self.curshader: Shader = None + self.con: ShaderContext = None + + self.vert: Shader = None + self.frag: Shader = None + self.geom: Shader = None + self.tesc: Shader = None + self.tese: Shader = None + + # Group stack (last in the list = innermost group) + self.parents: List[bpy.types.Node] = [] + + # Cache for computing nodes only once + self.parsed: Set[str] = set() + + # What to parse from the node tree + self.parse_surface = True + self.parse_opacity = True + self.parse_displacement = True + self.basecol_only = False + + self.procedurals_written: set[Shader] = set() + + # Already exported radiance/irradiance (currently we can only convert + # an already existing texture as radiance/irradiance) + self.radiance_written = False + + self.normal_parsed = False + + self.dxdy_varying_input_value = False + """Whether the result of the previously parsed node differs + between fragments and represents an input value to which to apply + dx/dy offsets (if required by the parser pass). + """ + + # Shader output values + self.out_basecol: vec3str = 'vec3(0.8)' + self.out_roughness: floatstr = '0.0' + self.out_metallic: floatstr = '0.0' + self.out_occlusion: floatstr = '1.0' + self.out_specular: floatstr = '1.0' + self.out_opacity: floatstr = '1.0' + self.out_rior: floatstr = '1.450' + self.out_emission_col: vec3str = 'vec3(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_rior = '1.450' + self.out_emission_col = 'vec3(0.0)' + + def get_outs(self) -> Tuple[vec3str, floatstr, floatstr, floatstr, floatstr, floatstr, floatstr, vec3str]: + """Return the shader output values as a tuple.""" + return (self.out_basecol, self.out_roughness, self.out_metallic, self.out_occlusion, self.out_specular, + self.out_opacity, self.out_rior, self.out_emission_col) + + def get_parser_pass_suffix(self) -> str: + """Return a suffix for the current parser pass that can be appended + to shader variables to avoid compilation errors due to redefinitions. + """ + if self.current_pass == ParserPass.DX_SCREEN_SPACE: + return '_dx' + elif self.current_pass == ParserPass.DY_SCREEN_SPACE: + return '_dy' + return '' diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py new file mode 100644 index 0000000000..48707cc9ae --- /dev/null +++ b/blender/arm/material/shader.py @@ -0,0 +1,456 @@ +import arm.utils + +if arm.is_reload(__name__): + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +# Type aliases for type hints to make it easier to see which kind of +# shader data type is stored in a string +floatstr = str +vec2str = str +vec3str = str +vec4str = str + + +class ShaderData: + + def __init__(self, material): + self.material = material + self.contexts = [] + self.global_elems = [] # bone, weight, ipos, irot, iscl + self.sd = {} + self.data = {'shader_datas': [self.sd]} + self.matname = arm.utils.safesrc(arm.utils.asset_name(material)) + self.sd['name'] = self.matname + '_data' + self.sd['contexts'] = [] + + def add_context(self, props) -> 'ShaderContext': + con = ShaderContext(self.material, self.sd, props) + if con not in self.sd['contexts']: + for elem in self.global_elems: + con.add_elem(elem['name'], elem['data']) + self.sd['contexts'].append(con.get()) + return con + + def get(self): + return self.data + +class ShaderContext: + + def __init__(self, material, shader_data, props): + self.vert = None + self.frag = None + self.geom = None + self.tesc = None + self.tese = None + self.material = material + self.matname = arm.utils.safesrc(arm.utils.asset_name(material)) + self.shader_data = shader_data + self.data = {} + self.data['name'] = props['name'] + self.data['depth_write'] = props['depth_write'] + self.data['compare_mode'] = props['compare_mode'] + self.data['cull_mode'] = props['cull_mode'] + if 'vertex_elements' in props: + self.data['vertex_elements'] = props['vertex_elements'] + else: + self.data['vertex_elements'] = [{'name': 'pos', 'data': 'short4norm'}, {'name': 'nor', 'data': 'short2norm'}] # (p.xyz, n.z), (n.xy) + if 'blend_source' in props: + self.data['blend_source'] = props['blend_source'] + if 'blend_destination' in props: + self.data['blend_destination'] = props['blend_destination'] + if 'blend_operation' in props: + self.data['blend_operation'] = props['blend_operation'] + if 'alpha_blend_source' in props: + self.data['alpha_blend_source'] = props['alpha_blend_source'] + if 'alpha_blend_destination' in props: + self.data['alpha_blend_destination'] = props['alpha_blend_destination'] + if 'alpha_blend_operation' in props: + self.data['alpha_blend_operation'] = props['alpha_blend_operation'] + if 'color_writes_red' in props: + self.data['color_writes_red'] = props['color_writes_red'] + if 'color_writes_green' in props: + self.data['color_writes_green'] = props['color_writes_green'] + if 'color_writes_blue' in props: + self.data['color_writes_blue'] = props['color_writes_blue'] + if 'color_writes_alpha' in props: + self.data['color_writes_alpha'] = props['color_writes_alpha'] + if 'color_attachments' in props: + self.data['color_attachments'] = props['color_attachments'] + + self.data['texture_units'] = [] + self.tunits = self.data['texture_units'] + self.data['constants'] = [] + self.constants = self.data['constants'] + + def add_elem(self, name, data): + elem = { 'name': name, 'data': data } + if elem not in self.data['vertex_elements']: + self.data['vertex_elements'].append(elem) + self.sort_vs() + + def sort_vs(self): + vs = [] + ar = ['pos', 'nor', 'tex', 'tex1', 'morph', 'col', 'tang', 'bone', 'weight', 'ipos', 'irot', 'iscl'] + for ename in ar: + elem = self.get_elem(ename) + if elem != None: + vs.append(elem) + self.data['vertex_elements'] = vs + + def is_elem(self, name): + for elem in self.data['vertex_elements']: + if elem['name'] == name: + return True + return False + + def get_elem(self, name): + for elem in self.data['vertex_elements']: + if elem['name'] == name: + return elem + return None + + def get(self): + return self.data + + 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 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, + default_value=None, is_arm_mat_param=None): + for c in self.tunits: + if c['name'] == name: + return + + c = {'name': name} + if link is not None: + c['link'] = link + if is_image is not None: + c['is_image'] = is_image + if addr_u is not None: + c['addressing_u'] = addr_u + if addr_v is not None: + c['addressing_v'] = addr_v + if filter_min is not None: + c['filter_min'] = filter_min + if filter_mag is not None: + c['filter_mag'] = filter_mag + if mipmap_filter is not None: + c['mipmap_filter'] = mipmap_filter + 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) + + def make_vert(self, custom_name: str = None): + if custom_name is None: + self.data['vertex_shader'] = self.matname + '_' + self.data['name'] + '.vert' + else: + self.data['vertex_shader'] = custom_name + '.vert' + self.vert = Shader(self, 'vert') + return self.vert + + def make_frag(self, custom_name: str = None): + if custom_name is None: + self.data['fragment_shader'] = self.matname + '_' + self.data['name'] + '.frag' + else: + self.data['fragment_shader'] = custom_name + '.frag' + self.frag = Shader(self, 'frag') + return self.frag + + def make_geom(self, custom_name: str = None): + if custom_name is None: + self.data['geometry_shader'] = self.matname + '_' + self.data['name'] + '.geom' + else: + self.data['geometry_shader'] = custom_name + '.geom' + self.geom = Shader(self, 'geom') + return self.geom + + def make_tesc(self, custom_name: str = None): + if custom_name is None: + self.data['tesscontrol_shader'] = self.matname + '_' + self.data['name'] + '.tesc' + else: + self.data['tesscontrol_shader'] = custom_name + '.tesc' + self.tesc = Shader(self, 'tesc') + return self.tesc + + def make_tese(self, custom_name: str = None): + if custom_name is None: + self.data['tesseval_shader'] = self.matname + '_' + self.data['name'] + '.tese' + else: + self.data['tesseval_shader'] = custom_name + '.tese' + self.tese = Shader(self, 'tese') + return self.tese + + +class Shader: + def __init__(self, context, shader_type): + self.context = context + self.shader_type = shader_type + self.includes = [] + self.ins = [] + self.outs = [] + self.uniforms_top = [] + self.uniforms = [] + self.constants = [] + self.functions = {} + self.main = '' + self.main_init = '' + self.main_normal = '' + self.main_textures = '' + self.main_attribs = '' + self.header = '' + self.write_pre = False + self.write_normal = 0 + self.write_textures = 0 + self.tab = 1 + self.vstruct_as_vsin = True + self.lock = False + self.geom_passthrough = False + self.is_linked = False # Use already generated shader + self.noprocessing = False + + def has_include(self, s): + return s in self.includes + + def add_include(self, s): + if not self.has_include(s): + self.includes.append(s) + + def add_include_front(self, s): + if not self.has_include(s): + pos = 0 + # make sure compiled.inc is always on top + if len(self.includes) > 0 and self.includes[0] == 'compiled.inc': + pos = 1 + self.includes.insert(pos, s) + + def add_in(self, s): + if s not in self.ins: + self.ins.append(s) + + def add_out(self, s): + if s not in self.outs: + self.outs.append(s) + + def add_uniform(self, s, link=None, included=False, top=False, + tex_addr_u=None, tex_addr_v=None, + tex_filter_min=None, tex_filter_mag=None, + tex_mipmap_filter=None, default_value=None, is_arm_mat_param=None): + ar = s.split(' ') + # layout(RGBA8) image3D voxels + utype = ar[-2] + uname = ar[-1] + if utype.startswith('sampler') or utype.startswith('image') or utype.startswith('uimage'): + is_image = True if (utype.startswith('image') or utype.startswith('uimage')) else None + if uname[-1] == ']': # Array of samplers - sampler2D mySamplers[2] + # Add individual units - mySamplers[0], mySamplers[1] + for i in range(int(uname[-2])): + uname_array = uname[:-2] + str(i) + ']' + self.context.add_texture_unit( + uname_array, link, is_image, + tex_addr_u, tex_addr_v, + tex_filter_min, tex_filter_mag, tex_mipmap_filter) + else: + self.context.add_texture_unit( + uname, link, is_image, + tex_addr_u, tex_addr_v, + 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]: + ar[0] = 'floats' + ar[1] = ar[1].split('[', 1)[0] + 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, 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) + elif not included and s not in self.uniforms: + self.uniforms.append(s) + + def add_const(self, type_str: str, name: str, value_str: str, array_size: int = 0): + """ + Add a global constant to the shader. + + Parameters + ---------- + type_str: str + The name of the type, like 'float' or 'vec3'. If the + constant is an array, there is no need to add `[]` to the + type + name: str + The name of the variable + value_str: str + The value of the constant as a string + array_size: int + If not 0 (default value), create an array with the given size + """ + if array_size == 0: + self.constants.append(f'{type_str} {name} = {value_str}') + elif array_size > 0: + self.constants.append(f'{type_str} {name}[{array_size}] = {type_str}[]({value_str})') + + def add_function(self, s): + fname = s.split('(', 1)[0] + if fname in self.functions: + return + self.functions[fname] = s + + def contains(self, s): + return s in self.main or \ + s in self.main_init or \ + s in self.main_normal or \ + s in self.ins or \ + s in self.main_textures or \ + s in self.main_attribs + + def replace(self, old, new): + self.main = self.main.replace(old, new) + self.main_init = self.main_init.replace(old, new) + self.main_normal = self.main_normal.replace(old, new) + self.main_textures = self.main_textures.replace(old, new) + self.main_attribs = self.main_attribs.replace(old, new) + self.uniforms = [u.replace(old, new) for u in self.uniforms] + + def write_init(self, s, unique=True): + """Prepend to the main function. If `unique` is true (default), look for other occurences first.""" + if unique and self.contains(s): + return + + self.main_init = '\t' + s + '\n' + self.main_init + + def write(self, s): + if self.lock: + return + if self.write_textures > 0: + self.main_textures += '\t' * 1 + s + '\n' + elif self.write_normal > 0: + self.main_normal += '\t' * 1 + s + '\n' + elif self.write_pre: + self.main_init += '\t' * 1 + s + '\n' + else: + self.main += '\t' * self.tab + s + '\n' + + def write_header(self, s): + self.header += s + '\n' + + def write_attrib(self, s): + self.main_attribs += '\t' + s + '\n' + + def is_equal(self, sh): + self.vstruct_to_vsin() + return self.ins == sh.ins and \ + self.main == sh.main and \ + self.main_normal == sh.main_normal and \ + self.main_init == sh.main_init and \ + self.main_textures == sh.main_textures and \ + self.main_attribs == sh.main_attribs + + def data_size(self, data): + if data == 'float1': + return '1' + elif data == 'float2': + return '2' + elif data == 'float3': + return '3' + elif data == 'float4': + return '4' + elif data == 'short2norm': + return '2' + elif data == 'short4norm': + return '4' + + def vstruct_to_vsin(self): + if self.shader_type != 'vert' or self.ins != [] or not self.vstruct_as_vsin: # Vertex structure as vertex shader input + return + vs = self.context.data['vertex_elements'] + for e in vs: + self.add_in('vec' + self.data_size(e['data']) + ' ' + e['name']) + + def get(self): + if self.noprocessing: + return self.main + + s = '#version 450\n' + + s += self.header + + in_ext = '' + out_ext = '' + + if self.shader_type == 'vert': + self.vstruct_to_vsin() + + elif self.shader_type == 'tesc': + in_ext = '[]' + out_ext = '[]' + s += 'layout(vertices = 3) out;\n' + # Gen outs + for sin in self.ins: + ar = sin.rsplit(' ', 1) # vec3 wnormal + tc_s = 'tc_' + ar[1] + self.add_out(ar[0] + ' ' + tc_s) + # Pass data + self.write('{0}[gl_InvocationID] = {1}[gl_InvocationID];'.format(tc_s, ar[1])) + + elif self.shader_type == 'tese': + in_ext = '[]' + s += 'layout(triangles, equal_spacing, ccw) in;\n' + + elif self.shader_type == 'geom': + in_ext = '[]' + s += 'layout(triangles) in;\n' + if not self.geom_passthrough: + s += 'layout(triangle_strip) out;\n' + s += 'layout(max_vertices=3) out;\n' + + for a in self.uniforms_top: + s += 'uniform ' + a + ';\n' + for a in self.includes: + s += '#include "' + a + '"\n' + if self.geom_passthrough: + s += 'layout(passthrough) in gl_PerVertex { vec4 gl_Position; } gl_in[];\n' + for a in self.ins: + if self.geom_passthrough: + s += 'layout(passthrough) ' + s += 'in {0}{1};\n'.format(a, in_ext) + for a in self.outs: + if not self.geom_passthrough: + s += 'out {0}{1};\n'.format(a, out_ext) + for a in self.uniforms: + s += 'uniform ' + a + ';\n' + for c in self.constants: + s += 'const ' + c + ';\n' + for f in self.functions: + s += self.functions[f] + '\n' + s += 'void main() {\n' + s += self.main_attribs + s += self.main_textures + s += self.main_normal + s += self.main_init + s += self.main + s += '}\n' + return s diff --git a/blender/arm/node_utils.py b/blender/arm/node_utils.py new file mode 100644 index 0000000000..8c24065315 --- /dev/null +++ b/blender/arm/node_utils.py @@ -0,0 +1,250 @@ +import collections.abc +from typing import Any, Generator, Optional, Type, Union + +import bpy +import mathutils +from bpy.types import NodeSocket, NodeInputs, NodeOutputs +from nodeitems_utils import NodeItem + +import arm.log +import arm.logicnode.arm_sockets +import arm.utils + +if arm.is_reload(__name__): + arm.log = arm.reload_module(arm.log) + arm.logicnode.arm_sockets = arm.reload_module(arm.logicnode.arm_sockets) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def find_node_by_link(node_group, to_node, inp): + for link in node_group.links: + if link.to_node == to_node and link.to_socket == inp: + if link.from_node.bl_idname == 'NodeReroute': # Step through reroutes + return find_node_by_link(node_group, link.from_node, link.from_node.inputs[0]) + return link.from_node + + +def find_node_by_link_from(node_group, from_node, outp): + for link in node_group.links: + if link.from_node == from_node and link.from_socket == outp: + return link.to_node + +def find_link(node_group, to_node, inp): + for link in node_group.links: + if link.to_node == to_node and link.to_socket == inp: + return link + + +def get_node_by_type(node_group: bpy.types.NodeTree, ntype: str) -> bpy.types.Node: + for node in node_group.nodes: + if node.type == ntype: + return node + + +def iter_nodes_by_type(node_group: bpy.types.NodeTree, ntype: str) -> Generator[bpy.types.Node, None, None]: + for node in node_group.nodes: + if node.type == ntype: + yield node + + +def input_get_connected_node(input_socket: bpy.types.NodeSocket) -> tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: + """Get the node and the output socket of that node that is connected + to the given input, while following reroutes. If the input has + multiple incoming connections, the first one is followed. If the + connection route ends without a connected node, `(None, None)` is + returned. + """ + # If this method is called while a socket is being unconnected, it + # can happen that is_linked is true but there are no links + if not input_socket.is_linked or len(input_socket.links) == 0: + return None, None + + link: bpy.types.NodeLink = input_socket.links[0] + from_node = link.from_node + + if from_node.type == 'REROUTE': + return input_get_connected_node(from_node.inputs[0]) + + return from_node, link.from_socket + + +def output_get_connected_node(output_socket: bpy.types.NodeSocket) -> tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: + """Get the node and the input socket of that node that is connected + to the given output, while following reroutes. If the output has + multiple outgoing connections, the first one is followed. If the + connection route ends without a connected node, `(None, None)` is + returned. + """ + if not output_socket.is_linked or len(output_socket.links) == 0: + return None, None + + link: bpy.types.NodeLink = output_socket.links[0] + to_node = link.to_node + + if to_node.type == 'REROUTE': + return output_get_connected_node(to_node.outputs[0]) + + return to_node, link.to_socket + + +def get_socket_index(sockets: Union[NodeInputs, NodeOutputs], socket: NodeSocket) -> int: + """Find the socket index in the given node input or output + collection, return -1 if not found. + """ + for i in range(0, len(sockets)): + if sockets[i] == socket: + return i + return -1 + + +def get_socket_type(socket: NodeSocket) -> str: + if isinstance(socket, arm.logicnode.arm_sockets.ArmCustomSocket): + return socket.arm_socket_type + else: + return socket.type + + +def get_socket_default(socket: NodeSocket) -> Any: + """Get the socket's default value, or `None` if it doesn't exist.""" + if isinstance(socket, arm.logicnode.arm_sockets.ArmCustomSocket): + if socket.arm_socket_type != 'NONE': + return socket.default_value_raw + + # Shader-type sockets don't have a default value + elif socket.type != 'SHADER': + return socket.default_value + + return None + + +def set_socket_default(socket: NodeSocket, value: Any): + """Set the socket's default value if it exists.""" + if isinstance(socket, arm.logicnode.arm_sockets.ArmCustomSocket): + if socket.arm_socket_type != 'NONE': + socket.default_value_raw = value + + # Shader-type sockets don't have a default value + elif socket.type != 'SHADER': + socket.default_value = value + + +def get_export_tree_name(tree: bpy.types.NodeTree, do_warn=False) -> str: + """Return the name of the given node tree that's used in the + exported Haxe code. + + If `do_warn` is true, a warning is displayed if the export name + differs from the actual tree name. + """ + export_name = arm.utils.safesrc(tree.name[0].upper() + tree.name[1:]) + + if export_name != tree.name: + arm.log.warn(f'The logic node tree "{tree.name}" had to be temporarily renamed to "{export_name}" on export due to Haxe limitations. Referencing the corresponding trait by its logic node tree name may not work as expected.') + + return export_name + + +def get_export_node_name(node: bpy.types.Node) -> str: + """Return the name of the given node that's used in the exported + Haxe code. + """ + return '_' + arm.utils.safesrc(node.name) + + +def get_haxe_property_names(node: bpy.types.Node) -> Generator[tuple[str, str], None, None]: + """Generator that yields the names of all node properties that have + a counterpart in the node's Haxe class. + """ + for i in range(0, 10): + prop_name = f'property{i}_get' + prop_found = hasattr(node, prop_name) + if not prop_found: + prop_name = f'property{i}' + prop_found = hasattr(node, prop_name) + if prop_found: + # Haxe properties are called property0 - property9 even if + # their Python equivalent can end with '_get', so yield + # both names + yield prop_name, f'property{i}' + + +def haxe_format_socket_val(socket_val: Any, array_outer_brackets=True) -> str: + """Formats a socket value to be valid Haxe syntax. + + If `array_outer_brackets` is false, no square brackets are put + around array values. + + Make sure that elements of sequence types are not yet in Haxe + syntax, otherwise they are strings and get additional quotes! + """ + if isinstance(socket_val, bool): + socket_val = str(socket_val).lower() + + elif isinstance(socket_val, str): + socket_val = '"{:s}"'.format(socket_val.replace('"', '\\"')) + + elif isinstance(socket_val, (collections.abc.Sequence, bpy.types.bpy_prop_array, mathutils.Color, mathutils.Euler, mathutils.Vector)): + socket_val = ','.join(haxe_format_socket_val(v, array_outer_brackets=True) for v in socket_val) + if array_outer_brackets: + socket_val = f'[{socket_val}]' + + elif socket_val is None: + # Don't write 'None' into the Haxe code + socket_val = 'null' + + return str(socket_val) + + +def haxe_format_val(prop) -> str: + """Formats a basic value to be valid Haxe syntax.""" + if isinstance(prop, str): + res = '"' + str(prop) + '"' + elif isinstance(prop, bool): + res = str(prop).lower() + else: + if prop is None: + res = 'null' + else: + res = str(prop) + + return str(res) + + +def haxe_format_prop_value(node: bpy.types.Node, prop_name: str) -> str: + """Formats a property value to be valid Haxe syntax.""" + prop_value = getattr(node, prop_name) + if isinstance(prop_value, str): + prop_value = '"' + str(prop_value) + '"' + elif isinstance(prop_value, bool): + prop_value = str(prop_value).lower() + elif hasattr(prop_value, 'name'): # PointerProperty + prop_value = '"' + str(prop_value.name) + '"' + elif isinstance(prop_value, bpy.types.bpy_prop_array): + prop_value = '[' + ','.join(haxe_format_val(prop) for prop in prop_value) + ']' + else: + if prop_value is None: + prop_value = 'null' + else: + prop_value = str(prop_value) + + return prop_value + + +def nodetype_to_nodeitem(node_type: Type[bpy.types.Node]) -> NodeItem: + """Create a NodeItem from a given node class.""" + # Internal node types seem to have no bl_idname attribute + if issubclass(node_type, bpy.types.NodeInternal): + return NodeItem(node_type.__name__) + + return NodeItem(node_type.bl_idname) + + +def copy_basic_node_props(from_node: bpy.types.Node, to_node: bpy.types.Node): + """Copy non-node-specific properties to a different node.""" + to_node.parent = from_node.parent + to_node.location = from_node.location + to_node.select = from_node.select + + to_node.arm_logic_id = from_node.arm_logic_id + to_node.arm_watch = from_node.arm_watch diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py new file mode 100644 index 0000000000..3e24b70444 --- /dev/null +++ b/blender/arm/nodes_logic.py @@ -0,0 +1,407 @@ +from typing import Any, Callable +import webbrowser + +import bpy +import blf +from bpy.props import BoolProperty, StringProperty + +import arm.logicnode.arm_nodes as arm_nodes +import arm.logicnode.replacement +import arm.logicnode.tree_variables +import arm.logicnode.arm_node_group +import arm.logicnode +import arm.props_traits +import arm.ui_icons as ui_icons +import arm.utils + +if arm.is_reload(__name__): + arm_nodes = arm.reload_module(arm_nodes) + arm.logicnode.replacement = arm.reload_module(arm.logicnode.replacement) + arm.logicnode.tree_variables = arm.reload_module(arm.logicnode.tree_variables) + arm.logicnode = arm.reload_module(arm.logicnode) + arm.props_traits = arm.reload_module(arm.props_traits) + ui_icons = arm.reload_module(ui_icons) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +registered_nodes = [] +registered_categories = [] + + +class ArmLogicTree(bpy.types.NodeTree): + """Logic nodes""" + bl_idname = 'ArmLogicTreeType' + bl_label = 'Logic Node Editor' + bl_icon = 'DECORATE' + + def update(self): + pass + + +class ARM_MT_NodeAddOverride(bpy.types.Menu): + """ + Overrides the `Add node` menu. If called from the logic node + editor, the custom menu is drawn, otherwise the default one is drawn. + + TODO: Find a better solution to custom menus, this will conflict + with other add-ons overriding this menu. + """ + bl_idname = "NODE_MT_add" + bl_label = "Add" + bl_translation_context = bpy.app.translations.contexts.operator_default + + overridden_menu: bpy.types.Menu = None + overridden_draw: Callable = None + + def draw(self, context): + if context.space_data.tree_type == 'ArmLogicTreeType': + layout = self.layout + + # Invoke the search + layout.operator_context = "INVOKE_DEFAULT" + layout.operator('arm.node_search', icon="VIEWZOOM") + + for category_section in arm_nodes.category_items.values(): + layout.separator() + + for category in category_section: + safe_category_name = arm.utils.safesrc(category.name.lower()) + layout.menu(f'ARM_MT_{safe_category_name}_menu', text=category.name, icon=category.icon) + + else: + ARM_MT_NodeAddOverride.overridden_draw(self, context) + + +class ARM_OT_AddNodeOverride(bpy.types.Operator): + bl_idname = "arm.add_node_override" + bl_label = "Add Node" + bl_property = "type" + bl_options = {'INTERNAL'} + + type: StringProperty(name="NodeItem type") + use_transform: BoolProperty(name="Use Transform") + + def invoke(self, context, event): + bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.type, use_transform=self.use_transform) + return {'FINISHED'} + + @classmethod + def description(cls, context, properties): + """Show the node's bl_description attribute as a tooltip or, if + it doesn't exist, its docstring.""" + nodetype = arm.utils.type_name_to_type(properties.type) + + if hasattr(nodetype, 'bl_description'): + return nodetype.bl_description.split('.')[0] + + if nodetype.__doc__ is None: + return "" + + return nodetype.__doc__.split('.')[0].strip() + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + +def get_category_draw_func(category: arm_nodes.ArmNodeCategory): + def draw_category_menu(self, context): + layout = self.layout + + for index, node_section in enumerate(category.node_sections.values()): + if index != 0: + layout.separator() + + for node_item in node_section: + op = layout.operator("arm.add_node_override", text=node_item.label) + op.type = node_item.nodetype + op.use_transform = True + + return draw_category_menu + + +def register_nodes(): + global registered_nodes + + # Re-register all nodes for now.. + if len(registered_nodes) > 0 or len(registered_categories) > 0: + unregister_nodes() + + arm.logicnode.init_nodes(subpackages_only=True) + + for node_type in arm_nodes.nodes: + # Don't register internal nodes, they are already registered + if not issubclass(node_type, bpy.types.NodeInternal): + registered_nodes.append(node_type) + bpy.utils.register_class(node_type) + + # Also add Blender's layout nodes + arm_nodes.add_node(bpy.types.NodeReroute, 'Layout') + arm_nodes.add_node(bpy.types.NodeFrame, 'Layout') + + # Generate and register category menus + for category_section in arm_nodes.category_items.values(): + for category in category_section: + category.sort_nodes() + safe_category_name = arm.utils.safesrc(category.name.lower()) + menu_class = type(f'ARM_MT_{safe_category_name}Menu', (bpy.types.Menu, ), { + 'bl_space_type': 'NODE_EDITOR', + 'bl_idname': f'ARM_MT_{safe_category_name}_menu', + 'bl_label': category.name, + 'bl_description': category.description, + 'draw': get_category_draw_func(category) + }) + registered_categories.append(menu_class) + + bpy.utils.register_class(menu_class) + + +def unregister_nodes(): + global registered_nodes, registered_categories + + for n in registered_nodes: + if issubclass(n, arm_nodes.ArmLogicTreeNode): + n.on_unregister() + bpy.utils.unregister_class(n) + for c in registered_categories: + bpy.utils.unregister_class(c) + + registered_nodes = [] + registered_categories = [] + + +class ARM_PT_LogicNodePanel(bpy.types.Panel): + bl_label = 'Armory Logic Node' + bl_idname = 'ARM_PT_LogicNodePanel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Armory' + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + if context.active_node is not None and context.active_node.bl_idname.startswith('LN'): + layout.prop(context.active_node, 'arm_watch') + + layout.separator() + layout.operator('arm.open_node_documentation', icon='HELP') + column = layout.column(align=True) + column.operator('arm.open_node_python_source', icon='FILE_SCRIPT') + column.operator('arm.open_node_haxe_source', icon_value=ui_icons.get_id("haxe")) + + +class ArmOpenNodeHaxeSource(bpy.types.Operator): + """Expose Haxe source""" + bl_idname = 'arm.open_node_haxe_source' + bl_label = 'Open Node Haxe Source' + + def execute(self, context): + if context.selected_nodes is not None: + if len(context.selected_nodes) == 1: + if context.selected_nodes[0].bl_idname.startswith('LN'): + name = context.selected_nodes[0].bl_idname[2:] + version = arm.utils.get_last_commit() + if version == '': + version = 'main' + webbrowser.open(f'https://github.com/armory3d/armory/tree/{version}/Sources/armory/logicnode/{name}.hx') + return{'FINISHED'} + + +class ArmOpenNodePythonSource(bpy.types.Operator): + """Expose Python source""" + bl_idname = 'arm.open_node_python_source' + bl_label = 'Open Node Python Source' + + def execute(self, context): + if context.selected_nodes is not None: + if len(context.selected_nodes) == 1: + node = context.selected_nodes[0] + if node.bl_idname.startswith('LN') and node.arm_version is not None: + version = arm.utils.get_last_commit() + if version == '': + version = 'main' + rel_path = node.__module__.replace('.', '/') + webbrowser.open(f'https://github.com/armory3d/armory/tree/{version}/blender/{rel_path}.py') + return{'FINISHED'} + + +class ArmOpenNodeWikiEntry(bpy.types.Operator): + """Open the logic node's documentation in the Armory wiki""" + bl_idname = 'arm.open_node_documentation' + bl_label = 'Open Node Documentation' + + def execute(self, context): + if context.selected_nodes is not None: + if len(context.selected_nodes) == 1: + node = context.selected_nodes[0] + if node.bl_idname.startswith('LN') and node.arm_version is not None: + anchor = node.bl_label.lower().replace(" ", "-") + + category = arm_nodes.eval_node_category(node) + category_section = arm_nodes.get_category(category).category_section + + webbrowser.open(f'https://github.com/armory3d/armory/wiki/reference_{category_section}#{anchor}') + + return {'FINISHED'} + + +class ARM_PT_NodeDevelopment(bpy.types.Panel): + """Sidebar panel to ease development of logic nodes.""" + bl_label = 'Node Development' + bl_idname = 'ARM_PT_NodeDevelopment' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Armory' + + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ArmLogicTreeType' and context.space_data.edit_tree + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + node = context.active_node + if node is not None and node.bl_idname.startswith('LN'): + box = layout.box() + box.label(text='Selected Node') + col = box.column(align=True) + + self._draw_row(col, 'bl_idname', node.bl_idname) + self._draw_row(col, 'Category', arm_nodes.eval_node_category(node)) + self._draw_row(col, 'Section', node.arm_section) + self._draw_row(col, 'Specific Version', node.arm_version) + self._draw_row(col, 'Class Version', node.__class__.arm_version) + self._draw_row(col, 'Is Deprecated', node.arm_is_obsolete) + + is_var_node = isinstance(node, arm_nodes.ArmLogicVariableNodeMixin) + self._draw_row(col, 'Is Variable Node', is_var_node) + self._draw_row(col, 'Logic ID', node.arm_logic_id) + if is_var_node: + self._draw_row(col, 'Is Master Node', node.is_master_node) + + layout.separator() + layout.operator('arm.node_replace_all') + + @staticmethod + def _draw_row(col: bpy.types.UILayout, text: str, val: Any): + split = col.split(factor=0.4) + split.label(text=text) + split.label(text=str(val)) + + +class ARM_OT_ReplaceNodesOperator(bpy.types.Operator): + bl_idname = "arm.node_replace_all" + bl_label = "Replace Deprecated Nodes" + bl_description = "Replace all deprecated nodes in the active node tree" + bl_options = {'REGISTER'} + + def execute(self, context): + arm.logicnode.replacement.replace_all() + return {'FINISHED'} + + @classmethod + def poll(cls, context): + return context.space_data is not None and context.space_data.type == 'NODE_EDITOR' + +class ARM_UL_interface_sockets(bpy.types.UIList): + """UI List of input and output sockets""" + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + socket = item + color = socket.draw_color(context, context.active_node) + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row(align=True) + + row.template_node_socket(color=color) + row.prop(socket, "display_label", text="", emboss=False, icon_value=icon) + elif self.layout_type == 'GRID': + layout.alignment = 'CENTER' + layout.template_node_socket(color=color) + +class DrawNodeBreadCrumbs(): + """A class to draw node tree breadcrumbs or context path""" + draw_handler = None + + @classmethod + def convert_array_to_string(cls, arr): + return ' > '.join(arr) + + @classmethod + def draw(cls, context): + if context.space_data.edit_tree and context.space_data.node_tree.bl_idname == "ArmLogicTreeType": + height = context.area.height + path_data = [path.node_tree.name for path in context.space_data.path] + str = cls.convert_array_to_string(path_data) + blf.position(0, 20, height-60, 0) + blf.size(0, 15, 72) + blf.draw(0, str) + + @classmethod + def register_draw(cls): + if cls.draw_handler is not None: + cls.unregister_draw() + cls.draw_handler = bpy.types.SpaceNodeEditor.draw_handler_add(cls.draw, tuple([bpy.context]), 'WINDOW', 'POST_PIXEL') + + @classmethod + def unregister_draw(cls): + if cls.draw_handler is not None: + bpy.types.SpaceNodeEditor.draw_handler_remove(cls.draw_handler, 'WINDOW') + cls.draw_handler = None + +def register(): + arm.logicnode.arm_nodes.register() + arm.logicnode.arm_sockets.register() + arm.logicnode.arm_node_group.register() + + bpy.utils.register_class(ArmLogicTree) + bpy.utils.register_class(ArmOpenNodeHaxeSource) + bpy.utils.register_class(ArmOpenNodePythonSource) + bpy.utils.register_class(ArmOpenNodeWikiEntry) + bpy.utils.register_class(ARM_OT_ReplaceNodesOperator) + ARM_MT_NodeAddOverride.overridden_menu = bpy.types.NODE_MT_add + ARM_MT_NodeAddOverride.overridden_draw = bpy.types.NODE_MT_add.draw + bpy.utils.register_class(ARM_MT_NodeAddOverride) + bpy.utils.register_class(ARM_OT_AddNodeOverride) + bpy.utils.register_class(ARM_UL_interface_sockets) + + # Register panels in correct order + bpy.utils.register_class(ARM_PT_LogicNodePanel) + arm.logicnode.tree_variables.register() + bpy.utils.register_class(ARM_PT_NodeDevelopment) + + arm.logicnode.init_categories() + DrawNodeBreadCrumbs.register_draw() + register_nodes() + + +def unregister(): + unregister_nodes() + DrawNodeBreadCrumbs.unregister_draw() + # Ensure that globals are reset if the addon is enabled again in the same Blender session + arm_nodes.reset_globals() + + bpy.utils.unregister_class(ARM_PT_NodeDevelopment) + arm.logicnode.tree_variables.unregister() + bpy.utils.unregister_class(ARM_PT_LogicNodePanel) + + bpy.utils.unregister_class(ARM_OT_ReplaceNodesOperator) + bpy.utils.unregister_class(ArmLogicTree) + bpy.utils.unregister_class(ArmOpenNodeHaxeSource) + bpy.utils.unregister_class(ArmOpenNodePythonSource) + bpy.utils.unregister_class(ArmOpenNodeWikiEntry) + bpy.utils.unregister_class(ARM_OT_AddNodeOverride) + bpy.utils.unregister_class(ARM_MT_NodeAddOverride) + bpy.utils.register_class(ARM_MT_NodeAddOverride.overridden_menu) + bpy.utils.unregister_class(ARM_UL_interface_sockets) + + arm.logicnode.arm_node_group.unregister() + arm.logicnode.arm_sockets.unregister() + arm.logicnode.arm_nodes.unregister() diff --git a/blender/arm/nodes_material.py b/blender/arm/nodes_material.py new file mode 100644 index 0000000000..fd6750c66d --- /dev/null +++ b/blender/arm/nodes_material.py @@ -0,0 +1,62 @@ +import bpy +import nodeitems_utils +from nodeitems_utils import NodeCategory + +import arm +import arm.material.arm_nodes.arm_nodes as arm_nodes +# Import all nodes so that they register. Do not remove this import +# even if it looks unused +from arm.material.arm_nodes import * + +if arm.is_reload(__name__): + arm_nodes = arm.reload_module(arm_nodes) + arm.material.arm_nodes = arm.reload_module(arm.material.arm_nodes) + from arm.material.arm_nodes import * +else: + arm.enable_reload(__name__) + +registered_nodes = [] + + +class MaterialNodeCategory(NodeCategory): + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'ShaderNodeTree' + + +def register_nodes(): + global registered_nodes + + # Re-register all nodes for now.. + if len(registered_nodes) > 0: + unregister_nodes() + + for n in arm_nodes.nodes: + registered_nodes.append(n) + bpy.utils.register_class(n) + + node_categories = [] + + for category in sorted(arm_nodes.category_items): + sorted_items = sorted(arm_nodes.category_items[category], key=lambda item: item.nodetype) + node_categories.append( + MaterialNodeCategory('ArmMaterial' + category + 'Nodes', category, items=sorted_items) + ) + + nodeitems_utils.register_node_categories('ArmMaterialNodes', node_categories) + + +def unregister_nodes(): + global registered_nodes + for n in registered_nodes: + bpy.utils.unregister_class(n) + registered_nodes = [] + nodeitems_utils.unregister_node_categories('ArmMaterialNodes') + + +def register(): + register_nodes() + + +def unregister(): + unregister_nodes() diff --git a/blender/arm/profiler.py b/blender/arm/profiler.py new file mode 100644 index 0000000000..ed66167a24 --- /dev/null +++ b/blender/arm/profiler.py @@ -0,0 +1,41 @@ +import cProfile +import os +import pstats + +import arm +from arm import log, utils + +if arm.is_reload(__name__): + log = arm.reload_module(log) + utils = arm.reload_module(utils) +else: + arm.enable_reload(__name__) + + +class Profile: + """Context manager for profiling the enclosed code when the given condition is true. + The output file is stored in the SDK directory and can be opened by tools such as SnakeViz. + """ + def __init__(self, filename_out: str, condition: bool): + self.filename_out = filename_out + self.condition = condition + self.pr = cProfile.Profile() + + def __enter__(self): + if self.condition: + self.pr.enable() + log.debug("Profiling started") + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.condition: + self.pr.disable() + log.debug("Profiling finished") + + profile_path = os.path.join(utils.get_sdk_path(), self.filename_out) + with open(profile_path, 'w', encoding="utf-8") as profile_file: + stats = pstats.Stats(self.pr, stream=profile_file) + stats.dump_stats(profile_path) + + return False diff --git a/blender/arm/props.py b/blender/arm/props.py new file mode 100644 index 0000000000..af6ef1f011 --- /dev/null +++ b/blender/arm/props.py @@ -0,0 +1,544 @@ +import bpy +from bpy.props import * +import re +import multiprocessing + +import arm.assets as assets +import arm.logicnode.replacement +import arm.logicnode.tree_variables +import arm.make +import arm.nodes_logic +import arm.utils +import arm.utils_vs + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + arm.logicnode.replacement = arm.reload_module(arm.logicnode.replacement) + arm.logicnode.tree_variables = arm.reload_module(arm.logicnode.tree_variables) + arm.make = arm.reload_module(arm.make) + arm.nodes_logic = arm.reload_module(arm.nodes_logic) + arm.utils = arm.reload_module(arm.utils) + arm.utils_vs = arm.reload_module(arm.utils_vs) +else: + arm.enable_reload(__name__) + +# Armory version +arm_version = '2023.5' +arm_commit = '$Id$' + +def get_project_html5_copy(self): + return self.get('arm_project_html5_copy', False) + +def set_project_html5_copy(self, value): + self['arm_project_html5_copy'] = value + if not value: + self['arm_project_html5_start_browser'] = False + +def get_project_html5_start_browser(self): + return self.get('arm_project_html5_start_browser', False) + +def set_project_html5_start_browser(self, value): + self['arm_project_html5_start_browser'] = value + +def set_project_name(self, value): + value = arm.utils.safestr(value) + if len(value) > 0: + self['arm_project_name'] = value + else: + self['arm_project_name'] = arm.utils.blend_name() + +def get_project_name(self): + return self.get('arm_project_name', arm.utils.blend_name()) + +def set_project_package(self, value): + value = arm.utils.safestr(value).replace('.', '_') + if (len(value) > 0) and (not value.isdigit()) and (not value[0].isdigit()): + self['arm_project_package'] = value + +def get_project_package(self): + return self.get('arm_project_package', 'arm') + +def set_version(self, value): + value = value.strip().replace(' ', '') + if re.match(r'^\d+(\.\d+){1,3}$', value) is not None: + check = True + v_i = value.split('.') + for item in v_i: + try: + i = int(item) + except ValueError: + check = False + break + if check: + self['arm_project_version'] = value + +def get_version(self): + return self.get('arm_project_version', '1.0.0') + +def set_project_bundle(self, value): + value = arm.utils.safestr(value) + v_a = value.strip().split('.') + if (len(value) > 0) and (not value.isdigit()) and (not value[0].isdigit()) and (len(v_a) > 1): + check = True + for item in v_a: + if (item.isdigit()) or (item[0].isdigit()): + check = False + break + if check: + self['arm_project_bundle'] = value + +def get_project_bundle(self): + return self.get('arm_project_bundle', 'org.armory3d') + +def get_android_build_apk(self): + if len(arm.utils.get_android_sdk_root_path()) > 0: + return self.get('arm_project_android_build_apk', False) + else: + set_android_build_apk(self, False) + return False + +def set_android_build_apk(self, value): + self['arm_project_android_build_apk'] = value + if not value: + wrd = bpy.data.worlds['Arm'] + wrd.arm_project_android_rename_apk = False + wrd.arm_project_android_copy_apk = False + wrd.arm_project_android_run_avd = False + +def get_win_build_arch(self): + if self.get('arm_project_win_build_arch', -1) == -1: + if arm.utils.get_os_is_windows_64(): + return 0 + else: + return 1 + else: + return self.get('arm_project_win_build_arch', 'x64') + +def set_win_build_arch(self, value): + self['arm_project_win_build_arch'] = value + +def set_win_build(self, value): + if arm.utils.get_os_is_windows(): + self['arm_project_win_build'] = value + else: + self['arm_project_win_build'] = 0 + if (self['arm_project_win_build'] == 0) or (self['arm_project_win_build'] == 1): + wrd = bpy.data.worlds['Arm'] + wrd.arm_project_win_build_open = False + +def get_win_build(self): + if arm.utils.get_os_is_windows(): + return self.get('arm_project_win_build', 0) + else: + return 0 + +def init_properties(): + global arm_version + bpy.types.World.arm_recompile = BoolProperty(name="Recompile", description="Recompile sources on next play", default=True) + bpy.types.World.arm_version = StringProperty(name="Version", description="Armory SDK version", default="") + bpy.types.World.arm_commit = StringProperty(name="Version Commit", description="Armory SDK version", default="") + bpy.types.World.arm_project_name = StringProperty(name="Name", description="Exported project name", default="", update=assets.invalidate_compiler_cache, set=set_project_name, get=get_project_name) + bpy.types.World.arm_project_package = StringProperty(name="Package", description="Package name for scripts", default="arm", update=assets.invalidate_compiler_cache, set=set_project_package, get=get_project_package) + bpy.types.World.arm_project_version = StringProperty(name="Version", description="Exported project version", default="1.0.0", update=assets.invalidate_compiler_cache, set=set_version, get=get_version) + bpy.types.World.arm_project_version_autoinc = BoolProperty(name="Auto-increment Build Number", description="Auto-increment build number", default=True, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_bundle = StringProperty(name="Bundle", description="Exported project bundle", default="org.armory3d", update=assets.invalidate_compiler_cache, set=set_project_bundle, get=get_project_bundle) + # Android Settings + bpy.types.World.arm_project_android_sdk_min = IntProperty(name="Minimal Version SDK", description="Minimal Version Android SDK", default=23, min=14, max=30, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_sdk_target = IntProperty(name="Target Version SDK", description="Target Version Android SDK", default=26, min=26, max=30, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_sdk_compile = IntProperty(name="Maximal Version SDK", description="Maximal Android SDK Version", default=30, min=26, max=30, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_build_apk = BoolProperty(name="Building APK After Publishing", description="Starting APK build after publishing", default=False, update=assets.invalidate_compiler_cache, get=get_android_build_apk, set=set_android_build_apk) + bpy.types.World.arm_project_android_rename_apk = BoolProperty(name="Rename APK To Project Name", description="Rename APK file to project name + version after build", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_copy_apk = BoolProperty(name="Copy APK To Specified Folder", description="Copy the APK file to the folder specified in the settings after build", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_run_avd = BoolProperty(name="Run Emulator After Building APK", description="Starting android emulator after APK build", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_android_list_avd = EnumProperty( + items=[(' ', ' ', ' ')], + name="Emulator", update=assets.invalidate_compiler_cache) + # HTML5 Settings + bpy.types.World.arm_project_html5_copy = BoolProperty(name="Copy Files To Specified Folder", description="Copy files to the folder specified in the settings after publish", default=False, update=assets.invalidate_compiler_cache, set=set_project_html5_copy, get=get_project_html5_copy) + bpy.types.World.arm_project_html5_start_browser = BoolProperty(name="Run Browser After Copy", description="Run browser after copy", default=False, update=assets.invalidate_compiler_cache, set=set_project_html5_start_browser, get=get_project_html5_start_browser) + bpy.types.World.arm_project_html5_popupmenu_in_browser = BoolProperty(name="Disable Browser Context Menu", description="Disable the browser context menu for the canvas element on the page", default=False, update=assets.invalidate_compiler_cache) + # Windows Settings + bpy.types.World.arm_project_win_list_vs = EnumProperty( + items=arm.utils_vs.supported_versions, + name="Visual Studio Version", default='17', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_win_build = EnumProperty( + items=[('nothing', 'Nothing', 'Nothing'), + ('open', 'Open in Visual Studio', 'Open in Visual Studio'), + ('compile', 'Compile', 'Compile the application'), + ('compile_and_run', 'Compile and Run', 'Compile and run the application')], + name="Action After Publishing", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_win_build_mode = EnumProperty( + items=[('Debug', 'Debug', 'Debug'), + ('Release', 'Release', 'Release')], + name="Mode", default='Debug', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_win_build_arch = EnumProperty( + items=[('x64', 'x64', 'x64'), + ('x86', 'x86', 'x86')], + name="Architecture", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_win_build_log = EnumProperty( + items=[('Summary', 'Summary', 'Show the error and warning summary at the end'), + ('NoSummary', 'No Summary', 'Don\'t show the error and warning summary at the end'), + ('WarningsAndErrorsOnly', 'Warnings and Errors Only', 'Show only warnings and errors'), + ('WarningsOnly', 'Warnings Only', 'Show only warnings'), + ('ErrorsOnly', 'Errors Only', 'Show only errors')], + name="Compile Log Parameter", update=assets.invalidate_compiler_cache, + default="Summary") + bpy.types.World.arm_project_win_build_cpu = IntProperty(name="CPU Count", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) + bpy.types.World.arm_project_win_build_open = BoolProperty(name="Open Build Directory", description="Open the build directory after successfully assemble", default=False) + + bpy.types.World.arm_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_physics = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Auto', 'Auto', 'Auto'), + ('Enabled', 'Enabled', 'Enabled')], + name="Physics", default='Auto', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_physics_engine = EnumProperty( + items=[('Bullet', 'Bullet', 'Bullet'), + ('Oimo', 'Oimo', 'Oimo')], + name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_navigation = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Auto', 'Auto', 'Auto'), + ('Enabled', 'Enabled', 'Enabled')], + name="Navigation", default='Auto', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_navigation_engine = EnumProperty( + items=[('Recast', 'Recast', 'Recast')], + name="Navigation Engine", default='Recast') + bpy.types.World.arm_ui = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Enabled', 'Enabled', 'Enabled'), + ('Auto', 'Auto', 'Auto')], + name="Zui", default='Auto', description="Include UI library", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_network = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Enabled', 'Enabled', 'Enabled'), + ('Auto', 'Auto', 'Auto')], + name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache) + bpy.types.World.arm_audio = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Enabled', 'Enabled', 'Enabled')], + name="Audio", default='Enabled', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_khafile = PointerProperty(name="Append Khafile", description="Source appended to the project's khafile.js after it is generated", update=assets.invalidate_compiler_cache, type=bpy.types.Text) + bpy.types.World.arm_texture_quality = FloatProperty(name="Texture Quality", default=1.0, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_sound_quality = FloatProperty(name="Sound Quality", default=0.9, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_copy_override = BoolProperty(name="Copy Override", description="Overrides any existing files when copying", default=False, update=assets.invalidate_compiled_data) + bpy.types.World.arm_minimize = BoolProperty(name="Binary Scene Data", description="Export scene data in binary", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_minify_js = BoolProperty(name="Minify JS", description="Minimize JavaScript output when publishing", default=True) + bpy.types.World.arm_no_traces = BoolProperty(name="No Traces", description="Don't compile trace calls in the program when publishing", default=False) + bpy.types.World.arm_optimize_data = BoolProperty(name="Optimize Data", description="Export more efficient geometry and shader data when publishing, prolongs build times", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_deinterleaved_buffers = BoolProperty(name="Deinterleaved Buffers", description="Use deinterleaved vertex buffers", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_export_tangents = BoolProperty(name="Precompute Tangents", description="Precompute tangents for normal mapping, otherwise computed in shader", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_batch_meshes = BoolProperty(name="Batch Meshes", description="Group meshes by materials to speed up rendering", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_batch_materials = BoolProperty(name="Batch Materials", description="Marge similar materials into single pipeline state", default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_stream_scene = BoolProperty(name="Stream Scene", description="Stream scene content", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_lod_gen_levels = IntProperty(name="Levels", description="Number of levels to generate", default=3, min=1) + bpy.types.World.arm_lod_gen_ratio = FloatProperty(name="Decimate Ratio", description="Decimate ratio", default=0.8) + bpy.types.World.arm_cache_build = BoolProperty(name="Cache Build", description="Cache build files to speed up compilation", default=True) + bpy.types.World.arm_assert_level = EnumProperty( + items=[ + ('Warning', 'Warning', 'Warning level, warnings don\'t throw an ArmAssertException'), + ('Error', 'Error', 'Error level. If assertions with this level fail, an ArmAssertException is thrown'), + ('NoAssertions', 'No Assertions', 'Ignore all assertions'), + ], + name="Assertion Level", description="Ignore all assertions below this level (assertions are turned off completely for published builds)", default='Warning', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_assert_quit = BoolProperty(name="Quit On Assertion Fail", description="Whether to close the game when an 'Error' level assertion fails", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_live_patch = BoolProperty(name="Live Patch", description="Live patching for Krom", default=False) + bpy.types.World.arm_clear_on_compile = BoolProperty(name="Clear Console", description="Clears the system console on compile", default=False) + bpy.types.World.arm_play_camera = EnumProperty( + items=[('Scene', 'Scene', 'Scene'), + ('Viewport', 'Viewport', 'Viewport')], + name="Camera", description="Viewport camera", default='Scene', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_play_scene = PointerProperty(name="Scene", description="Scene to launch", update=assets.invalidate_compiler_cache, type=bpy.types.Scene) + bpy.types.World.arm_play_renderpath = StringProperty(name="Render Path", description="Default renderpath for debugging", update=assets.invalidate_compiler_cache) + # Debug Console + bpy.types.World.arm_debug_console = BoolProperty(name="Enable", description="Show inspector in player and enable debug draw.\nRequires that Zui is not disabled", default=arm.utils.get_debug_console_auto(), update=assets.invalidate_shader_cache) + bpy.types.World.arm_debug_console_position = EnumProperty( + items=[('Left', 'Left', 'Left'), + ('Center', 'Center', 'Center'), + ('Right', 'Right', 'Right')], + name="Position", description="Position Debug Console", default='Right', update=assets.invalidate_shader_cache) + bpy.types.World.arm_debug_console_scale = FloatProperty(name="Scale Console", description="Scale Debug Console", default=1.0, min=0.3, max=10.0, subtype='FACTOR', update=assets.invalidate_shader_cache) + bpy.types.World.arm_debug_console_visible = BoolProperty(name="Visible", description="Setting the console visibility at application start", default=True, update=assets.invalidate_shader_cache) + bpy.types.World.arm_debug_console_trace_pos = BoolProperty(name="Print With Position", description="Whether to prepend the position of print/trace statements to the printed text", default=True) + bpy.types.World.arm_verbose_output = BoolProperty(name="Verbose Output", description="Print additional information to the console during compilation", default=False) + bpy.types.World.arm_runtime = EnumProperty( + items=[('Krom', 'Krom', 'Krom'), + ('Browser', 'Browser', 'Browser')], + name="Runtime", description="Runtime to use when launching the game", default='Krom', update=assets.invalidate_shader_cache) + bpy.types.World.arm_loadscreen = BoolProperty(name="Loading Screen", description="Show asset loading progress on published builds", default=True) + bpy.types.World.arm_vsync = BoolProperty(name="VSync", description="Vertical Synchronization", default=True, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_dce = BoolProperty(name="DCE", description="Enable dead code elimination for publish builds", default=True, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_asset_compression = BoolProperty(name="Asset Compression", description="Enable scene data compression with LZ4 when publishing. Warning: This will slow down export!", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_single_data_file = BoolProperty(name="Single Data File", description="Pack exported meshes and materials into single file", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_write_config = BoolProperty(name="Write Config", description="Allow this project to be configured at runtime via a JSON file", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_compiler_inline = BoolProperty(name="Compiler Inline", description="Favor speed over size", default=True, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_winmode = EnumProperty( + items = [('Window', 'Window', 'Window'), + ('Fullscreen', 'Fullscreen', 'Fullscreen')], + name="Mode", default='Window', description='Window mode to start in', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_winorient = EnumProperty( + items = [('Multi', 'Multi', 'Multi'), + ('Portrait', 'Portrait', 'Portrait'), + ('Landscape', 'Landscape', 'Landscape')], + name="Orientation", default='Landscape', description='Set screen orientation on mobile devices') + bpy.types.World.arm_winresize = BoolProperty(name="Resizable", description="Allow window resize", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_winmaximize = BoolProperty(name="Maximizable", description="Allow window maximize", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.arm_winminimize = BoolProperty(name="Minimizable", description="Allow window minimize", default=True, update=assets.invalidate_compiler_cache) + # For object + bpy.types.Object.arm_instanced = EnumProperty( + items = [('Off', 'Off', 'No instancing of children'), + ('Loc', 'Loc', 'Instances use their unique position (ipos)'), + ('Loc + Rot', 'Loc + Rot', 'Instances use their unique position and rotation (ipos and irot)'), + ('Loc + Scale', 'Loc + Scale', 'Instances use their unique position and scale (ipos and iscl)'), + ('Loc + Rot + Scale', 'Loc + Rot + Scale', 'Instances use their unique position, rotation and scale (ipos, irot, iscl)')], + name="Instanced Children", default='Off', + description='Whether to use instancing to draw the children of this object. If enabled, this option defines what attributes may vary between the instances', + update=assets.invalidate_instance_cache, + override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.arm_export = BoolProperty(name="Export", description="Export object data", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.arm_spawn = BoolProperty(name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.arm_mobile = BoolProperty(name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.arm_visible = BoolProperty(name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.arm_soft_body_margin = FloatProperty(name="Soft Body Margin", description="Collision margin", default=0.04) + bpy.types.Object.arm_rb_linear_factor = FloatVectorProperty(name="Linear Factor", size=3, description="Set to 0 to lock axis", default=[1,1,1]) + bpy.types.Object.arm_rb_angular_factor = FloatVectorProperty(name="Angular Factor", size=3, description="Set to 0 to lock axis", default=[1,1,1]) + bpy.types.Object.arm_rb_angular_friction = FloatProperty(name="Angular Friction", description="Angular Friction", default=0.1) + bpy.types.Object.arm_rb_trigger = BoolProperty(name="Trigger", description="Disable contact response", default=False) + bpy.types.Object.arm_rb_deactivation_time = FloatProperty(name="Deactivation Time", description="Delay putting rigid body into sleep", default=0.0) + bpy.types.Object.arm_rb_ccd = BoolProperty(name="Continuous Collision Detection", description="Improve collision for fast moving objects", default=False) + bpy.types.Object.arm_rb_collision_filter_mask = bpy.props.BoolVectorProperty( + name="Collision Collections Filter Mask", + description="Collision collections rigid body interacts with", + 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='') + # For speakers + bpy.types.Speaker.arm_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False) + bpy.types.Speaker.arm_loop = BoolProperty(name="Loop", description="Loop this sound", default=False) + bpy.types.Speaker.arm_stream = BoolProperty(name="Stream", description="Stream this sound", default=False) + # For mesh + bpy.types.Mesh.arm_cached = BoolProperty(name="Mesh Cached", description="No need to reexport mesh data", default=False) + bpy.types.Mesh.arm_aabb = FloatVectorProperty(name="AABB", size=3, default=[0,0,0]) + bpy.types.Mesh.arm_dynamic_usage = BoolProperty(name="Dynamic Usage", description="Mesh data can change at runtime", default=False) + bpy.types.Curve.arm_cached = BoolProperty(name="Mesh Cached", description="No need to reexport curve data", default=False) + bpy.types.Curve.arm_aabb = FloatVectorProperty(name="AABB", size=3, default=[0,0,0]) + bpy.types.Curve.arm_dynamic_usage = BoolProperty(name="Dynamic Data Usage", description="Curve data can change at runtime", default=False) + bpy.types.MetaBall.arm_cached = BoolProperty(name="Mesh Cached", description="No need to reexport metaball data", default=False) + bpy.types.MetaBall.arm_aabb = FloatVectorProperty(name="AABB", size=3, default=[0,0,0]) + bpy.types.MetaBall.arm_dynamic_usage = BoolProperty(name="Dynamic Data Usage", description="Metaball data can change at runtime", default=False) + # For armature + bpy.types.Armature.arm_cached = BoolProperty(name="Armature Cached", description="No need to reexport armature data", default=False) + bpy.types.Armature.arm_autobake = BoolProperty(name="Auto Bake", description="Bake constraints automatically", default=True) + bpy.types.Armature.arm_relative_bone_constraints = BoolProperty(name="Relative Bone Constraints", description="Constraint are applied relative to Armature's parent", default=False) + # For camera + bpy.types.Camera.arm_frustum_culling = BoolProperty(name="Frustum Culling", description="Perform frustum culling for this camera", default=True) + + # Render path generator + bpy.types.World.rp_preset = EnumProperty( + items=[('Desktop', 'Desktop', 'Desktop'), + ('Mobile', 'Mobile', 'Mobile'), + ('Max', 'Max', 'Max'), + ('2D/Baked', '2D/Baked', '2D/Baked'), + ], + name="Preset", description="Render path preset", default='Desktop') + bpy.types.World.arm_envtex_name = StringProperty(name="Environment Texture", default='') + bpy.types.World.arm_envtex_irr_name = StringProperty(name="Environment Irradiance", default='') + bpy.types.World.arm_envtex_num_mips = IntProperty(name="Number of mips", default=0) + bpy.types.World.arm_envtex_color = FloatVectorProperty(name="Environment Color", size=4, default=[0,0,0,1]) + bpy.types.World.arm_envtex_strength = FloatProperty(name="Environment Strength", default=1.0) + bpy.types.World.arm_envtex_sun_direction = FloatVectorProperty(name="Sun Direction", size=3, default=[0,0,0]) + bpy.types.World.arm_envtex_turbidity = FloatProperty(name="Turbidity", default=1.0) + bpy.types.World.arm_envtex_ground_albedo = FloatProperty(name="Ground Albedo", default=0.0) + bpy.types.World.arm_nishita_density = FloatVectorProperty(name="Nishita Density", size=3, default=[1, 1, 1]) + bpy.types.Material.arm_cast_shadow = BoolProperty(name="Cast Shadow", default=True) + bpy.types.Material.arm_receive_shadow = BoolProperty(name="Receive Shadow", description="Requires forward render path", default=True) + bpy.types.Material.arm_depth_read = BoolProperty(name="Read Depth", description="Allow this material to read from a depth texture which is copied from the depth buffer. The meshes using this material will be drawn after all meshes that don't read from the depth texture", default=False) + bpy.types.Material.arm_overlay = BoolProperty(name="Overlay", description="Renders the material, unshaded, over other shaded materials", default=False) + bpy.types.Material.arm_decal = BoolProperty(name="Decal", default=False) + bpy.types.Material.arm_two_sided = BoolProperty(name="Two-Sided", description="Flip normal when drawing back-face", default=False) + bpy.types.Material.arm_ignore_irradiance = BoolProperty(name="Ignore Irradiance", description="Ignore irradiance for material", default=False) + bpy.types.Material.arm_cull_mode = EnumProperty( + items=[('none', 'Both', 'None'), + ('clockwise', 'Front', 'Clockwise'), + ('counter_clockwise', 'Back', 'Counter-Clockwise')], + name="Cull Mode", default='clockwise', description="Draw geometry faces") + bpy.types.Material.arm_discard = BoolProperty(name="Alpha Test", default=False, description="Do not render fragments below specified opacity threshold") + bpy.types.Material.arm_discard_opacity = FloatProperty(name="Mesh Opacity", default=0.2, min=0, max=1) + bpy.types.Material.arm_discard_opacity_shadows = FloatProperty(name="Shadows Opacity", default=0.1, min=0, max=1) + bpy.types.Material.arm_custom_material = StringProperty(name="Custom Material", description="Write custom material", default='') + bpy.types.Material.arm_billboard = EnumProperty( + items=[('off', 'Off', 'Off'), + ('spherical', 'Spherical', 'Spherical'), + ('cylindrical', 'Cylindrical', 'Cylindrical')], + name="Billboard", default='off', description="Track camera", update=assets.invalidate_shader_cache) + bpy.types.Material.arm_tilesheet_flag = BoolProperty(name="Tilesheet Flag", description="This material is used for tilesheet", default=False) + bpy.types.Material.arm_particle_flag = BoolProperty(name="Particle Flag", description="This material is used for particles", default=False) + bpy.types.Material.arm_particle_fade = BoolProperty(name="Particle Fade", description="Fade particles in and out", default=False) + bpy.types.Material.arm_blending = BoolProperty(name="Blending", description="Enable additive blending", default=False) + bpy.types.Material.arm_blending_source = EnumProperty( + items=[('blend_one', 'One', 'One'), + ('blend_zero', 'Zero', 'Zero'), + ('source_alpha', 'Source Alpha', 'Source Alpha'), + ('destination_alpha', 'Destination Alpha', 'Destination Alpha'), + ('inverse_source_alpha', 'Inverse Source Alpha', 'Inverse Source Alpha'), + ('inverse_destination_alpha', 'Inverse Destination Alpha', 'Inverse Destination Alpha'), + ('source_color', 'Source Color', 'Source Color'), + ('destination_color', 'Destination Color', 'Destination Color'), + ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), + ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], + name='Source', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + bpy.types.Material.arm_blending_destination = EnumProperty( + items=[('blend_one', 'One', 'One'), + ('blend_zero', 'Zero', 'Zero'), + ('source_alpha', 'Source Alpha', 'Source Alpha'), + ('destination_alpha', 'Destination Alpha', 'Destination Alpha'), + ('inverse_source_alpha', 'Inverse Source Alpha', 'Inverse Source Alpha'), + ('inverse_destination_alpha', 'Inverse Destination Alpha', 'Inverse Destination Alpha'), + ('source_color', 'Source Color', 'Source Color'), + ('destination_color', 'Destination Color', 'Destination Color'), + ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), + ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], + name='Destination', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + bpy.types.Material.arm_blending_operation = EnumProperty( + items=[('add', 'Add', 'Add'), + ('subtract', 'Subtract', 'Subtract'), + ('reverse_subtract', 'Reverse Subtract', 'Reverse Subtract'), + ('min', 'Min', 'Min'), + ('max', 'Max', 'Max')], + name='Operation', default='add', description='Blending operation', update=assets.invalidate_shader_cache) + bpy.types.Material.arm_blending_source_alpha = EnumProperty( + items=[('blend_one', 'One', 'One'), + ('blend_zero', 'Zero', 'Zero'), + ('source_alpha', 'Source Alpha', 'Source Alpha'), + ('destination_alpha', 'Destination Alpha', 'Destination Alpha'), + ('inverse_source_alpha', 'Inverse Source Alpha', 'Inverse Source Alpha'), + ('inverse_destination_alpha', 'Inverse Destination Alpha', 'Inverse Destination Alpha'), + ('source_color', 'Source Color', 'Source Color'), + ('destination_color', 'Destination Color', 'Destination Color'), + ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), + ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], + name='Source (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + bpy.types.Material.arm_blending_destination_alpha = EnumProperty( + items=[('blend_one', 'One', 'One'), + ('blend_zero', 'Zero', 'Zero'), + ('source_alpha', 'Source Alpha', 'Source Alpha'), + ('destination_alpha', 'Destination Alpha', 'Destination Alpha'), + ('inverse_source_alpha', 'Inverse Source Alpha', 'Inverse Source Alpha'), + ('inverse_destination_alpha', 'Inverse Destination Alpha', 'Inverse Destination Alpha'), + ('source_color', 'Source Color', 'Source Color'), + ('destination_color', 'Destination Color', 'Destination Color'), + ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), + ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], + name='Destination (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + bpy.types.Material.arm_blending_operation_alpha = EnumProperty( + items=[('add', 'Add', 'Add'), + ('subtract', 'Subtract', 'Subtract'), + ('reverse_subtract', 'Reverse Subtract', 'Reverse Subtract'), + ('min', 'Min', 'Min'), + ('max', 'Max', 'Max')], + name='Operation (Alpha)', default='add', description='Blending operation', update=assets.invalidate_shader_cache) + # For scene + bpy.types.Scene.arm_export = BoolProperty(name="Export", description="Export scene data", default=True) + bpy.types.Scene.arm_terrain_textures = StringProperty(name="Textures", description="Set root folder for terrain assets", default="//Bundled/", subtype="DIR_PATH") + bpy.types.Scene.arm_terrain_sectors = IntVectorProperty(name="Sectors", description="Number of sectors to generate", default=[1,1], size=2) + bpy.types.Scene.arm_terrain_sector_size = FloatProperty(name="Sector Size", description="Dimensions for single sector", default=16) + bpy.types.Scene.arm_terrain_height_scale = FloatProperty(name="Height Scale", description="Scale height from the 0-1 range", default=5) + bpy.types.Scene.arm_terrain_object = PointerProperty(name="Object", type=bpy.types.Object, description="Terrain root object") + # For light + bpy.types.Light.arm_clip_start = FloatProperty(name="Clip Start", default=0.1) + bpy.types.Light.arm_clip_end = FloatProperty(name="Clip End", default=50.0) + bpy.types.Light.arm_fov = FloatProperty(name="Field of View", default=0.84) + bpy.types.Light.arm_shadows_bias = FloatProperty(name="Bias", description="Depth offset to fight shadow acne", default=1.0) + bpy.types.World.arm_light_ies_texture = StringProperty(name="IES Texture", default="") + bpy.types.World.arm_light_clouds_texture = StringProperty(name="Clouds Texture", default="") + + bpy.types.World.arm_rpcache_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.arm_scripts_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.arm_bundled_scripts_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.arm_canvas_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.arm_wasm_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.world_defs = StringProperty(name="World Shader Defs", default='') + bpy.types.World.compo_defs = StringProperty(name="Compositor Shader Defs", default='') + + bpy.types.World.arm_use_clouds = BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_darken_clouds = BoolProperty( + name="Darken Clouds at Night", + description="Darkens the clouds when the sun is low. This setting is for artistic purposes and is not physically correct", + default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_lower = FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_upper = FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_wind = FloatVectorProperty(name="Wind", default=[1.0, 0.0], size=2, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_secondary = FloatProperty(name="Secondary", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_precipitation = FloatProperty(name="Precipitation", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) + bpy.types.World.arm_clouds_steps = IntProperty(name="Steps", default=24, min=1, max=240, update=assets.invalidate_shader_cache) + + bpy.types.Material.export_uvs = BoolProperty(name="Export UVs", default=False) + bpy.types.Material.export_vcols = BoolProperty(name="Export VCols", default=False) + bpy.types.Material.export_tangents = BoolProperty(name="Export Tangents", default=False) + bpy.types.Material.arm_skip_context = StringProperty(name="Skip Context", default='') + bpy.types.Material.arm_material_id = IntProperty(name="ID", default=0) + bpy.types.NodeSocket.is_uniform = BoolProperty(name="Is Uniform", description="Mark node sockets to be processed as material uniforms", default=False) + bpy.types.NodeTree.arm_cached = BoolProperty(name="Node Tree Cached", description="No need to reexport node tree", default=False) + bpy.types.Material.signature = StringProperty(name="Signature", description="Unique string generated from material nodes", default="") + bpy.types.Material.arm_cached = BoolProperty(name="Material Cached", description="No need to reexport material data", default=False) + bpy.types.Node.arm_material_param = BoolProperty(name="Parameter", description="Control this node from script", default=False) + bpy.types.Node.arm_watch = BoolProperty(name="Watch", description="Watch value of this node in debug console", default=False) + bpy.types.Node.arm_version = IntProperty(name="Node Version", description="The version of an instanced node", default=0) + # Particles + bpy.types.ParticleSettings.arm_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Armory", default=1.0) + bpy.types.ParticleSettings.arm_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False) + + create_wrd() + +def create_wrd(): + if 'Arm' not in bpy.data.worlds: + wrd = bpy.data.worlds.new('Arm') + wrd.use_fake_user = True # Store data world object, add fake user to keep it alive + wrd.arm_version = arm_version + wrd.arm_commit = arm_commit + +def init_properties_on_load(): + if 'Arm' not in bpy.data.worlds: + init_properties() + # New project? + if bpy.data.filepath == '': + wrd = bpy.data.worlds['Arm'] + wrd.arm_debug_console = arm.utils.get_debug_console_auto() + arm.utils.fetch_script_names() + +def update_armory_world(): + global arm_version + wrd = bpy.data.worlds['Arm'] + + # Outdated project + file_version = tuple(map(int, wrd.arm_version.split('.'))) + sdk_version = tuple(map(int, arm_version.split('.'))) + if bpy.data.filepath != '' and (file_version < sdk_version or wrd.arm_commit != arm_commit): + + # For some breaking changes we need to use a special update + # routine first before regularly replacing nodes + if file_version < (2021, 8): + arm.logicnode.replacement.node_compat_sdk2108() + if file_version < (2022, 3): + arm.logicnode.tree_variables.node_compat_sdk2203() + if file_version < (2022, 9): + arm.logicnode.tree_variables.node_compat_sdk2209() + + arm.logicnode.replacement.replace_all() + + print(f'Project updated to SDK v{arm_version}({arm_commit})') + wrd.arm_version = arm_version + wrd.arm_commit = arm_commit + arm.make.clean() + +def register(): + init_properties() + arm.utils.fetch_bundled_script_names() + +def unregister(): + pass diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py new file mode 100644 index 0000000000..ca26aa3aee --- /dev/null +++ b/blender/arm/props_bake.py @@ -0,0 +1,401 @@ +import bpy +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +from arm.lightmapper import operators, properties, utility + +import arm.assets +import arm.utils + +if arm.is_reload(__name__): + arm.assets = arm.reload_module(arm.assets) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +class ArmBakeListItem(bpy.types.PropertyGroup): + obj: PointerProperty(type=bpy.types.Object, description="The object to bake") + res_x: IntProperty(name="X", description="Texture resolution", default=1024) + res_y: IntProperty(name="Y", description="Texture resolution", default=1024) + +class ARM_UL_BakeList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(item, "obj", text="", emboss=False, icon=custom_icon) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=str(item.res_x) + 'x' + str(item.res_y)) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon=custom_icon) + +class ArmBakeListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_bakelist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + scn = context.scene + scn.arm_bakelist.add() + scn.arm_bakelist_index = len(scn.arm_bakelist) - 1 + return{'FINISHED'} + + +class ArmBakeListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_bakelist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + scn = context.scene + return len(scn.arm_bakelist) > 0 + + def execute(self, context): + scn = context.scene + list = scn.arm_bakelist + index = scn.arm_bakelist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + scn.arm_bakelist_index = index + return{'FINISHED'} + +class ArmBakeListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "arm_bakelist.move_item" + bl_label = "Move an item in the list" + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + obj = bpy.context.scene + index = obj.arm_bakelist_index + list_length = len(obj.arm_bakelist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + obj.arm_bakelist.move(index, new_index) + obj.arm_bakelist_index = new_index + + def execute(self, context): + obj = bpy.context.scene + list = obj.arm_bakelist + index = obj.arm_bakelist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class ArmBakeButton(bpy.types.Operator): + '''Bake textures for listed objects''' + bl_idname = 'arm.bake_textures' + bl_label = 'Bake' + + def execute(self, context): + scn = context.scene + if len(scn.arm_bakelist) == 0: + return{'FINISHED'} + + self.report({'INFO'}, "Once baked, hit 'Armory Bake - Apply' to pack lightmaps") + + # At least one material required for now.. + for o in scn.arm_bakelist: + ob = o.obj + if len(ob.material_slots) == 0: + if not 'MaterialDefault' in bpy.data.materials: + mat = bpy.data.materials.new(name='MaterialDefault') + mat.use_nodes = True + else: + mat = bpy.data.materials['MaterialDefault'] + ob.data.materials.append(mat) + + # Single user materials + for o in scn.arm_bakelist: + ob = o.obj + for slot in ob.material_slots: + # Temp material already exists + if slot.material.name.endswith('_temp'): + continue + n = slot.material.name + '_' + ob.name + '_temp' + if not n in bpy.data.materials: + slot.material = slot.material.copy() + slot.material.name = n + + # Images for baking + for o in scn.arm_bakelist: + ob = o.obj + img_name = ob.name + '_baked' + sc = scn.arm_bakelist_scale / 100 + rx = o.res_x * sc + ry = o.res_y * sc + # Get image + if img_name not in bpy.data.images or bpy.data.images[img_name].size[0] != rx or bpy.data.images[img_name].size[1] != ry: + img = bpy.data.images.new(img_name, int(rx), int(ry)) + img.name = img_name # Force img_name (in case Blender picked img_name.001) + else: + img = bpy.data.images[img_name] + for slot in ob.material_slots: + # Add image nodes + mat = slot.material + mat.use_nodes = True + nodes = mat.node_tree.nodes + if 'Baked Image' in nodes: + img_node = nodes['Baked Image'] + else: + img_node = nodes.new('ShaderNodeTexImage') + img_node.name = 'Baked Image' + img_node.location = (100, 100) + img_node.image = img + img_node.select = True + nodes.active = img_node + + obs = bpy.context.view_layer.objects + + # Unwrap + active = obs.active + for o in scn.arm_bakelist: + ob = o.obj + uv_layers = ob.data.uv_layers + if not 'UVMap_baked' in uv_layers: + uvmap = uv_layers.new(name='UVMap_baked') + uv_layers.active_index = len(uv_layers) - 1 + obs.active = ob + if scn.arm_bakelist_unwrap == 'Lightmap Pack': + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + bpy.ops.uv.lightmap_pack(PREF_CONTEXT='ALL_FACES') + else: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = ob + ob.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.smart_project() + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + else: + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_baked': + uv_layers.active_index = i + break + obs.active = active + + # Materials for runtime + # TODO: use single mat per object + for o in scn.arm_bakelist: + ob = o.obj + img_name = ob.name + '_baked' + for slot in ob.material_slots: + n = slot.material.name[:-5] + '_baked' + if not n in bpy.data.materials: + mat = bpy.data.materials.new(name=n) + mat.use_nodes = True + mat.use_fake_user = True + nodes = mat.node_tree.nodes + img_node = nodes.new('ShaderNodeTexImage') + img_node.name = 'Baked Image' + img_node.location = (100, 100) + img_node.image = bpy.data.images[img_name] + mat.node_tree.links.new(img_node.outputs[0], nodes['Principled BSDF'].inputs[0]) + else: + mat = bpy.data.materials[n] + nodes = mat.node_tree.nodes + nodes['Baked Image'].image = bpy.data.images[img_name] + + # Bake + bpy.ops.object.select_all(action='DESELECT') + for o in scn.arm_bakelist: + o.obj.select_set(True) + obs.active = scn.arm_bakelist[0].obj + bpy.ops.object.bake('INVOKE_DEFAULT', type='COMBINED') + bpy.ops.object.select_all(action='DESELECT') + + return{'FINISHED'} + +class ArmBakeApplyButton(bpy.types.Operator): + '''Pack baked textures and restore materials''' + bl_idname = 'arm.bake_apply' + bl_label = 'Apply' + + def execute(self, context): + scn = context.scene + if len(scn.arm_bakelist) == 0: + return{'FINISHED'} + for material in bpy.data.materials: + if not material.users: + bpy.data.materials.remove(material) + + # Remove leftover _baked materials for removed objects + for mat in bpy.data.materials: + if mat.name.endswith('_baked'): + has_user = False + for ob in bpy.data.objects: + if ob.type == 'MESH' and mat.name.endswith('_' + ob.name + '_baked'): + has_user = True + break + if not has_user: + bpy.data.materials.remove(mat, do_unlink=True) + # Recache lightmaps + arm.assets.invalidate_unpacked_data(None, None) + for o in scn.arm_bakelist: + ob = o.obj + img_name = ob.name + '_baked' + # Save images + bpy.data.images[img_name].pack() + #bpy.data.images[img_name].save() + for slot in ob.material_slots: + mat = slot.material + # Remove temp material + if mat.name.endswith('_temp'): + old = slot.material + slot.material = bpy.data.materials[old.name.split('_' + ob.name)[0] + "_" + ob.name + "_baked"] + bpy.data.materials.remove(old, do_unlink=True) + # Restore uv slots + for o in scn.arm_bakelist: + ob = o.obj + uv_layers = ob.data.uv_layers + uv_layers.active_index = 0 + uv_layers["UVMap_baked"].active_render = True + + return{'FINISHED'} + +class ArmBakeSpecialsMenu(bpy.types.Menu): + bl_label = "Bake" + bl_idname = "ARM_MT_BakeListSpecials" + + def draw(self, context): + layout = self.layout + layout.operator("arm.bake_add_all") + layout.operator("arm.bake_add_selected") + layout.operator("arm.bake_clear_all") + layout.operator("arm.bake_remove_baked_materials") + +class ArmBakeAddAllButton(bpy.types.Operator): + '''Fill the list with scene objects''' + bl_idname = 'arm.bake_add_all' + bl_label = 'Add All' + + def execute(self, context): + scn = context.scene + scn.arm_bakelist.clear() + for ob in scn.objects: + if ob.type == 'MESH': + scn.arm_bakelist.add().obj = ob + return{'FINISHED'} + +class ArmBakeAddSelectedButton(bpy.types.Operator): + '''Add selected objects to the list''' + bl_idname = 'arm.bake_add_selected' + bl_label = 'Add Selected' + + def contains(self, scn, ob): + for o in scn.arm_bakelist: + if o == ob: + return True + return False + + def execute(self, context): + scn = context.scene + for ob in context.selected_objects: + if ob.type == 'MESH' and not self.contains(scn, ob): + scn.arm_bakelist.add().obj = ob + return{'FINISHED'} + +class ArmBakeClearAllButton(bpy.types.Operator): + '''Clear the list''' + bl_idname = 'arm.bake_clear_all' + bl_label = 'Clear' + + def execute(self, context): + scn = context.scene + scn.arm_bakelist.clear() + return{'FINISHED'} + +class ArmBakeRemoveBakedMaterialsButton(bpy.types.Operator): + '''Clear the list''' + bl_idname = 'arm.bake_remove_baked_materials' + bl_label = 'Remove Baked Materials' + + def execute(self, context): + for mat in bpy.data.materials: + if mat.name.endswith('_baked'): + bpy.data.materials.remove(mat, do_unlink=True) + return{'FINISHED'} + +def register(): + bpy.utils.register_class(ArmBakeListItem) + bpy.utils.register_class(ARM_UL_BakeList) + bpy.utils.register_class(ArmBakeListNewItem) + bpy.utils.register_class(ArmBakeListDeleteItem) + bpy.utils.register_class(ArmBakeListMoveItem) + bpy.utils.register_class(ArmBakeButton) + bpy.utils.register_class(ArmBakeApplyButton) + bpy.utils.register_class(ArmBakeSpecialsMenu) + bpy.utils.register_class(ArmBakeAddAllButton) + bpy.utils.register_class(ArmBakeAddSelectedButton) + bpy.utils.register_class(ArmBakeClearAllButton) + bpy.utils.register_class(ArmBakeRemoveBakedMaterialsButton) + bpy.types.Scene.arm_bakelist_scale = FloatProperty(name="Resolution", description="Resolution scale", default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0, subtype='PERCENTAGE') + bpy.types.Scene.arm_bakelist = CollectionProperty(type=ArmBakeListItem) + bpy.types.Scene.arm_bakelist_index = IntProperty(name="Index for my_list", default=0) + bpy.types.Scene.arm_bakelist_unwrap = EnumProperty( + items = [('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), + ('Smart UV Project', 'Smart UV Project', 'Smart UV Project')], + name = "UV Unwrap", default='Smart UV Project') + + + #Register lightmapper + bpy.types.Scene.arm_bakemode = EnumProperty( + items = [('Static Map', 'Static Map', 'Static Map'), + ('Lightmap', 'Lightmap', 'Lightmap')], + name = "Bake mode", default='Static Map') + + operators.register() + properties.register() + +def unregister(): + bpy.utils.unregister_class(ArmBakeListItem) + bpy.utils.unregister_class(ARM_UL_BakeList) + bpy.utils.unregister_class(ArmBakeListNewItem) + bpy.utils.unregister_class(ArmBakeListDeleteItem) + bpy.utils.unregister_class(ArmBakeListMoveItem) + bpy.utils.unregister_class(ArmBakeButton) + bpy.utils.unregister_class(ArmBakeApplyButton) + bpy.utils.unregister_class(ArmBakeSpecialsMenu) + bpy.utils.unregister_class(ArmBakeAddAllButton) + bpy.utils.unregister_class(ArmBakeAddSelectedButton) + bpy.utils.unregister_class(ArmBakeClearAllButton) + bpy.utils.unregister_class(ArmBakeRemoveBakedMaterialsButton) + + #Unregister lightmapper + + operators.unregister() + properties.unregister() \ No newline at end of file diff --git a/blender/arm/props_collision_filter_mask.py b/blender/arm/props_collision_filter_mask.py new file mode 100644 index 0000000000..f14c25aa97 --- /dev/null +++ b/blender/arm/props_collision_filter_mask.py @@ -0,0 +1,38 @@ +import bpy + + +class ARM_PT_RbCollisionFilterMaskPanel(bpy.types.Panel): + bl_label = "Collections Filter Mask" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "physics" + bl_parent_id = "ARM_PT_PhysicsPropsPanel" + + @classmethod + def poll(self, context): + obj = context.object + if obj is None: + return False + return obj.rigid_body is not None + + def draw(self, context): + layout = self.layout + layout.use_property_split = False + layout.use_property_decorate = False + obj = context.object + layout.prop(obj, 'arm_rb_collision_filter_mask', text="", expand=True) + col_mask = '' + for b in obj.arm_rb_collision_filter_mask: + col_mask = ('1' if b else '0') + col_mask + col = layout.column() + row = col.row() + row.alignment = 'RIGHT' + row.label(text=f'Integer Mask Value: {str(int(col_mask, 2))}') + + +def register(): + bpy.utils.register_class(ARM_PT_RbCollisionFilterMaskPanel) + + +def unregister(): + bpy.utils.unregister_class(ARM_PT_RbCollisionFilterMaskPanel) diff --git a/blender/arm/props_exporter.py b/blender/arm/props_exporter.py new file mode 100644 index 0000000000..f8620a8952 --- /dev/null +++ b/blender/arm/props_exporter.py @@ -0,0 +1,488 @@ +import os +import shutil +import stat +import subprocess +import webbrowser + +import bpy +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +import arm.assets as assets +import arm.utils +import arm.utils_vs + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + arm.utils = arm.reload_module(arm.utils) + arm.utils_vs = arm.reload_module(arm.utils_vs) +else: + arm.enable_reload(__name__) + + +def remove_readonly(func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + +def update_gapi_custom(self, context): + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_win(self, context): + if os.path.isdir(arm.utils.get_fp_build() + '/windows-build'): + shutil.rmtree(arm.utils.get_fp_build() + '/windows-build', onerror=remove_readonly) + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_linux(self, context): + if os.path.isdir(arm.utils.get_fp_build() + '/linux-build'): + shutil.rmtree(arm.utils.get_fp_build() + '/linux-build', onerror=remove_readonly) + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_mac(self, context): + if os.path.isdir(arm.utils.get_fp_build() + '/osx-build'): + shutil.rmtree(arm.utils.get_fp_build() + '/osx-build', onerror=remove_readonly) + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_android(self, context): + if os.path.isdir(arm.utils.get_fp_build() + '/android-build'): + shutil.rmtree(arm.utils.get_fp_build() + '/android-build', onerror=remove_readonly) + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_ios(self, context): + if os.path.isdir(arm.utils.get_fp_build() + '/ios-build'): + shutil.rmtree(arm.utils.get_fp_build() + '/ios-build', onerror=remove_readonly) + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +def update_gapi_html5(self, context): + bpy.data.worlds['Arm'].arm_recompile = True + assets.invalidate_compiled_data(self, context) + +class ArmExporterListItem(bpy.types.PropertyGroup): + name: StringProperty( + name="Name", + description="A name for this item", + default="Preset") + + arm_project_rp: StringProperty( + name="Render Path", + description="A name for this item", + default="") + + arm_project_scene: PointerProperty( + name="Scene", + description="Scene to load when launching", + type=bpy.types.Scene) + + arm_project_target: EnumProperty( + items = [('html5', 'HTML5 (JS)', 'html5'), + ('windows-hl', 'Windows (C)', 'windows-hl'), + ('krom-windows', 'Windows (Krom)', 'krom-windows'), + ('macos-hl', 'macOS (C)', 'macos-hl'), + ('krom-macos', 'macOS (Krom)', 'krom-macos'), + ('linux-hl', 'Linux (C)', 'linux-hl'), + ('krom-linux', 'Linux (Krom)', 'krom-linux'), + ('ios-hl', 'iOS (C)', 'ios-hl'), + ('android-hl', 'Android (C)', 'android-hl'), + ('node', 'Node (JS)', 'node'), + ('custom', 'Custom', 'custom'),], + name="Target", default='html5', description='Build platform') + + arm_project_khamake: StringProperty(name="Khamake", description="Specify arguments for the 'node Kha/make' call") + + arm_gapi_custom: EnumProperty( + items = [('opengl', 'OpenGL', 'opengl'), + ('vulkan', 'Vulkan', 'vulkan'), + ('direct3d11', 'Direct3D11', 'direct3d11'), + ('direct3d12', 'Direct3D12', 'direct3d12'), + ('metal', 'Metal', 'metal')], + name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_custom) + arm_gapi_win: EnumProperty( + items = [('direct3d11', 'Auto', 'direct3d11'), + ('opengl', 'OpenGL', 'opengl'), + ('vulkan', 'Vulkan', 'vulkan'), + ('direct3d11', 'Direct3D11', 'direct3d11'), + ('direct3d12', 'Direct3D12', 'direct3d12')], + name="Graphics API", default='direct3d11', description='Based on currently selected target', update=update_gapi_win) + arm_gapi_linux: EnumProperty( + items = [('opengl', 'Auto', 'opengl'), + ('opengl', 'OpenGL', 'opengl'), + ('vulkan', 'Vulkan', 'vulkan')], + name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_linux) + arm_gapi_android: EnumProperty( + items = [('opengl', 'Auto', 'opengl'), + ('opengl', 'OpenGL', 'opengl'), + ('vulkan', 'Vulkan', 'vulkan')], + name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_android) + arm_gapi_mac: EnumProperty( + items = [('opengl', 'Auto', 'opengl'), + ('opengl', 'OpenGL', 'opengl'), + ('metal', 'Metal', 'metal')], + name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_mac) + arm_gapi_ios: EnumProperty( + items = [('opengl', 'Auto', 'opengl'), + ('opengl', 'OpenGL', 'opengl'), + ('metal', 'Metal', 'metal')], + name="Graphics API", default='opengl', description='Based on currently selected target', update=update_gapi_ios) + arm_gapi_html5: EnumProperty( + items = [('webgl', 'Auto', 'webgl'), + ('webgl', 'WebGL2', 'webgl')], + name="Graphics API", default='webgl', description='Based on currently selected target', update=update_gapi_html5) + +class ArmExporterAndroidPermissionListItem(bpy.types.PropertyGroup): + arm_android_permissions: EnumProperty( + items = [('ACCESS_COARSE_LOCATION ', 'Access Coarse Location', 'Allows an app to access approximate location'), + ('ACCESS_NETWORK_STATE', 'Access Network State', 'Allows applications to access information about networks'), + ('ACCESS_FINE_LOCATION', 'Access Fine Location', 'Allows an app to access precise location'), + ('ACCESS_WIFI_STATE', 'Access Wi-Fi State', 'Allows applications to access information about Wi-Fi networks'), + ('BLUETOOTH', 'Bluetooth', 'Allows applications to connect to paired bluetooth devices'), + ('BLUETOOTH_ADMIN', 'Bluetooth Admin', 'Allows applications to discover and pair bluetooth devices'), + ('CAMERA', 'Camera', 'Required to be able to access the camera device'), + ('EXPAND_STATUS_BAR', 'Expand Status Bar', 'Allows an application to expand or collapse the status bar'), + ('FOREGROUND_SERVICE', 'Foreground Service', 'Allows a regular application to use Service.startForeground'), + ('GET_ACCOUNTS', 'Get Accounts', 'Allows access to the list of accounts in the Accounts Service'), + ('INTERNET', 'Internet', 'Allows applications to open network sockets'), + ('READ_EXTERNAL_STORAGE', 'Read External Storage', 'Allows an application to read from external storage.'), + ('VIBRATE', 'Vibrate', 'Allows access to the vibrator'), + ('WRITE_EXTERNAL_STORAGE', 'Write External Storage', 'Allows an application to write to external storage')], + name="Permission", default='VIBRATE', description='Android Permission') + +class ArmExporterAndroidAbiListItem(bpy.types.PropertyGroup): + arm_android_abi: EnumProperty( + items = [('arm64-v8a', 'arm64-v8a', 'This ABI is for ARMv8-A based CPUs, which support the 64-bit AArch64 architecture'), + ('armeabi-v7a', 'armeabi-v7a', 'This ABI is for 32-bit ARM-based CPUs'), + ('x86', 'x86', 'This ABI is for CPUs supporting the instruction set commonly known as x86, i386, or IA-32'), + ('x86_64', 'x86_64', 'This ABI is for CPUs supporting the instruction set commonly referred to as x86-64')], + name="Android ABI", default='arm64-v8a', description='Android ABI') + +class ARM_UL_ExporterList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'DOT' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=item.arm_project_target) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class ARM_UL_Exporter_AndroidPermissionList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'DOT' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=item.arm_android_permissions) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class ARM_UL_Exporter_AndroidAbiList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'DOT' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + col = row.column() + col.alignment = 'RIGHT' + col.label(text=item.arm_android_abi) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class ArmExporterListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_exporterlist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + mdata.arm_exporterlist.add() + mdata.arm_exporterlist_index = len(mdata.arm_exporterlist) - 1 + if len(mdata.arm_rplist) > mdata.arm_exporterlist_index: + mdata.arm_exporterlist[-1].arm_project_rp = mdata.arm_rplist[mdata.arm_rplist_index].name + mdata.arm_exporterlist[-1].arm_project_scene = context.scene + return{'FINISHED'} + +class ArmExporterListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_exporterlist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + mdata = bpy.data.worlds['Arm'] + return len(mdata.arm_exporterlist) > 0 + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_exporterlist + index = mdata.arm_exporterlist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + mdata.arm_exporterlist_index = index + return{'FINISHED'} + +class ArmExporterListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "arm_exporterlist.move_item" + bl_label = "Move an item in the list" + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + mdata = bpy.data.worlds['Arm'] + index = mdata.arm_exporterlist_index + list_length = len(mdata.arm_exporterlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + mdata.arm_exporterlist.move(index, new_index) + mdata.arm_exporterlist_index = new_index + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_exporterlist + index = mdata.arm_exporterlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class ArmExporter_AndroidPermissionListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_exporter_android_permission_list.new_item" + bl_label = "Add a new item" + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + mdata.arm_exporter_android_permission_list.add() + mdata.arm_exporter_android_permission_list_index = len(mdata.arm_exporter_android_permission_list) - 1 + return{'FINISHED'} + +class ArmExporter_AndroidPermissionListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_exporter_android_permission_list.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + mdata = bpy.data.worlds['Arm'] + return len(mdata.arm_exporter_android_permission_list) > 0 + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_exporter_android_permission_list + index = mdata.arm_exporter_android_permission_list_index + + list.remove(index) + + if index > 0: + index = index - 1 + + mdata.arm_exporter_android_permission_list_index = index + return{'FINISHED'} + +class ArmExporter_AndroidAbiListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_exporter_android_abi_list.new_item" + bl_label = "Add a new item" + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + mdata.arm_exporter_android_abi_list.add() + mdata.arm_exporter_android_abi_list_index = len(mdata.arm_exporter_android_abi_list) - 1 + return{'FINISHED'} + +class ArmExporter_AndroidAbiListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_exporter_android_abi_list.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + mdata = bpy.data.worlds['Arm'] + return len(mdata.arm_exporter_android_abi_list) > 0 + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_exporter_android_abi_list + index = mdata.arm_exporter_android_abi_list_index + + list.remove(index) + + if index > 0: + index = index - 1 + + mdata.arm_exporter_android_abi_list_index = index + return{'FINISHED'} + + +class ArmExporterSpecialsMenu(bpy.types.Menu): + bl_label = "More" + bl_idname = "ARM_MT_ExporterListSpecials" + + def draw(self, context): + layout = self.layout + layout.operator("arm.exporter_open_folder") + layout.operator("arm.exporter_gpuprofile") + if arm.utils.get_os_is_windows(): + layout.operator("arm.exporter_open_vs") + + +class ArmoryExporterOpenFolderButton(bpy.types.Operator): + """Open published folder""" + bl_idname = 'arm.exporter_open_folder' + bl_label = 'Open Folder' + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_exporterlist) == 0: + return {'CANCELLED'} + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + p = os.path.join(arm.utils.get_fp_build(), item.arm_project_target) + if os.path.exists(p): + webbrowser.open('file://' + p) + return{'FINISHED'} + +class ArmExporterGpuProfileButton(bpy.types.Operator): + '''GPU profile''' + bl_idname = 'arm.exporter_gpuprofile' + bl_label = 'Open in RenderDoc' + + def execute(self, context): + p = arm.utils.get_renderdoc_path() + if p == '': + self.report({'ERROR'}, 'Configure RenderDoc path in Armory add-on preferences') + return {'CANCELLED'} + pbin = '' + base = arm.utils.get_fp_build() + ext1 = '/krom-windows/' + arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_name) + '.exe' + ext2 = '/krom-linux/' + arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_name) + if os.path.exists(base + ext1): + pbin = base + ext1 + elif os.path.exists(base + ext2): + pbin = base + ext2 + if pbin == '': + self.report({'ERROR'}, 'Publish project using Krom target first') + return {'CANCELLED'} + subprocess.Popen([p, pbin]) + return{'FINISHED'} + + +class ARM_OT_ExporterOpenVS(bpy.types.Operator): + """Open the generated project in Visual Studio, if installed""" + bl_idname = 'arm.exporter_open_vs' + bl_label = 'Open in Visual Studio' + + @classmethod + def poll(cls, context): + if not arm.utils.get_os_is_windows(): + cls.poll_message_set('This operator is only supported on Windows') + return False + + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_exporterlist) == 0: + cls.poll_message_set('No export configuration exists') + return False + + if wrd.arm_exporterlist[wrd.arm_exporterlist_index].arm_project_target != 'windows-hl': + cls.poll_message_set('This operator only works with the Windows (C) target') + return False + + return True + + def execute(self, context): + version_major, version_min_full, err = arm.utils_vs.fetch_project_version() + if err is not None: + if err == 'err_file_not_found': + self.report({'ERROR'}, 'Publish project using Windows (C) target first') + elif err.startswith('err_invalid_version'): + self.report({'ERROR'}, 'Could not parse Visual Studio version, check console for details') + return {'CANCELLED'} + + success = arm.utils_vs.open_project_in_vs(version_major, version_min_full) + if not success: + self.report({'ERROR'}, 'Could not open the project in Visual Studio, check console for details') + return {'CANCELLED'} + + return{'FINISHED'} + + +REG_CLASSES = ( + ArmExporterListItem, + ArmExporterAndroidPermissionListItem, + ArmExporterAndroidAbiListItem, + ARM_UL_ExporterList, + ARM_UL_Exporter_AndroidPermissionList, + ARM_UL_Exporter_AndroidAbiList, + ArmExporterListNewItem, + ArmExporterListDeleteItem, + ArmExporterListMoveItem, + ArmExporter_AndroidPermissionListNewItem, + ArmExporter_AndroidPermissionListDeleteItem, + ArmExporter_AndroidAbiListNewItem, + ArmExporter_AndroidAbiListDeleteItem, + ArmExporterSpecialsMenu, + ArmExporterGpuProfileButton, + ArmoryExporterOpenFolderButton, + ARM_OT_ExporterOpenVS +) +_reg_classes, _unreg_classes = bpy.utils.register_classes_factory(REG_CLASSES) + + +def register(): + _reg_classes() + + bpy.types.World.arm_exporterlist = CollectionProperty(type=ArmExporterListItem) + bpy.types.World.arm_exporterlist_index = IntProperty(name="Index for my_list", default=0) + bpy.types.World.arm_exporter_android_permission_list = CollectionProperty(type=ArmExporterAndroidPermissionListItem) + bpy.types.World.arm_exporter_android_permission_list_index = IntProperty(name="Index for my_list", default=0) + bpy.types.World.arm_exporter_android_abi_list = CollectionProperty(type=ArmExporterAndroidAbiListItem) + bpy.types.World.arm_exporter_android_abi_list_index = IntProperty(name="Index for my_list", default=0) + + +def unregister(): + _unreg_classes() diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py new file mode 100644 index 0000000000..2ad67bf2f0 --- /dev/null +++ b/blender/arm/props_lod.py @@ -0,0 +1,157 @@ +import bpy +from bpy.props import * + +def update_size_prop(self, context): + if context.object == None: + return + mdata = context.object.data + i = mdata.arm_lodlist_index + ar = mdata.arm_lodlist + # Clamp screen size to not exceed previous entry + if i > 0 and ar[i - 1].screen_size_prop < self.screen_size_prop: + self.screen_size_prop = ar[i - 1].screen_size_prop + +class ArmLodListItem(bpy.types.PropertyGroup): + # Group of properties representing an item in the list + name: StringProperty( + name="Name", + description="A name for this item", + default="") + + enabled_prop: BoolProperty( + name="", + description="A name for this item", + default=True) + + screen_size_prop: FloatProperty( + name="Screen Size", + description="A name for this item", + min=0.0, + max=1.0, + default=0.0, + update=update_size_prop) + +class ARM_UL_LodList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.use_property_split = False + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") + name = item.name + if name == '': + name = 'None' + row.label(text=name, icon='OBJECT_DATAMODE') + col = row.column() + col.alignment = 'RIGHT' + col.label(text="{:.2f}".format(item.screen_size_prop)) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon='OBJECT_DATAMODE') + +class ArmLodListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_lodlist.new_item" + bl_label = "Add a new item" + bl_options = {'UNDO'} + + def execute(self, context): + mdata = bpy.context.object.data + mdata.arm_lodlist.add() + mdata.arm_lodlist_index = len(mdata.arm_lodlist) - 1 + return{'FINISHED'} + + +class ArmLodListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_lodlist.delete_item" + bl_label = "Deletes an item" + bl_options = {'INTERNAL', 'UNDO'} + + @classmethod + def poll(cls, context): + """ Enable if there's something in the list """ + if bpy.context.object is None: + return False + mdata = bpy.context.object.data + return len(mdata.arm_lodlist) > 0 + + def execute(self, context): + mdata = bpy.context.object.data + lodlist = mdata.arm_lodlist + index = mdata.arm_lodlist_index + + n = lodlist[index].name + if n in context.scene.collection.objects: + obj = bpy.data.objects[n] + context.scene.collection.objects.unlink(obj) + + lodlist.remove(index) + + if index > 0: + index = index - 1 + + mdata.arm_lodlist_index = index + return{'FINISHED'} + +class ArmLodListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "arm_lodlist.move_item" + bl_label = "Move an item in the list" + bl_options = {'INTERNAL', 'UNDO'} + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + mdata = bpy.context.object.data + index = mdata.arm_lodlist_index + list_length = len(mdata.arm_lodlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + mdata.arm_lodlist.move(index, new_index) + mdata.arm_lodlist_index = new_index + + def execute(self, context): + mdata = bpy.context.object.data + list = mdata.arm_lodlist + index = mdata.arm_lodlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +def register(): + bpy.utils.register_class(ArmLodListItem) + bpy.utils.register_class(ARM_UL_LodList) + bpy.utils.register_class(ArmLodListNewItem) + bpy.utils.register_class(ArmLodListDeleteItem) + bpy.utils.register_class(ArmLodListMoveItem) + + bpy.types.Mesh.arm_lodlist = CollectionProperty(type=ArmLodListItem) + bpy.types.Mesh.arm_lodlist_index = IntProperty(name="Index for my_list", default=0) + bpy.types.Mesh.arm_lod_material = BoolProperty(name="Material Lod", description="Use materials of lod objects", default=False) + +def unregister(): + bpy.utils.unregister_class(ArmLodListItem) + bpy.utils.unregister_class(ARM_UL_LodList) + bpy.utils.unregister_class(ArmLodListNewItem) + bpy.utils.unregister_class(ArmLodListDeleteItem) + bpy.utils.unregister_class(ArmLodListMoveItem) diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py new file mode 100644 index 0000000000..510e10eb5f --- /dev/null +++ b/blender/arm/props_properties.py @@ -0,0 +1,158 @@ +import bpy +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +class ArmPropertyListItem(bpy.types.PropertyGroup): + type_prop: EnumProperty( + items = [('string', 'String', 'String'), + ('integer', 'Integer', 'Integer'), + ('float', 'Float', 'Float'), + ('boolean', 'Boolean', 'Boolean'), + ], + name = "Type") + name_prop: StringProperty(name="Name", description="A name for this item", default="my_prop") + string_prop: StringProperty(name="String", description="A name for this item", default="text") + integer_prop: IntProperty(name="Integer", description="A name for this item", default=0) + float_prop: FloatProperty(name="Float", description="A name for this item", default=0.0) + boolean_prop: BoolProperty(name="Boolean", description="A name for this item", default=False) + +class ARM_UL_PropertyList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.use_property_split = False + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "name_prop", text="", emboss=False, icon="OBJECT_DATAMODE") + layout.prop(item, item.type_prop + "_prop", text="", emboss=(item.type_prop == 'boolean')) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon="OBJECT_DATAMODE") + +class ArmPropertyListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_propertylist.new_item" + bl_label = "New" + + type_prop: EnumProperty( + items = [('string', 'String', 'String'), + ('integer', 'Integer', 'Integer'), + ('float', 'Float', 'Float'), + ('boolean', 'Boolean', 'Boolean'), + ], + name = "Type") + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self,context): + layout = self.layout + layout.prop(self, "type_prop", expand=True) + + def execute(self, context): + obj = bpy.context.object + prop = obj.arm_propertylist.add() + prop.type_prop = self.type_prop + obj.arm_propertylist_index = len(obj.arm_propertylist) - 1 + return{'FINISHED'} + +class ArmPropertyListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_propertylist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + obj = bpy.context.object + if obj == None: + return False + return len(obj.arm_propertylist) > 0 + + def execute(self, context): + obj = bpy.context.object + lst = obj.arm_propertylist + index = obj.arm_propertylist_index + + if len(lst) <= index: + return{'FINISHED'} + + lst.remove(index) + + if index > 0: + index = index - 1 + + obj.arm_propertylist_index = index + return{'FINISHED'} + +class ArmPropertyListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "arm_propertylist.move_item" + bl_label = "Move an item in the list" + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + obj = bpy.context.object + index = obj.arm_propertylist_index + list_length = len(obj.arm_propertylist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + obj.arm_propertylist.move(index, new_index) + obj.arm_propertylist_index = new_index + + def execute(self, context): + obj = bpy.context.object + list = obj.arm_propertylist + index = obj.arm_propertylist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +def draw_properties(layout, obj): + layout.label(text="Properties") + rows = 2 + if len(obj.arm_traitlist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_PropertyList", "The_List", obj, "arm_propertylist", obj, "arm_propertylist_index", rows=rows) + col = row.column(align=True) + op = col.operator("arm_propertylist.new_item", icon='ADD', text="") + op = col.operator("arm_propertylist.delete_item", icon='REMOVE', text="") + if len(obj.arm_propertylist) > 1: + col.separator() + op = col.operator("arm_propertylist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_propertylist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + +def register(): + bpy.utils.register_class(ArmPropertyListItem) + bpy.utils.register_class(ARM_UL_PropertyList) + bpy.utils.register_class(ArmPropertyListNewItem) + bpy.utils.register_class(ArmPropertyListDeleteItem) + bpy.utils.register_class(ArmPropertyListMoveItem) + bpy.types.Object.arm_propertylist = CollectionProperty(type=ArmPropertyListItem) + bpy.types.Object.arm_propertylist_index = IntProperty(name="Index for arm_propertylist", default=0) + +def unregister(): + bpy.utils.unregister_class(ArmPropertyListItem) + bpy.utils.unregister_class(ARM_UL_PropertyList) + bpy.utils.unregister_class(ArmPropertyListNewItem) + bpy.utils.unregister_class(ArmPropertyListDeleteItem) + bpy.utils.unregister_class(ArmPropertyListMoveItem) diff --git a/blender/arm/props_renderpath.py b/blender/arm/props_renderpath.py new file mode 100644 index 0000000000..1c06f4363e --- /dev/null +++ b/blender/arm/props_renderpath.py @@ -0,0 +1,757 @@ +from typing import Optional + +import bpy +from bpy.props import * + +import arm.assets as assets +import arm.utils + +if arm.is_reload(__name__): + assets = arm.reload_module(assets) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + +atlas_sizes = [ ('256', '256', '256'), + ('512', '512', '512'), + ('1024', '1024', '1024'), + ('2048', '2048', '2048'), + ('4096', '4096', '4096'), + ('8192', '8192', '8192'), + ('16384', '16384', '16384'), + ('32768', '32768', '32768') ] + +def atlas_sizes_from_min(min_size: int) -> list: + """ Create an enum list of atlas sizes from a minimal size """ + sizes = [] + for i in range(len(atlas_sizes)): + if int(atlas_sizes[i][0]) > min_size: + sizes.append(atlas_sizes[i]) + return sizes + +def update_spot_sun_atlas_size_options(scene: bpy.types.Scene, context: bpy.types.Context) -> list: + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return [] + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + return atlas_sizes_from_min(int(rpdat.rp_shadowmap_cascade)) + +def update_point_atlas_size_options(scene: bpy.types.Scene, context: bpy.types.Context) -> list: + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return [] + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + return atlas_sizes_from_min(int(rpdat.rp_shadowmap_cube) * 2) + + +def update_preset(self, context): + rpdat = self.arm_rplist[-1] + + if self.rp_preset == 'Desktop': + rpdat.rp_renderer = 'Deferred' + rpdat.arm_material_model = 'Full' + rpdat.rp_shadows = True + rpdat.rp_shadowmap_cube = '512' + rpdat.rp_shadowmap_cascade = '1024' + rpdat.rp_shadowmap_cascades = '4' + rpdat.rp_translucency_state = 'Auto' + rpdat.rp_overlays_state = 'Auto' + rpdat.rp_decals_state = 'Auto' + rpdat.rp_sss_state = 'Auto' + rpdat.rp_blending_state = 'Auto' + rpdat.rp_depth_texture_state = 'Auto' + rpdat.rp_draw_order = 'Auto' + rpdat.rp_hdr = True + rpdat.rp_background = 'World' + rpdat.rp_stereo = False + rpdat.rp_voxels = 'Off' + rpdat.rp_render_to_texture = True + rpdat.rp_supersampling = '1' + rpdat.rp_antialiasing = 'SMAA' + rpdat.rp_compositornodes = True + rpdat.rp_volumetriclight = False + rpdat.rp_ssgi = 'SSAO' + rpdat.arm_ssrs = False + rpdat.arm_micro_shadowing = False + rpdat.rp_ssr = False + rpdat.rp_bloom = False + rpdat.arm_bloom_quality = 'medium' + rpdat.arm_bloom_anti_flicker = True + rpdat.rp_autoexposure = False + rpdat.rp_motionblur = 'Off' + rpdat.arm_rp_resolution = 'Display' + rpdat.arm_texture_filter = 'Anisotropic' + rpdat.arm_irradiance = True + rpdat.arm_radiance = True + rpdat.rp_pp = False + elif self.rp_preset == 'Mobile': + rpdat.rp_renderer = 'Forward' + rpdat.rp_depthprepass = False + rpdat.arm_material_model = 'Mobile' + rpdat.rp_shadows = True + rpdat.rp_shadowmap_cube = '256' + rpdat.rp_shadowmap_cascade = '1024' + rpdat.rp_shadowmap_cascades = '1' + rpdat.rp_translucency_state = 'Off' + rpdat.rp_overlays_state = 'Off' + rpdat.rp_decals_state = 'Off' + rpdat.rp_sss_state = 'Off' + rpdat.rp_blending_state = 'Off' + rpdat.rp_depth_texture_state = 'Auto' + rpdat.rp_draw_order = 'Auto' + rpdat.rp_hdr = False + rpdat.rp_background = 'Clear' + rpdat.rp_stereo = False + rpdat.rp_voxels = 'Off' + rpdat.rp_render_to_texture = False + rpdat.rp_supersampling = '1' + rpdat.rp_antialiasing = 'Off' + rpdat.rp_compositornodes = False + rpdat.rp_volumetriclight = False + rpdat.rp_ssgi = 'Off' + rpdat.arm_ssrs = False + rpdat.arm_micro_shadowing = False + rpdat.rp_ssr = False + rpdat.rp_bloom = False + rpdat.arm_bloom_quality = 'low' + rpdat.arm_bloom_anti_flicker = False + rpdat.rp_autoexposure = False + rpdat.rp_motionblur = 'Off' + rpdat.arm_rp_resolution = 'Display' + rpdat.arm_texture_filter = 'Linear' + rpdat.arm_irradiance = True + rpdat.arm_radiance = False + rpdat.rp_pp = False + elif self.rp_preset == 'Max': + rpdat.rp_renderer = 'Deferred' + rpdat.rp_shadows = True + rpdat.rp_shadowmap_cube = '2048' + rpdat.rp_shadowmap_cascade = '4096' + rpdat.rp_shadowmap_cascades = '4' + rpdat.rp_translucency_state = 'Auto' + rpdat.rp_overlays_state = 'Auto' + rpdat.rp_decals_state = 'Auto' + rpdat.rp_sss_state = 'Auto' + rpdat.rp_blending_state = 'Auto' + rpdat.rp_depth_texture_state = 'Auto' + rpdat.rp_draw_order = 'Auto' + rpdat.rp_hdr = True + rpdat.rp_background = 'World' + rpdat.rp_stereo = False + rpdat.rp_voxels = True + rpdat.rp_voxelgi_resolution = '128' + rpdat.arm_voxelgi_revoxelize = False + rpdat.arm_voxelgi_camera = False + rpdat.rp_voxelgi_emission = False + rpdat.rp_render_to_texture = True + rpdat.rp_supersampling = '1' + rpdat.rp_antialiasing = 'TAA' + rpdat.rp_compositornodes = True + rpdat.rp_volumetriclight = False + rpdat.rp_ssgi = 'RTAO' + rpdat.arm_ssrs = False + rpdat.arm_micro_shadowing = True + rpdat.rp_ssr = True + rpdat.rp_ss_refraction = True + rpdat.arm_ssr_half_res = False + rpdat.rp_bloom = True + rpdat.arm_bloom_quality = 'high' + rpdat.arm_bloom_anti_flicker = True + rpdat.rp_autoexposure = False + rpdat.rp_motionblur = 'Off' + rpdat.arm_rp_resolution = 'Display' + rpdat.arm_material_model = 'Full' + rpdat.arm_texture_filter = 'Anisotropic' + rpdat.arm_irradiance = True + rpdat.arm_radiance = True + rpdat.rp_pp = False + elif self.rp_preset == '2D/Baked': + rpdat.rp_renderer = 'Forward' + rpdat.rp_depthprepass = False + rpdat.arm_material_model = 'Solid' + rpdat.rp_shadows = False + rpdat.rp_shadowmap_cube = '512' + rpdat.rp_shadowmap_cascade = '1024' + rpdat.rp_shadowmap_cascades = '1' + rpdat.rp_translucency_state = 'Off' + rpdat.rp_overlays_state = 'Off' + rpdat.rp_decals_state = 'Off' + rpdat.rp_sss_state = 'Off' + rpdat.rp_blending_state = 'Off' + rpdat.rp_depth_texture_state = 'Off' + rpdat.rp_draw_order = 'Auto' + rpdat.rp_hdr = False + rpdat.rp_background = 'Clear' + rpdat.rp_stereo = False + rpdat.rp_voxels = 'Off' + rpdat.rp_render_to_texture = False + rpdat.rp_supersampling = '1' + rpdat.rp_antialiasing = 'Off' + rpdat.rp_compositornodes = False + rpdat.rp_volumetriclight = False + rpdat.rp_ssgi = 'Off' + rpdat.arm_ssrs = False + rpdat.arm_micro_shadowing = False + rpdat.rp_ssr = False + rpdat.rp_bloom = False + rpdat.arm_bloom_quality = 'low' + rpdat.arm_bloom_anti_flicker = False + rpdat.rp_autoexposure = False + rpdat.rp_motionblur = 'Off' + rpdat.arm_rp_resolution = 'Display' + rpdat.arm_texture_filter = 'Linear' + rpdat.arm_irradiance = False + rpdat.arm_radiance = False + rpdat.rp_pp = False + update_renderpath(self, context) + +def update_renderpath(self, context): + if not assets.invalidate_enabled: + return + assets.invalidate_shader_cache(self, context) + bpy.data.worlds['Arm'].arm_recompile = True + +def udpate_shadowmap_cascades(self, context): + bpy.data.worlds['Arm'].arm_recompile = True + update_renderpath(self, context) + +def update_material_model(self, context): + assets.invalidate_shader_cache(self, context) + update_renderpath(self, context) + +def update_translucency_state(self, context): + if self.rp_translucency_state == 'On': + self.rp_translucency = True + elif self.rp_translucency_state == 'Off': + self.rp_translucency = False + else: # Auto - updates rp at build time if translucent mat is used + return + update_renderpath(self, context) + +def update_decals_state(self, context): + if self.rp_decals_state == 'On': + self.rp_decals = True + elif self.rp_decals_state == 'Off': + self.rp_decals = False + else: # Auto - updates rp at build time if decal mat is used + return + update_renderpath(self, context) + +def update_overlays_state(self, context): + if self.rp_overlays_state == 'On': + self.rp_overlays = True + elif self.rp_overlays_state == 'Off': + self.rp_overlays = False + else: # Auto - updates rp at build time if x-ray mat is used + return + update_renderpath(self, context) + +def update_blending_state(self, context): + if self.rp_blending_state == 'On': + self.rp_blending = True + elif self.rp_blending_state == 'Off': + self.rp_blending = False + else: # Auto - updates rp at build time if blending mat is used + return + update_renderpath(self, context) + + +def update_depth_texture_state(self, context): + if self.rp_depth_texture_state == 'On': + self.rp_depth_texture = True + elif self.rp_depth_texture_state == 'Off': + self.rp_depth_texture = False + else: # Auto - updates rp at build time if depth texture mat is used + return + update_renderpath(self, context) + + +def update_sss_state(self, context): + if self.rp_sss_state == 'On': + self.rp_sss = True + elif self.rp_sss_state == 'Off': + self.rp_sss = False + else: # Auto - updates rp at build time if sss mat is used + return + update_renderpath(self, context) + +class ArmRPListItem(bpy.types.PropertyGroup): + name: StringProperty( + name="Name", + description="A name for this item", + default="Desktop") + + rp_driver: StringProperty(name="Driver", default="Armory", update=assets.invalidate_compiled_data) + rp_renderer: EnumProperty( + items=[('Forward', 'Forward Clustered', 'Forward'), + ('Deferred', 'Deferred Clustered', 'Deferred'), + # ('Raytracer', 'Raytracer', 'Raytracer (Direct3D 12)', 'ERROR', 2), + ], + name="Renderer", description="Renderer type", default='Deferred', update=update_renderpath) + rp_depthprepass: BoolProperty(name="Depth Prepass", description="Depth Prepass for mesh context", default=False, update=update_renderpath) + rp_hdr: BoolProperty(name="HDR", description="Render in HDR Space", default=True, update=update_renderpath) + rp_render_to_texture: BoolProperty(name="Post Process", description="Render scene to texture for further processing", default=True, update=update_renderpath) + rp_background: EnumProperty( + items=[('World', 'World', 'World'), + ('Clear', 'Clear', 'Clear'), + ('Off', 'No Clear', 'Off'), + ], + name="Background", description="Background type", default='World', update=update_renderpath) + arm_irradiance: BoolProperty(name="Irradiance", description="Generate spherical harmonics", default=True, update=assets.invalidate_shader_cache) + arm_radiance: BoolProperty(name="Radiance", description="Generate radiance textures", default=True, update=assets.invalidate_shader_cache) + arm_radiance_size: EnumProperty( + items=[('512', '512', '512'), + ('1024', '1024', '1024'), + ('2048', '2048', '2048')], + name="Map Size", description="Prefiltered map size", default='1024', update=assets.invalidate_envmap_data) + rp_autoexposure: BoolProperty(name="Auto Exposure", description="Adjust exposure based on luminance", default=False, update=update_renderpath) + rp_compositornodes: BoolProperty(name="Compositor", description="Draw compositor nodes", default=True, update=update_renderpath) + rp_shadows: BoolProperty(name="Shadows", description="Enable shadow casting", default=True, update=update_renderpath) + rp_max_lights: EnumProperty( + items=[('4', '4', '4'), + ('8', '8', '8'), + ('16', '16', '16'), + ('24', '24', '24'), + ('32', '32', '32'), + ('64', '64', '64'),], + name="Max Lights", description="Max number of lights that can be visible in the screen", default='16') + rp_max_lights_cluster: EnumProperty( + items=[('4', '4', '4'), + ('8', '8', '8'), + ('16', '16', '16'), + ('24', '24', '24'), + ('32', '32', '32'), + ('64', '64', '64'),], + name="Max Lights Shadows", description="Max number of rendered shadow maps that can be visible in the screen. Always equal or lower than Max Lights", default='16') + rp_shadowmap_atlas: BoolProperty(name="Shadow Map Atlasing", description="Group shadow maps of lights of the same type in the same texture", default=False, update=update_renderpath) + rp_shadowmap_atlas_single_map: BoolProperty(name="Shadow Map Atlas single map", description="Use a single texture for all different light types.", default=False, update=update_renderpath) + rp_shadowmap_atlas_lod: BoolProperty(name="Shadow Map Atlas LOD (Experimental)", description="When enabled, the size of the shadow map will be determined on runtime based on the distance of the light to the camera", default=False, update=update_renderpath) + rp_shadowmap_atlas_lod_subdivisions: EnumProperty( + items=[('2', '2', '2'), + ('3', '3', '3'), + ('4', '4', '4'), + ('5', '5', '5'), + ('6', '6', '6'), + ('7', '7', '7'), + ('8', '8', '8'),], + name="LOD Subdivisions", description="Number of subdivisions of the default tile size for LOD", default='2', update=update_renderpath) + rp_shadowmap_atlas_max_size_point: EnumProperty( + items=update_point_atlas_size_options, + name="Max Atlas Texture Size Points", description="Sets the limit of the size of the texture.", update=update_renderpath) + rp_shadowmap_atlas_max_size_spot: EnumProperty( + items=update_spot_sun_atlas_size_options, + name="Max Atlas Texture Size Spots", description="Sets the limit of the size of the texture.", update=update_renderpath) + rp_shadowmap_atlas_max_size_sun: EnumProperty( + items=update_spot_sun_atlas_size_options, + name="Max Atlas Texture Size Sun", description="Sets the limit of the size of the texture.", update=update_renderpath) + rp_shadowmap_atlas_max_size: EnumProperty( + items=update_spot_sun_atlas_size_options, + name="Max Atlas Texture Size", description="Sets the limit of the size of the texture.", update=update_renderpath) + rp_shadowmap_cube: EnumProperty( + items=[('256', '256', '256'), + ('512', '512', '512'), + ('1024', '1024', '1024'), + ('2048', '2048', '2048'), + ('4096', '4096', '4096'),], + name="Cube Size", description="Cube map resolution", default='512', update=update_renderpath) + rp_shadowmap_cascade: EnumProperty( + items=[('256', '256', '256'), + ('512', '512', '512'), + ('1024', '1024', '1024'), + ('2048', '2048', '2048'), + ('4096', '4096', '4096'), + ('8192', '8192', '8192'), + ('16384', '16384', '16384'),], + name="Cascade Size", description="Shadow map resolution", default='1024', update=update_renderpath) + rp_shadowmap_cascades: EnumProperty( + items=[('1', '1', '1'), + ('2', '2', '2'), + ('4', '4', '4')], + name="Cascades", description="Shadow map cascades", default='4', update=udpate_shadowmap_cascades) + arm_pcfsize: FloatProperty(name="PCF Size", description="Filter size", default=1.0) + rp_supersampling: EnumProperty( + items=[('1', '1', '1'), + ('1.5', '1.5', '1.5'), + ('2', '2', '2'), + ('4', '4', '4')], + name="Super Sampling", description="Screen resolution multiplier", default='1', update=update_renderpath) + rp_antialiasing: EnumProperty( + items=[('Off', 'No AA', 'Off'), + ('FXAA', 'FXAA', 'FXAA'), + ('SMAA', 'SMAA', 'SMAA'), + ('TAA', 'TAA', 'TAA')], + name="Anti Aliasing", description="Post-process anti aliasing technique", default='SMAA', update=update_renderpath) + rp_volumetriclight: BoolProperty(name="Volumetric Light", description="Use volumetric lighting", default=False, update=update_renderpath) + rp_ssr: BoolProperty(name="SSR", description="Screen space reflections", default=False, update=update_renderpath) + rp_ss_refraction: BoolProperty(name="SSRefraction", description="Screen space refractions", default=False, update=update_renderpath) + rp_ssgi: EnumProperty( + items=[('Off', 'No AO', 'Off'), + ('SSAO', 'SSAO', 'Screen space ambient occlusion'), + ('RTAO', 'RTAO', 'Ray-traced ambient occlusion'), + ], + name="SSGI", description="Screen space global illumination", default='SSAO', update=update_renderpath) + rp_bloom: BoolProperty(name="Bloom", description="Bloom processing", default=False, update=update_renderpath) + arm_bloom_follow_blender: BoolProperty(name="Use Blender Settings", description="Use Blender settings instead of Armory settings", default=True) + rp_motionblur: EnumProperty( + items=[('Off', 'Off', 'Off'), + ('Camera', 'Camera', 'Camera'), + ('Object', 'Object', 'Object')], + name="Motion Blur", description="Velocity buffer is used for object based motion blur", default='Off', update=update_renderpath) + rp_translucency: BoolProperty(name="Translucency", description="Current render-path state", default=False) + rp_translucency_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name="Translucency", description="Order independent translucency", default='Auto', update=update_translucency_state) + rp_decals: BoolProperty(name="Decals", description="Current render-path state", default=False) + rp_decals_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name="Decals", description="Decals pass", default='Auto', update=update_decals_state) + rp_overlays: BoolProperty(name="Overlays", description="Current render-path state", default=False) + rp_overlays_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name="Overlays", description="X-Ray pass", default='Auto', update=update_overlays_state) + rp_sss: BoolProperty(name="SSS", description="Current render-path state", default=False) + rp_sss_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name="SSS", description="Sub-surface scattering pass", default='Auto', update=update_sss_state) + rp_blending: BoolProperty(name="Blending", description="Current render-path state", default=False) + rp_blending_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name="Blending", description="Blending pass", default='Auto', update=update_blending_state) + rp_draw_order: EnumProperty( + items=[('Auto', 'Auto', 'Auto'), + ('Distance', 'Distance', 'Distance'), + ('Shader', 'Shader', 'Shader')], + name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data) + rp_depth_texture: BoolProperty(name="Depth Texture", description="Current render-path state", default=False) + rp_depth_texture_state: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off'), + ('Auto', 'Auto', 'Auto')], + name='Depth Texture', description='Whether materials can read from a depth texture', default='Auto', update=update_depth_texture_state) + rp_stereo: BoolProperty(name="VR", description="Stereo rendering", default=False, update=update_renderpath) + rp_water: BoolProperty(name="Water", description="Enable water surface pass", default=False, update=update_renderpath) + rp_pp: BoolProperty(name="Realtime postprocess", description="Realtime postprocess", default=False, update=update_renderpath) + rp_voxels: BoolProperty(name="Voxel AO", description="Ambient occlusion", default=False, update=update_renderpath) + rp_voxelgi_resolution: EnumProperty( + items=[('32', '32', '32'), + ('64', '64', '64'), + ('128', '128', '128'), + ('256', '256', '256'), + ('512', '512', '512')], + name="Resolution", description="3D texture resolution", default='128', update=update_renderpath) + rp_voxelgi_resolution_z: EnumProperty( + items=[('2.0', '2.0', '2.0'), + ('1.0', '1.0', '1.0'), + ('0.5', '0.5', '0.5'), + ('0.25', '0.25', '0.25')], + name="Resolution Z", description="3D texture z resolution multiplier", default='1.0', update=update_renderpath) + arm_clouds: BoolProperty(name="Clouds", description="Enable clouds pass", default=False, update=assets.invalidate_shader_cache) + arm_ssrs: BoolProperty(name="SSRS", description="Screen-space ray-traced shadows", default=False, update=assets.invalidate_shader_cache) + arm_micro_shadowing: BoolProperty(name="Micro Shadowing", description="Use the shaders' occlusion parameter to compute micro shadowing for the scene's sun lamp. This option is not available for render paths using mobile or solid material models", default=False, update=assets.invalidate_shader_cache) + arm_texture_filter: EnumProperty( + items=[('Anisotropic', 'Anisotropic', 'Anisotropic'), + ('Linear', 'Linear', 'Linear'), + ('Point', 'Closest', 'Point'), + ('Manual', 'Manual', 'Manual')], + name="Texture Filtering", description="Set Manual to honor interpolation setting on Image Texture node", default='Anisotropic') + arm_material_model: EnumProperty( + items=[('Full', 'Full', 'Full'), + ('Mobile', 'Mobile', 'Mobile'), + ('Solid', 'Solid', 'Solid'), + ], + name="Materials", description="Material builder", default='Full', update=update_material_model) + arm_rp_displacement: EnumProperty( + items=[('Off', 'Off', 'Off'), + ('Vertex', 'Vertex', 'Vertex'), + ('Tessellation', 'Tessellation', 'Tessellation')], + name="Displacement", description="Enable material displacement", default='Vertex', update=assets.invalidate_shader_cache) + arm_tess_mesh_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7) + arm_tess_mesh_outer: IntProperty(name="Outer", description="Outer tessellation level", default=7) + arm_tess_shadows_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7) + arm_tess_shadows_outer: IntProperty(name="Outer", description="Outer tessellation level", default=7) + arm_rp_resolution: EnumProperty( + items=[('Display', 'Display', 'Display'), + ('Custom', 'Custom', 'Custom')], + name="Resolution", description="Resolution to perform rendering at", default='Display', update=update_renderpath) + arm_rp_resolution_size: IntProperty(name="Size", description="Resolution height in pixels(for example 720p), width is auto-fit to preserve aspect ratio", default=720, min=0, update=update_renderpath) + arm_rp_resolution_filter: EnumProperty( + items=[('Linear', 'Linear', 'Linear'), + ('Point', 'Closest', 'Point')], + name="Filter", description="Scaling filter", default='Linear') + rp_dynres: BoolProperty(name="Dynamic Resolution", description="Dynamic resolution scaling for performance", default=False, update=update_renderpath) + rp_chromatic_aberration: BoolProperty(name="Chromatic Aberration", description="Add chromatic aberration (scene fringe)", default=False, update=assets.invalidate_shader_cache) + arm_ssr_half_res: BoolProperty(name="Half Res", description="Trace in half resolution", default=False, update=update_renderpath) + arm_voxelgi_dimensions: FloatProperty(name="Dimensions", description="Voxelization bounds",default=16, update=assets.invalidate_compiled_data) + arm_voxelgi_revoxelize: BoolProperty(name="Revoxelize", description="Revoxelize scene each frame", default=False, update=assets.invalidate_shader_cache) + arm_voxelgi_temporal: BoolProperty(name="Temporal Filter", description="Use temporal filtering to stabilize voxels", default=False, update=assets.invalidate_shader_cache) + arm_voxelgi_camera: BoolProperty(name="Dynamic Camera", description="Use camera as voxelization origin", default=False, update=assets.invalidate_shader_cache) + arm_voxelgi_shadows: BoolProperty(name="Shadows", description="Use voxels to render shadows", default=False, update=update_renderpath) + arm_samples_per_pixel: EnumProperty( + items=[('1', '1', '1'), + ('2', '2', '2'), + ('4', '4', '4'), + ('8', '8', '8'), + ('16', '16', '16')], + name="MSAA", description="Samples per pixel usable for render paths drawing directly to framebuffer", default='1') + + arm_voxelgi_cones: EnumProperty( + items=[('9', '9', '9'), + ('5', '5', '5'), + ('3', '3', '3'), + ('1', '1', '1'), + ], + name="Cones", description="Number of cones to trace", default='5', update=assets.invalidate_shader_cache) + arm_voxelgi_occ: FloatProperty(name="Occlusion", description="", default=1.0, update=assets.invalidate_shader_cache) + arm_voxelgi_env: FloatProperty(name="Env Map", description="Contribute light from environment map", default=0.0, update=assets.invalidate_shader_cache) + arm_voxelgi_step: FloatProperty(name="Step", description="Step size", default=1.0, update=assets.invalidate_shader_cache) + arm_voxelgi_offset: FloatProperty(name="Offset", description="Ray offset", default=1.0, update=assets.invalidate_shader_cache) + arm_voxelgi_range: FloatProperty(name="Range", description="Maximum range", default=2.0, update=assets.invalidate_shader_cache) + arm_voxelgi_aperture: FloatProperty(name="Aperture", description="Cone aperture for shadow trace", default=1.0, update=assets.invalidate_shader_cache) + arm_sss_width: FloatProperty(name="Width", description="SSS blur strength", default=1.0, update=assets.invalidate_shader_cache) + arm_water_color: FloatVectorProperty(name="Color", size=3, default=[1, 1, 1], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) + arm_water_level: FloatProperty(name="Level", default=0.0, update=assets.invalidate_shader_cache) + arm_water_displace: FloatProperty(name="Displace", default=1.0, update=assets.invalidate_shader_cache) + arm_water_speed: FloatProperty(name="Speed", default=1.0, update=assets.invalidate_shader_cache) + arm_water_freq: FloatProperty(name="Freq", default=1.0, update=assets.invalidate_shader_cache) + arm_water_density: FloatProperty(name="Density", default=1.0, update=assets.invalidate_shader_cache) + arm_water_refract: FloatProperty(name="Refract", default=1.0, update=assets.invalidate_shader_cache) + arm_water_reflect: FloatProperty(name="Reflect", default=1.0, update=assets.invalidate_shader_cache) + arm_ssgi_strength: FloatProperty(name="Strength", default=1.0, update=assets.invalidate_shader_cache) + arm_ssgi_radius: FloatProperty(name="Radius", default=1.0, update=assets.invalidate_shader_cache) + arm_ssgi_step: FloatProperty(name="Step", default=2.0, update=assets.invalidate_shader_cache) + arm_ssgi_max_steps: IntProperty(name="Max Steps", default=8, update=assets.invalidate_shader_cache) + arm_ssgi_rays: EnumProperty( + items=[('9', '9', '9'), + ('5', '5', '5'), + ], + name="Rays", description="Number of rays to trace for RTAO", default='5', update=assets.invalidate_shader_cache) + arm_ssgi_half_res: BoolProperty(name="Half Res", description="Trace in half resolution", default=False, update=assets.invalidate_shader_cache) + arm_bloom_threshold: FloatProperty(name="Threshold", description="Brightness above which a pixel is contributing to the bloom effect", min=0, default=0.8, update=assets.invalidate_shader_cache) + arm_bloom_knee: FloatProperty(name="Knee", description="Smoothen transition around the threshold (higher values = smoother transition)", min=0, max=1, default=0.5, update=assets.invalidate_shader_cache) + arm_bloom_strength: FloatProperty(name="Strength", description="Strength of the bloom effect", min=0, default=0.05, update=assets.invalidate_shader_cache) + arm_bloom_radius: FloatProperty(name="Radius", description="Glow radius (screen-size independent)", min=0, default=6.5, update=assets.invalidate_shader_cache) + arm_bloom_anti_flicker: BoolProperty(name="Anti-Flicker Filter", description="Apply a filter to reduce flickering caused by fireflies (single very bright pixels)", default=True, update=assets.invalidate_shader_cache) + arm_bloom_quality: EnumProperty( + name="Quality", + description="Resampling quality of the bloom pass", + items=[ + ("low", "Low", "Lowest visual quality but best performance"), + ("medium", "Medium", "Compromise between quality and performance"), + ("high", "High", "Best quality, but slowest") + ], + default="medium", + update=assets.invalidate_shader_cache + ) + arm_motion_blur_intensity: FloatProperty(name="Intensity", default=1.0, update=assets.invalidate_shader_cache) + arm_ssr_ray_step: FloatProperty(name="Step", default=0.03, update=assets.invalidate_shader_cache) + arm_ssr_search_dist: FloatProperty(name="Search", default=5.0, update=assets.invalidate_shader_cache) + arm_ssr_falloff_exp: FloatProperty(name="Falloff", default=5.0, update=assets.invalidate_shader_cache) + arm_ssr_jitter: FloatProperty(name="Jitter", default=0.6, update=assets.invalidate_shader_cache) + arm_ss_refraction_ray_step: FloatProperty(name="Step", default=0.25, update=assets.invalidate_shader_cache) + arm_ss_refraction_search_dist: FloatProperty(name="Search", default=5.0, update=assets.invalidate_shader_cache) + arm_ss_refraction_falloff_exp: FloatProperty(name="Falloff", default=5.0, update=assets.invalidate_shader_cache) + arm_ss_refraction_jitter: FloatProperty(name="Jitter", default=0.6, update=assets.invalidate_shader_cache) + arm_volumetric_light_air_turbidity: FloatProperty(name="Air Turbidity", default=1.0, update=assets.invalidate_shader_cache) + arm_volumetric_light_air_color: FloatVectorProperty(name="Air Color", size=3, default=[1.0, 1.0, 1.0], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) + arm_volumetric_light_steps: IntProperty(name="Steps", default=20, min=0, update=assets.invalidate_shader_cache) + arm_shadowmap_split: FloatProperty(name="Cascade Split", description="Split factor for cascaded shadow maps, higher factor favors detail on close surfaces", default=0.8, update=assets.invalidate_shader_cache) + arm_shadowmap_bounds: FloatProperty(name="Cascade Bounds", description="Multiply cascade bounds to capture bigger area", default=1.0, update=assets.invalidate_compiled_data) + arm_autoexposure_strength: FloatProperty(name="Auto Exposure Strength", default=1.0, update=assets.invalidate_shader_cache) + arm_autoexposure_speed: FloatProperty(name="Auto Exposure Speed", default=1.0, update=assets.invalidate_shader_cache) + arm_ssrs_ray_step: FloatProperty(name="Step", default=0.01, update=assets.invalidate_shader_cache) + arm_chromatic_aberration_type: EnumProperty( + items=[('Simple', 'Simple', 'Simple'), + ('Spectral', 'Spectral', 'Spectral'), + ], + name="Aberration type", description="Aberration type", default='Simple', update=assets.invalidate_shader_cache) + arm_chromatic_aberration_strength: FloatProperty(name="Strength", default=2.00, update=assets.invalidate_shader_cache) + arm_chromatic_aberration_samples: IntProperty(name="Samples", default=32, min=8, update=assets.invalidate_shader_cache) + # Compositor + arm_letterbox: BoolProperty(name="Letterbox", default=False, update=assets.invalidate_shader_cache) + arm_letterbox_color: FloatVectorProperty(name="Color", size=3, default=[0, 0, 0], subtype='COLOR', min=0, max=1, update=assets.invalidate_shader_cache) + arm_letterbox_size: FloatProperty(name="Size", default=0.1, update=assets.invalidate_shader_cache) + arm_distort: BoolProperty(name="Distort", default=False, update=assets.invalidate_shader_cache) + arm_distort_strength: FloatProperty(name="Strength", default=2.0, update=assets.invalidate_shader_cache) + arm_grain: BoolProperty(name="Film Grain", default=False, update=assets.invalidate_shader_cache) + arm_grain_strength: FloatProperty(name="Strength", default=2.0, update=assets.invalidate_shader_cache) + arm_sharpen: BoolProperty(name="Sharpen", default=False, update=assets.invalidate_shader_cache) + arm_sharpen_strength: FloatProperty(name="Strength", default=0.25, update=assets.invalidate_shader_cache) + arm_fog: BoolProperty(name="Volumetric Fog", default=False, update=assets.invalidate_shader_cache) + arm_fog_color: FloatVectorProperty(name="Color", size=3, subtype='COLOR', default=[0.5, 0.6, 0.7], min=0, max=1, update=assets.invalidate_shader_cache) + arm_fog_amounta: FloatProperty(name="Amount A", default=0.25, update=assets.invalidate_shader_cache) + arm_fog_amountb: FloatProperty(name="Amount B", default=0.5, update=assets.invalidate_shader_cache) + arm_tonemap: EnumProperty( + items=[('Off', 'Off', 'Off'), + ('Filmic', 'Filmic', 'Filmic'), + ('Filmic2', 'Filmic2', 'Filmic2'), + ('Reinhard', 'Reinhard', 'Reinhard'), + ('Uncharted', 'Uncharted', 'Uncharted')], + name='Tonemap', description='Tonemapping operator', default='Filmic', update=assets.invalidate_shader_cache) + arm_fisheye: BoolProperty(name="Fish Eye", default=False, update=assets.invalidate_shader_cache) + arm_vignette: BoolProperty(name="Vignette", default=False, update=assets.invalidate_shader_cache) + arm_vignette_strength: FloatProperty(name="Strength", default=0.7, update=assets.invalidate_shader_cache) + arm_lensflare: BoolProperty(name="Lens Flare", default=False, update=assets.invalidate_shader_cache) + arm_lens: BoolProperty(name="Lens Texture", description="Grime Overlay", default=False, update=assets.invalidate_shader_cache) + arm_lens_texture: StringProperty(name="Texture", description="Lens filepath", default="lenstexture.jpg", update=assets.invalidate_shader_cache) + arm_lens_texture_masking: BoolProperty(name="Luminance Masking", description="Luminance masking", default=False, update=assets.invalidate_shader_cache) + arm_lens_texture_masking_centerMinClip : FloatProperty(name="Center Min Clip", default=0.5, update=assets.invalidate_shader_cache) + arm_lens_texture_masking_centerMaxClip : FloatProperty(name="Center Max Clip", default=0.1, update=assets.invalidate_shader_cache) + arm_lens_texture_masking_luminanceMax : FloatProperty(name="Luminance Min", default=0.1, update=assets.invalidate_shader_cache) + arm_lens_texture_masking_luminanceMin : FloatProperty(name="Luminance Max", default=2.5, update=assets.invalidate_shader_cache) + arm_lens_texture_masking_brightnessExp : FloatProperty(name="Brightness Exponent", default=2.0, update=assets.invalidate_shader_cache) + arm_lut: BoolProperty(name="LUT Colorgrading", description="Colorgrading", default=False, update=assets.invalidate_shader_cache) + arm_lut_texture: StringProperty(name="Texture", description="LUT filepath", default="luttexture.jpg", update=assets.invalidate_shader_cache) + arm_skin: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off')], + name='Skinning', description='Enable skinning', default='On', update=assets.invalidate_shader_cache) + arm_use_armature_deform_only: BoolProperty(name="Only Deform Bones", description="Only write deforming bones (and non-deforming ones when they have deforming children)", default=False, update=assets.invalidate_compiled_data) + arm_skin_max_bones_auto: BoolProperty(name="Auto Bones", description="Calculate amount of maximum bones based on armatures", default=True, update=assets.invalidate_compiled_data) + arm_skin_max_bones: IntProperty(name="Max Bones", default=50, min=1, max=3000, update=assets.invalidate_shader_cache) + arm_morph_target: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off')], + name='Shape key', description='Enable shape keys', default='On', update=assets.invalidate_shader_cache) + arm_particles: EnumProperty( + items=[('On', 'On', 'On'), + ('Off', 'Off', 'Off')], + name='Particles', description='Enable particle simulation', default='On', update=assets.invalidate_shader_cache) + # Material override flags + arm_culling: BoolProperty(name="Culling", default=True) + arm_two_sided_area_light: BoolProperty(name="Two-Sided Area Light", description="Emit light from both faces of area plane", default=False, update=assets.invalidate_shader_cache) + + @staticmethod + def get_by_name(name: str) -> Optional['ArmRPListItem']: + wrd = bpy.data.worlds['Arm'] + # Assume unique rp names + for i in range(len(wrd.arm_rplist)): + if wrd.arm_rplist[i].name == name: + return wrd.arm_rplist[i] + return None + + +class ARM_UL_RPList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + custom_icon = 'OBJECT_DATAMODE' + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class ArmRPListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_rplist.new_item" + bl_label = "New" + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self,context): + layout = self.layout + layout.prop(bpy.data.worlds['Arm'], 'rp_preset', expand=True) + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + wrd.arm_rplist.add() + wrd.arm_rplist_index = len(wrd.arm_rplist) - 1 + wrd.arm_rplist[wrd.arm_rplist_index].name = bpy.data.worlds['Arm'].rp_preset + update_preset(wrd, context) + return{'FINISHED'} + +class ArmRPListDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "arm_rplist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + mdata = bpy.data.worlds['Arm'] + return len(mdata.arm_rplist) > 0 + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_rplist + index = mdata.arm_rplist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + mdata.arm_rplist_index = index + return{'FINISHED'} + +class ArmRPListMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "arm_rplist.move_item" + bl_label = "Move an item in the list" + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + def move_index(self): + # Move index of an item render queue while clamping it + mdata = bpy.data.worlds['Arm'] + index = mdata.arm_rplist_index + list_length = len(mdata.arm_rplist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + mdata.arm_rplist.move(index, new_index) + mdata.arm_rplist_index = new_index + + def execute(self, context): + mdata = bpy.data.worlds['Arm'] + list = mdata.arm_rplist + index = mdata.arm_rplist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +def register(): + bpy.utils.register_class(ArmRPListItem) + bpy.utils.register_class(ARM_UL_RPList) + bpy.utils.register_class(ArmRPListNewItem) + bpy.utils.register_class(ArmRPListDeleteItem) + bpy.utils.register_class(ArmRPListMoveItem) + + bpy.types.World.arm_rplist = CollectionProperty(type=ArmRPListItem) + bpy.types.World.rp_driver_list = CollectionProperty(type=bpy.types.PropertyGroup) + bpy.types.World.arm_rplist_index = IntProperty(name="Index for my_list", default=0, update=update_renderpath) + +def unregister(): + bpy.utils.unregister_class(ArmRPListItem) + bpy.utils.unregister_class(ARM_UL_RPList) + bpy.utils.unregister_class(ArmRPListNewItem) + bpy.utils.unregister_class(ArmRPListDeleteItem) + bpy.utils.unregister_class(ArmRPListMoveItem) diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py new file mode 100644 index 0000000000..1f7d53b6db --- /dev/null +++ b/blender/arm/props_tilesheet.py @@ -0,0 +1,282 @@ +import bpy +from bpy.props import * + +class ArmTilesheetActionListItem(bpy.types.PropertyGroup): + name: StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + start_prop: IntProperty( + name="Start", + description="A name for this item", + default=0) + + end_prop: IntProperty( + name="End", + description="A name for this item", + default=0) + + loop_prop: BoolProperty( + name="Loop", + description="A name for this item", + default=True) + +class ARM_UL_TilesheetActionList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon = custom_icon) + +class ArmTilesheetActionListNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "arm_tilesheetactionlist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + trait.arm_tilesheetactionlist.add() + trait.arm_tilesheetactionlist_index = len(trait.arm_tilesheetactionlist) - 1 + return{'FINISHED'} + +class ArmTilesheetActionListDeleteItem(bpy.types.Operator): + """Delete the selected item from the list""" + bl_idname = "arm_tilesheetactionlist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """Enable if there's something in the list""" + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + return len(trait.arm_tilesheetactionlist) > 0 + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + list = trait.arm_tilesheetactionlist + index = trait.arm_tilesheetactionlist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + trait.arm_tilesheetactionlist_index = index + return{'FINISHED'} + +class ArmTilesheetActionListMoveItem(bpy.types.Operator): + """Move an item in the list""" + bl_idname = "arm_tilesheetactionlist.move_item" + bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) + + @classmethod + def poll(self, context): + """Enable if there's something in the list""" + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + return len(trait.arm_tilesheetactionlist) > 0 + + def move_index(self): + # Move index of an item render queue while clamping it + wrd = bpy.data.worlds['Arm'] + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + index = trait.arm_tilesheetactionlist_index + list_length = len(trait.arm_tilesheetactionlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + trait.arm_tilesheetactionlist.move(index, new_index) + trait.arm_tilesheetactionlist_index = new_index + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + list = trait.arm_tilesheetactionlist + index = trait.arm_tilesheetactionlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class ArmTilesheetListItem(bpy.types.PropertyGroup): + name: StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + tilesx_prop: IntProperty( + name="Tiles X", + description="A name for this item", + default=0) + + tilesy_prop: IntProperty( + name="Tiles Y", + description="A name for this item", + default=0) + + framerate_prop: FloatProperty( + name="Frame Rate", + description="A name for this item", + default=4.0) + + arm_tilesheetactionlist: CollectionProperty(type=ArmTilesheetActionListItem) + arm_tilesheetactionlist_index: IntProperty(name="Index for arm_tilesheetactionlist", default=0) + +class ARM_UL_TilesheetList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon=custom_icon) + +class ArmTilesheetListNewItem(bpy.types.Operator): + """Add a new item to the list""" + bl_idname = "arm_tilesheetlist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + wrd.arm_tilesheetlist.add() + wrd.arm_tilesheetlist_index = len(wrd.arm_tilesheetlist) - 1 + return{'FINISHED'} + +class ArmTilesheetListDeleteItem(bpy.types.Operator): + """Delete the selected item from the list""" + bl_idname = "arm_tilesheetlist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + wrd = bpy.data.worlds['Arm'] + return len(wrd.arm_tilesheetlist) > 0 + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + list = wrd.arm_tilesheetlist + index = wrd.arm_tilesheetlist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + wrd.arm_tilesheetlist_index = index + return{'FINISHED'} + +class ArmTilesheetListMoveItem(bpy.types.Operator): + """Move an item in the list""" + bl_idname = "arm_tilesheetlist.move_item" + bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list. """ + wrd = bpy.data.worlds['Arm'] + return len(wrd.arm_tilesheetlist) > 0 + + def move_index(self): + # Move index of an item render queue while clamping it + wrd = bpy.data.worlds['Arm'] + index = wrd.arm_tilesheetlist_index + list_length = len(wrd.arm_tilesheetlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + wrd.arm_tilesheetlist.move(index, new_index) + wrd.arm_tilesheetlist_index = new_index + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + list = wrd.arm_tilesheetlist + index = wrd.arm_tilesheetlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +def register(): + bpy.utils.register_class(ArmTilesheetActionListItem) + bpy.utils.register_class(ARM_UL_TilesheetActionList) + bpy.utils.register_class(ArmTilesheetActionListNewItem) + bpy.utils.register_class(ArmTilesheetActionListDeleteItem) + bpy.utils.register_class(ArmTilesheetActionListMoveItem) + + bpy.utils.register_class(ArmTilesheetListItem) + bpy.utils.register_class(ARM_UL_TilesheetList) + bpy.utils.register_class(ArmTilesheetListNewItem) + bpy.utils.register_class(ArmTilesheetListDeleteItem) + bpy.utils.register_class(ArmTilesheetListMoveItem) + + bpy.types.World.arm_tilesheetlist = CollectionProperty(type=ArmTilesheetListItem) + bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for arm_tilesheetlist", default=0) + +def unregister(): + bpy.utils.unregister_class(ArmTilesheetListItem) + bpy.utils.unregister_class(ARM_UL_TilesheetList) + bpy.utils.unregister_class(ArmTilesheetListNewItem) + bpy.utils.unregister_class(ArmTilesheetListDeleteItem) + bpy.utils.unregister_class(ArmTilesheetListMoveItem) + + bpy.utils.unregister_class(ArmTilesheetActionListItem) + bpy.utils.unregister_class(ARM_UL_TilesheetActionList) + bpy.utils.unregister_class(ArmTilesheetActionListNewItem) + bpy.utils.unregister_class(ArmTilesheetActionListDeleteItem) + bpy.utils.unregister_class(ArmTilesheetActionListMoveItem) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py new file mode 100644 index 0000000000..566ca5a455 --- /dev/null +++ b/blender/arm/props_traits.py @@ -0,0 +1,1012 @@ +import json +import os +import shutil +import subprocess +from typing import Union +import webbrowser + +from bpy.types import Menu, NodeTree +from bpy.props import * +import bpy.utils.previews + +import arm.make as make +from arm.props_traits_props import * +import arm.ui_icons as ui_icons +import arm.utils +import arm.write_data as write_data + +if arm.is_reload(__name__): + arm.make = arm.reload_module(arm.make) + arm.props_traits_props = arm.reload_module(arm.props_traits_props) + from arm.props_traits_props import * + ui_icons = arm.reload_module(ui_icons) + arm.utils = arm.reload_module(arm.utils) + arm.write_data = arm.reload_module(arm.write_data) +else: + arm.enable_reload(__name__) + +ICON_HAXE = ui_icons.get_id('haxe') +ICON_NODES = 'NODETREE' +ICON_CANVAS = 'NODE_COMPOSITING' +ICON_BUNDLED = ui_icons.get_id('bundle') +ICON_WASM = ui_icons.get_id('wasm') + +# Pay attention to the ID number parameter for backward compatibility! +# This is important if the enum is reordered or the string identifier +# is changed as the number is what's stored in the blend file +PROP_TYPES_ENUM = [ + ('Haxe Script', 'Haxe', 'Haxe script', ICON_HAXE, 0), + ('Logic Nodes', 'Nodes', 'Logic nodes (visual scripting)', ICON_NODES, 4), + ('UI Canvas', 'UI', 'User interface', ICON_CANVAS, 2), + ('Bundled Script', 'Bundled', 'Premade script with common functionality', ICON_BUNDLED, 3), + ('WebAssembly', 'Wasm', 'WebAssembly', ICON_WASM, 1) +] + +def trigger_recompile(self, context): + wrd = bpy.data.worlds['Arm'] + wrd.arm_recompile = True + +def update_trait_group(self, context): + o = context.object if self.is_object else context.scene + if o == None: + return + i = o.arm_traitlist_index + if i >= 0 and i < len(o.arm_traitlist): + t = o.arm_traitlist[i] + if t.type_prop == 'Haxe Script' or t.type_prop == 'Bundled Script': + t.name = t.class_name_prop + elif t.type_prop == 'WebAssembly': + t.name = t.webassembly_prop + elif t.type_prop == 'UI Canvas': + t.name = t.canvas_name_prop + elif t.type_prop == 'Logic Nodes': + if t.node_tree_prop != None: + t.name = t.node_tree_prop.name + # Fetch props + if t.type_prop == 'Bundled Script' and t.name != '': + file_path = arm.utils.get_sdk_path() + '/armory/Sources/armory/trait/' + t.name + '.hx' + if os.path.exists(file_path): + arm.utils.fetch_script_props(file_path) + arm.utils.fetch_prop(o) + # Show trait users as collections + if self.is_object: + for col in bpy.data.collections: + if col.name.startswith('Trait|') and o.name in col.objects: + col.objects.unlink(o) + for t in o.arm_traitlist: + if 'Trait|' + t.name not in bpy.data.collections: + col = bpy.data.collections.new('Trait|' + t.name) + else: + col = bpy.data.collections['Trait|' + t.name] + try: + col.objects.link(o) + except RuntimeError: + # Object is already in that collection. This can + # happen when multiple same traits are copied with + # bpy.ops.arm.copy_traits_to_active + pass + +class ArmTraitListItem(bpy.types.PropertyGroup): + def poll_node_trees(self, tree: NodeTree): + """Ensure that only logic node trees show up as node traits""" + return tree.bl_idname == 'ArmLogicTreeType' + + name: StringProperty(name="Name", description="The name of the trait", default="", override={"LIBRARY_OVERRIDABLE"}) + enabled_prop: BoolProperty(name="", description="Whether this trait is enabled", default=True, update=trigger_recompile, override={"LIBRARY_OVERRIDABLE"}) + is_object: BoolProperty(name="", default=True) + fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False, override={"LIBRARY_OVERRIDABLE"}) + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) + + class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}, poll=poll_node_trees) + + arm_traitpropslist: CollectionProperty(type=ArmTraitPropListItem) + arm_traitpropslist_index: IntProperty(name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + arm_traitpropswarnings: CollectionProperty(type=ArmTraitPropWarning) + +class ARM_UL_TraitList(bpy.types.UIList): + """List of traits.""" + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.use_property_split = False + + custom_icon = "NONE" + custom_icon_value = 0 + if item.type_prop == "Haxe Script": + custom_icon_value = ICON_HAXE + elif item.type_prop == "WebAssembly": + custom_icon_value = ICON_WASM + elif item.type_prop == "UI Canvas": + custom_icon = "NODE_COMPOSITING" + elif item.type_prop == "Bundled Script": + custom_icon_value = ICON_BUNDLED + elif item.type_prop == "Logic Nodes": + custom_icon = 'NODETREE' + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") + # Display " " for props without a name to right-align the + # fake_user button + row.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) + + row = layout.row(align=True) + row.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") + +class ArmTraitListNewItem(bpy.types.Operator): + bl_idname = "arm_traitlist.new_item" + bl_label = "Add Trait" + bl_description = "Add a new trait item to the list" + + is_object: BoolProperty(name="Is Object Trait", description="Whether this trait belongs to an object or a scene", default=False) + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) + + # Show more options when invoked from the operator search menu + invoked_by_search: BoolProperty(name="", default=True) + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self, width=400) + + def draw(self, context): + layout = self.layout + + if self.invoked_by_search: + row = layout.row() + row.prop(self, "is_object") + + row = layout.row() + row.scale_y = 1.3 + row.prop(self, "type_prop", expand=True) + + def execute(self, context): + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + trait = obj.arm_traitlist.add() + trait.is_object = self.is_object + trait.type_prop = self.type_prop + obj.arm_traitlist_index = len(obj.arm_traitlist) - 1 + trigger_recompile(None, None) + return{'FINISHED'} + +class ArmTraitListDeleteItem(bpy.types.Operator): + """Delete the selected item from the list""" + bl_idname = "arm_traitlist.delete_item" + bl_label = "Remove Trait" + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="", description="A name for this item", default=False) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + obj = bpy.context.object + if obj is None: + return False + return len(obj.arm_traitlist) > 0 + + def execute(self, context): + obj = bpy.context.object + lst = obj.arm_traitlist + index = obj.arm_traitlist_index + + if len(lst) <= index: + return {'FINISHED'} + + try: + lst.remove(index) + except TypeError as e: + if obj.override_library is not None: + return {'CANCELLED'} + else: + raise e + + update_trait_group(self, context) + + if index > 0: + index = index - 1 + + obj.arm_traitlist_index = index + + return {'FINISHED'} + +class ArmTraitListDeleteItemScene(bpy.types.Operator): + """Delete the selected item from the list""" + bl_idname = "arm_traitlist.delete_item_scene" + bl_label = "Deletes an item" + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="", description="A name for this item", default=False) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + obj = bpy.context.scene + if obj == None: + return False + return len(obj.arm_traitlist) > 0 + + def execute(self, context): + obj = bpy.context.scene + lst = obj.arm_traitlist + index = obj.arm_traitlist_index + + if len(lst) <= index: + return{'FINISHED'} + + lst.remove(index) + + if index > 0: + index = index - 1 + + obj.arm_traitlist_index = index + return{'FINISHED'} + +class ArmTraitListMoveItem(bpy.types.Operator): + """Move an item in the list""" + bl_idname = "arm_traitlist.move_item" + bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + + direction: EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + is_object: BoolProperty(name="", description="A name for this item", default=False) + + def move_index(self): + # Move index of an item render queue while clamping it + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + index = obj.arm_traitlist_index + list_length = len(obj.arm_traitlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + obj.arm_traitlist.move(index, new_index) + obj.arm_traitlist_index = new_index + + def execute(self, context): + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + list = obj.arm_traitlist + index = obj.arm_traitlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +class ArmEditScriptButton(bpy.types.Operator): + bl_idname = 'arm.edit_script' + bl_label = 'Edit Script' + bl_description = 'Edit script in the text editor' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="", description="A name for this item", default=False) + + def execute(self, context): + + arm.utils.check_default_props() + + if not os.path.exists(os.path.join(arm.utils.get_fp(), "khafile.js")): + print('Generating Krom project for IDE build configuration') + make.build('krom') + + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + + item = obj.arm_traitlist[obj.arm_traitlist_index] + pkg = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + # Replace the haxe package syntax with the os-dependent path syntax for opening + hx_path = os.path.join(arm.utils.get_fp(), 'Sources', pkg, item.class_name_prop.replace('.', os.sep) + '.hx') + arm.utils.open_editor(hx_path) + return{'FINISHED'} + +class ArmEditBundledScriptButton(bpy.types.Operator): + bl_idname = 'arm.edit_bundled_script' + bl_label = 'Edit Script' + bl_description = 'Copy script to project and edit in the text editor' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(default=False) + + def execute(self, context): + if not arm.utils.check_saved(self): + return {'CANCELLED'} + + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + sdk_path = arm.utils.get_sdk_path() + project_path = arm.utils.get_fp() + item = obj.arm_traitlist[obj.arm_traitlist_index] + + pkg = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_package) + source_hx_path = os.path.join(sdk_path, 'armory', 'Sources', 'armory', 'trait', item.class_name_prop + '.hx') + target_dir = os.path.join(project_path, 'Sources', pkg) + target_hx_path = os.path.join(target_dir, item.class_name_prop + '.hx') + + if not os.path.isfile(target_hx_path): + if not os.path.exists(target_dir): + os.makedirs(target_dir) + + # Rewrite package and copy + with open(source_hx_path, encoding="utf-8") as sf: + sf.readline() + with open(target_hx_path, 'w', encoding="utf-8") as tf: + tf.write('package ' + pkg + ';\n') + shutil.copyfileobj(sf, tf) + + arm.utils.fetch_script_names() + + # From bundled to script + item.type_prop = 'Haxe Script' + + # Open the trait in the code editor + bpy.ops.arm.edit_script('EXEC_DEFAULT', is_object=self.is_object) + + return{'FINISHED'} + +class ArmEditWasmScriptButton(bpy.types.Operator): + bl_idname = 'arm.edit_wasm_script' + bl_label = 'Edit Script' + bl_description = 'Copy script to project and edit in the text editor' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(default=False) + + def execute(self, context): + if not arm.utils.check_saved(self): + return {'CANCELLED'} + + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + + item = obj.arm_traitlist[obj.arm_traitlist_index] + wasm_path = os.path.join(arm.utils.get_fp(), 'Bundled', item.webassembly_prop + '.wasm') + arm.utils.open_editor(wasm_path) + return{'FINISHED'} + +class ArmoryGenerateNavmeshButton(bpy.types.Operator): + """Generate navmesh from selected meshes""" + bl_idname = 'arm.generate_navmesh' + bl_label = 'Generate Navmesh' + + def execute(self, context): + obj = context.active_object + + if obj.type != 'MESH': + return{'CANCELLED'} + + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + depsgraph = bpy.context.evaluated_depsgraph_get() + armature = obj.find_armature() + apply_modifiers = not armature + + obj_eval = obj.evaluated_get(depsgraph) if apply_modifiers else obj + export_mesh = obj_eval.to_mesh() + # TODO: build tilecache here + print("Started visualization generation") + # For visualization + nav_full_path = arm.utils.get_fp_build() + '/compiled/Assets/navigation' + if not os.path.exists(nav_full_path): + os.makedirs(nav_full_path) + + nav_mesh_name = 'nav_' + obj_eval.data.name + mesh_path = nav_full_path + '/' + nav_mesh_name + '.obj' + + with open(mesh_path, 'w') as f: + for v in export_mesh.vertices: + f.write("v %.4f " % (v.co[0] * obj_eval.scale.x)) + f.write("%.4f " % (v.co[2] * obj_eval.scale.z)) + f.write("%.4f\n" % (v.co[1] * obj_eval.scale.y)) # Flipped + for p in export_mesh.polygons: + f.write("f") + for i in reversed(p.vertices): # Flipped normals + f.write(" %d" % (i + 1)) + f.write("\n") + + buildnavjs_path = arm.utils.get_sdk_path() + '/lib/haxerecast/buildnavjs' + + # append config values + nav_config = {} + for trait in obj.arm_traitlist: + # check if trait is navmesh here + if trait.arm_traitpropslist and trait.class_name_prop == 'NavMesh': + for prop in trait.arm_traitpropslist: # Append props + name = prop.name + value = prop.get_value() + nav_config[name] = value + nav_config_json = json.dumps(nav_config) + + args = [arm.utils.get_node_path(), buildnavjs_path, nav_mesh_name, nav_config_json] + proc = subprocess.Popen(args, cwd=nav_full_path) + proc.wait() + + navmesh = bpy.ops.import_scene.obj(filepath=mesh_path) + navmesh = bpy.context.selected_objects[0] + + navmesh.name = nav_mesh_name + navmesh.rotation_euler = (0, 0, 0) + navmesh.location = (obj.location.x, obj.location.y, obj.location.z) + navmesh.arm_export = False + + bpy.context.view_layer.objects.active = navmesh + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles() + bpy.ops.object.editmode_toggle() + + obj_eval.to_mesh_clear() + + print("Finished visualization generation") + + return{'FINISHED'} + +class ArmEditCanvasButton(bpy.types.Operator): + bl_idname = 'arm.edit_canvas' + bl_label = 'Edit Canvas' + bl_description = 'Edit UI Canvas' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="", description="A name for this item", default=False) + + def execute(self, context): + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + project_path = arm.utils.get_fp() + item = obj.arm_traitlist[obj.arm_traitlist_index] + canvas_path = project_path + '/Bundled/canvas/' + item.canvas_name_prop + '.json' + sdk_path = arm.utils.get_sdk_path() + ext = 'd3d11' if arm.utils.get_os() == 'win' else 'opengl' + armory2d_path = sdk_path + '/lib/armory_tools/armory2d/' + ext + krom_location, krom_path = arm.utils.krom_paths() + os.chdir(krom_location) + cpath = canvas_path.replace('\\', '/') + uiscale = str(arm.utils.get_ui_scale()) + cmd = [krom_path, armory2d_path, armory2d_path, cpath, uiscale] + if arm.utils.get_os() == 'win': + cmd.append('--consolepid') + cmd.append(str(os.getpid())) + subprocess.Popen(cmd) + return{'FINISHED'} + +class ArmNewScriptDialog(bpy.types.Operator): + bl_idname = "arm.new_script" + bl_label = "New Script" + bl_description = 'Create a blank script' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="Object trait", description="Is this an object trait?", default=False) + class_name: StringProperty(name="Name", description="The class name") + + def execute(self, context): + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + self.class_name = self.class_name.replace(' ', '') + write_data.write_traithx(self.class_name) + arm.utils.fetch_script_names() + item = obj.arm_traitlist[obj.arm_traitlist_index] + item.class_name_prop = self.class_name + return {'FINISHED'} + + def invoke(self, context, event): + if not arm.utils.check_saved(self): + return {'CANCELLED'} + self.class_name = 'MyTrait' + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "class_name") + +class ArmNewTreeNodeDialog(bpy.types.Operator): + bl_idname = "arm.new_treenode" + bl_label = "New Node Tree" + bl_description = 'Create a blank Node Tree' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="Object Node Tree", description="Is this an object Node Tree?", default=False) + class_name: StringProperty(name="Name", description="The Node Tree name") + + def execute(self, context): + if self.is_object: + obj = context.object + else: + obj = context.scene + self.class_name = self.class_name.replace(' ', '') + # Create new node tree + node_tree = bpy.data.node_groups.new(self.class_name, 'ArmLogicTreeType') + # Set new node tree + item = obj.arm_traitlist[obj.arm_traitlist_index] + if item.node_tree_prop is None: + item.node_tree_prop = node_tree + return {'FINISHED'} + + def invoke(self, context, event): + if not arm.utils.check_saved(self): + return {'CANCELLED'} + self.class_name = 'MyNodeTree' + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "class_name") + +class ArmEditTreeNodeDialog(bpy.types.Operator): + bl_idname = "arm.edit_treenode" + bl_label = "Edit Node Tree" + bl_description = 'Edit this Node Tree in the Logic Node Editor' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="Object Node Tree", description="Is this an object Node Tree?", default=False) + + def execute(self, context): + if self.is_object: + obj = context.object + else: + obj = context.scene + # Check len node tree list + if len(obj.arm_traitlist) > 0: + item = obj.arm_traitlist[obj.arm_traitlist_index] + # Loop for all spaces + context_screen = context.screen + if item is not None and context_screen is not None: + areas = context_screen.areas + for area in areas: + for space in area.spaces: + if space.type == 'NODE_EDITOR': + if space.tree_type == 'ArmLogicTreeType': + # Set Node Tree + space.node_tree = item.node_tree_prop + return {'FINISHED'} + +class ArmGetTreeNodeDialog(bpy.types.Operator): + bl_idname = "arm.get_treenode" + bl_label = "From Node Editor" + bl_description = 'Use the Node Tree from the opened Node Tree Editor for this trait' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="Object Node Tree", description="Is this an object Node Tree?", default=False) + + def execute(self, context): + if self.is_object: + obj = context.object + else: + obj = context.scene + # Check len node tree list + if len(obj.arm_traitlist) > 0: + item = obj.arm_traitlist[obj.arm_traitlist_index] + # Loop for all spaces + context_screen = context.screen + if item is not None and context_screen is not None: + areas = context_screen.areas + for area in areas: + for space in area.spaces: + if space.type == 'NODE_EDITOR': + if space.tree_type == 'ArmLogicTreeType' and space.node_tree is not None: + # Set Node Tree in Item + item.node_tree_prop = space.node_tree + return {'FINISHED'} + +class ArmNewCanvasDialog(bpy.types.Operator): + bl_idname = "arm.new_canvas" + bl_label = "New Canvas" + bl_description = 'Create a blank canvas' + bl_options = {'INTERNAL'} + + is_object: BoolProperty(name="Object trait", description="Is this an object trait?", default=False) + canvas_name: StringProperty(name="Name", description="The canvas name") + + def execute(self, context): + if self.is_object: + obj = bpy.context.object + else: + obj = bpy.context.scene + self.canvas_name = self.canvas_name.replace(' ', '') + write_data.write_canvasjson(self.canvas_name) + arm.utils.fetch_script_names() + item = obj.arm_traitlist[obj.arm_traitlist_index] + item.canvas_name_prop = self.canvas_name + return {'FINISHED'} + + def invoke(self, context, event): + if not arm.utils.check_saved(self): + return {'CANCELLED'} + self.canvas_name = 'MyCanvas' + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "canvas_name") + + +class ArmNewWasmButton(bpy.types.Operator): + """Create new WebAssembly module""" + bl_idname = 'arm.new_wasm' + bl_label = 'New Module' + + def execute(self, context): + webbrowser.open('https://esmbly.github.io/WebAssemblyStudio/') + return {'FINISHED'} + + +class ArmRefreshScriptsButton(bpy.types.Operator): + """Fetch all script names and trait properties.""" + bl_idname = 'arm.refresh_scripts' + bl_label = 'Refresh Traits' + + poll_msg = ( + "Cannot refresh scripts for overrides at the moment due to" + " Blender limitations. Please use the 'Refresh' operator in" + " the linked file." + ) + + def execute(self, context): + arm.utils.fetch_bundled_script_names() + arm.utils.fetch_bundled_trait_props() + arm.utils.fetch_script_names() + arm.utils.fetch_trait_props() + arm.utils.fetch_wasm_names() + return{'FINISHED'} + +class ArmRefreshObjectScriptsButton(bpy.types.Operator): + """Fetch all script names and trait properties.""" + bl_idname = 'arm.refresh_object_scripts' + bl_label = 'Refresh Traits' + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + cls.poll_message_set(ArmRefreshScriptsButton.poll_msg) + # Technically we could keep the operator enabled here since + # fetch_trait_props() checks for overrides and the operator does + # not depend on the current object, but this way the user + # can recognize why refreshing doesn't work. + return context.object.override_library is None + + def execute(self, context): + return ArmRefreshScriptsButton.execute(self, context) + + +class ArmRefreshCanvasListButton(bpy.types.Operator): + """Fetch all canvas names""" + bl_idname = 'arm.refresh_canvas_list' + bl_label = 'Refresh Canvas Traits' + + def execute(self, context): + arm.utils.fetch_script_names() + return{'FINISHED'} + +class ARM_PT_TraitPanel(bpy.types.Panel): + bl_label = "Armory Traits" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + obj = bpy.context.object + draw_traits_panel(self.layout, obj, is_object=True) + +class ARM_PT_SceneTraitPanel(bpy.types.Panel): + bl_label = "Armory Scene Traits" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + obj = bpy.context.scene + draw_traits_panel(self.layout, obj, is_object=False) + +class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): + bl_label = 'Copy Traits from Active Object' + bl_idname = 'arm.copy_traits_to_active' + bl_description = 'Copies the traits of the active object to all other selected objects' + + overwrite: BoolProperty(name="Overwrite", default=True) + + @classmethod + def poll(cls, context): + return context.active_object is not None and len(context.selected_objects) > 1 + + def draw_message_box(self, context): + layout = self.layout + layout = layout.column(align=True) + layout.alignment = 'EXPAND' + + layout.label(text='Warning: At least one target object already has', icon='ERROR') + layout.label(text='traits assigned to it!', icon='BLANK1') + layout.separator() + layout.label(text='Do you want to overwrite the already existing traits', icon='BLANK1') + layout.label(text='or append to them?', icon='BLANK1') + layout.separator() + + row = layout.row(align=True) + row.active_default = True + row.operator('arm.copy_traits_to_active', text='Overwrite').overwrite = True + row.active_default = False + row.operator('arm.copy_traits_to_active', text='Append').overwrite = False + row.operator('arm.discard_popup', text='Cancel') + + def execute(self, context): + source_obj = bpy.context.active_object + + for target_obj in bpy.context.selected_objects: + if source_obj == target_obj: + continue + + # Offset for trait iteration when appending traits + offset = 0 + if not self.overwrite: + offset = len(target_obj.arm_traitlist) + + arm.utils.merge_into_collection( + source_obj.arm_traitlist, target_obj.arm_traitlist, clear_dst=self.overwrite) + + for i in range(len(source_obj.arm_traitlist)): + arm.utils.merge_into_collection( + source_obj.arm_traitlist[i].arm_traitpropslist, + target_obj.arm_traitlist[i + offset].arm_traitpropslist + ) + + return {"FINISHED"} + + def invoke(self, context, event): + show_dialog = False + + # Test if there is a target object which has traits that would + # get overwritten + source_obj = bpy.context.active_object + for target_object in bpy.context.selected_objects: + if source_obj == target_object: + continue + else: + if target_object.arm_traitlist: + show_dialog = True + + if show_dialog: + context.window_manager.popover(self.__class__.draw_message_box, ui_units_x=16) + else: + bpy.ops.arm.copy_traits_to_active() + + return {'INTERFACE'} + +class ARM_MT_context_menu(Menu): + bl_label = "Trait Specials" + + def draw(self, _context): + layout = self.layout + + layout.operator("arm.copy_traits_to_active", icon='PASTEDOWN') + layout.operator("arm.print_traits", icon='CONSOLE') + +def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, bpy.types.Scene], is_object: bool) -> None: + layout.use_property_split = True + layout.use_property_decorate = False + + # Make the list bigger when there are a few traits + num_rows = 2 + if len(obj.arm_traitlist) > 1: + num_rows = 4 + + row = layout.row() + row.template_list("ARM_UL_TraitList", "The_List", obj, "arm_traitlist", obj, "arm_traitlist_index", rows=num_rows) + + col = row.column(align=True) + op = col.operator("arm_traitlist.new_item", icon='ADD', text="") + op.invoked_by_search = False + op.is_object = is_object + if is_object: + op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="") + else: + op = col.operator("arm_traitlist.delete_item_scene", icon='REMOVE', text="") + op.is_object = is_object + + col.separator() + + col.menu("ARM_MT_context_menu", icon='DOWNARROW_HLT', text="") + + if len(obj.arm_traitlist) > 1: + col.separator() + op = col.operator("arm_traitlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op.is_object = is_object + op = col.operator("arm_traitlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + op.is_object = is_object + + # Draw trait specific content + if obj.arm_traitlist_index >= 0 and len(obj.arm_traitlist) > 0: + item = obj.arm_traitlist[obj.arm_traitlist_index] + + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.scale_y = 1.2 + + if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': + if item.type_prop == 'Haxe Script': + row.operator("arm.new_script", icon="FILE_NEW").is_object = is_object + column = row.column(align=True) + column.enabled = item.class_name_prop != '' + column.operator("arm.edit_script", icon_value=ICON_HAXE).is_object = is_object + + # Bundled scripts + else: + if item.class_name_prop == 'NavMesh': + row.operator("arm.generate_navmesh", icon="UV_VERTEXSEL") + else: + row.enabled = item.class_name_prop != '' + row.operator("arm.edit_bundled_script", icon_value=ICON_HAXE).is_object = is_object + + refresh_op = "arm.refresh_object_scripts" if is_object else "arm.refresh_scripts" + row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") + + # Default props + row = layout.row() + if item.type_prop == 'Haxe Script': + row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_scripts_list", text="Class") + else: + row.prop_search(item, "class_name_prop", bpy.data.worlds['Arm'], "arm_bundled_scripts_list", text="Class") + + elif item.type_prop == 'WebAssembly': + row.operator("arm.new_wasm", icon="FILE_NEW") + + column = row.column(align=True) + column.enabled = item.webassembly_prop != '' + + column.operator("arm.edit_wasm_script", icon_value=ICON_WASM).is_object = is_object + + refresh_op = "arm.refresh_object_scripts" if is_object else "arm.refresh_scripts" + row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") + + row = layout.row() + row.prop_search(item, "webassembly_prop", bpy.data.worlds['Arm'], "arm_wasm_list", text="Module") + + elif item.type_prop == 'UI Canvas': + row.operator("arm.new_canvas", icon="FILE_NEW").is_object = is_object + column = row.column(align=True) + column.enabled = item.canvas_name_prop != '' + column.operator("arm.edit_canvas", icon="NODE_COMPOSITING").is_object = is_object + + refresh_op = "arm.refresh_object_scripts" if is_object else "arm.refresh_scripts" + row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") + + row = layout.row() + row.prop_search(item, "canvas_name_prop", bpy.data.worlds['Arm'], "arm_canvas_list", text="Canvas") + + elif item.type_prop == 'Logic Nodes': + # Check if there is at least one active Logic Node Editor + is_editor_active = False + if bpy.context.screen is not None: + areas = bpy.context.screen.areas + for area in areas: + for space in area.spaces: + if space.type == 'NODE_EDITOR': + if space.tree_type == 'ArmLogicTreeType' and space.node_tree is not None: + is_editor_active = True + break + if is_editor_active: + break + + row.operator("arm.new_treenode", text="New Tree", icon="ADD").is_object = is_object + + column = row.column(align=True) + column.enabled = is_editor_active and item.node_tree_prop is not None + column.operator("arm.edit_treenode", text="Edit Tree", icon="NODETREE").is_object = is_object + + column = row.column(align=True) + column.enabled = is_editor_active and item is not None + column.operator("arm.get_treenode", text="From Editor", icon="IMPORT").is_object = is_object + + row = layout.row() + row.prop_search(item, "node_tree_prop", bpy.data, "node_groups", text="Tree") + + # ===================== + # Draw trait properties + if (item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script') and item.class_name_prop != '': + if item.arm_traitpropslist: + layout.label(text="Trait Properties:") + if item.arm_traitpropswarnings: + box = layout.box() + box.label(text=f"Warnings ({len(item.arm_traitpropswarnings)}):", icon="ERROR") + col = box.column(align=True) + + for warning in item.arm_traitpropswarnings: + col.label(text=f'"{warning.propName}": {warning.warning}') + + propsrows = max(len(item.arm_traitpropslist), 6) + row = layout.row() + row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + +def register(): + bpy.utils.register_class(ArmTraitListItem) + bpy.utils.register_class(ARM_UL_TraitList) + bpy.utils.register_class(ArmTraitListNewItem) + bpy.utils.register_class(ArmTraitListDeleteItem) + bpy.utils.register_class(ArmTraitListDeleteItemScene) + bpy.utils.register_class(ArmTraitListMoveItem) + bpy.utils.register_class(ArmEditScriptButton) + bpy.utils.register_class(ArmEditBundledScriptButton) + bpy.utils.register_class(ArmEditWasmScriptButton) + bpy.utils.register_class(ArmoryGenerateNavmeshButton) + bpy.utils.register_class(ArmEditCanvasButton) + bpy.utils.register_class(ArmNewScriptDialog) + bpy.utils.register_class(ArmNewTreeNodeDialog) + bpy.utils.register_class(ArmEditTreeNodeDialog) + bpy.utils.register_class(ArmGetTreeNodeDialog) + bpy.utils.register_class(ArmNewCanvasDialog) + bpy.utils.register_class(ArmNewWasmButton) + bpy.utils.register_class(ArmRefreshScriptsButton) + bpy.utils.register_class(ArmRefreshObjectScriptsButton) + bpy.utils.register_class(ArmRefreshCanvasListButton) + bpy.utils.register_class(ARM_PT_TraitPanel) + bpy.utils.register_class(ARM_PT_SceneTraitPanel) + bpy.utils.register_class(ARM_OT_CopyTraitsFromActive) + bpy.utils.register_class(ARM_MT_context_menu) + + bpy.types.Object.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) + bpy.types.Object.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + bpy.types.Scene.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) + bpy.types.Scene.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + +def unregister(): + bpy.utils.unregister_class(ARM_OT_CopyTraitsFromActive) + bpy.utils.unregister_class(ARM_MT_context_menu) + bpy.utils.unregister_class(ArmTraitListItem) + bpy.utils.unregister_class(ARM_UL_TraitList) + bpy.utils.unregister_class(ArmTraitListNewItem) + bpy.utils.unregister_class(ArmTraitListDeleteItem) + bpy.utils.unregister_class(ArmTraitListDeleteItemScene) + bpy.utils.unregister_class(ArmTraitListMoveItem) + bpy.utils.unregister_class(ArmEditScriptButton) + bpy.utils.unregister_class(ArmEditBundledScriptButton) + bpy.utils.unregister_class(ArmEditWasmScriptButton) + bpy.utils.unregister_class(ArmoryGenerateNavmeshButton) + bpy.utils.unregister_class(ArmEditCanvasButton) + bpy.utils.unregister_class(ArmNewScriptDialog) + bpy.utils.unregister_class(ArmGetTreeNodeDialog) + bpy.utils.unregister_class(ArmEditTreeNodeDialog) + bpy.utils.unregister_class(ArmNewTreeNodeDialog) + bpy.utils.unregister_class(ArmNewCanvasDialog) + bpy.utils.unregister_class(ArmNewWasmButton) + bpy.utils.unregister_class(ArmRefreshObjectScriptsButton) + bpy.utils.unregister_class(ArmRefreshScriptsButton) + bpy.utils.unregister_class(ArmRefreshCanvasListButton) + bpy.utils.unregister_class(ARM_PT_TraitPanel) + bpy.utils.unregister_class(ARM_PT_SceneTraitPanel) diff --git a/blender/arm/props_traits_props.py b/blender/arm/props_traits_props.py new file mode 100644 index 0000000000..126d01582e --- /dev/null +++ b/blender/arm/props_traits_props.py @@ -0,0 +1,164 @@ +import bpy +from bpy.props import * + +__all__ = ['ArmTraitPropWarning', 'ArmTraitPropListItem', 'ARM_UL_PropList'] + +PROP_TYPE_ICONS = { + "String": "SORTALPHA", + "Int": "CHECKBOX_DEHLT", + "Float": "RADIOBUT_OFF", + "Bool": "CHECKMARK", + "Vec2": "ORIENTATION_VIEW", + "Vec3": "ORIENTATION_GLOBAL", + "Vec4": "MESH_ICOSPHERE", + "Object": "OBJECT_DATA", + "CameraObject": "CAMERA_DATA", + "LightObject": "LIGHT_DATA", + "MeshObject": "MESH_DATA", + "SpeakerObject": "OUTLINER_DATA_SPEAKER" +} + + +def filter_objects(item, b_object): + if item.type == "CameraObject": + return b_object.type == "CAMERA" + if item.type == "LightObject": + return b_object.type == "LIGHT" + if item.type == "MeshObject": + return b_object.type == "MESH" + if item.type == "SpeakerObject": + return b_object.type == "SPEAKER" + + if item.type == "Object": + return True + + +class ArmTraitPropWarning(bpy.types.PropertyGroup): + propName: StringProperty(name="Property Name") + warning: StringProperty(name="Warning") + + +class ArmTraitPropListItem(bpy.types.PropertyGroup): + """Group of properties representing an item in the list.""" + name: StringProperty( + name="Name", + description="The name of this property", + default="Untitled") + + type: EnumProperty( + items=( + # (Haxe Type, Display Name, Description) + ("String", "String", "String Type"), + ("Int", "Integer", "Integer Type"), + ("Float", "Float", "Float Type"), + ("Bool", "Boolean", "Boolean Type"), + ("Vec2", "Vec2", "2D Vector Type"), + ("Vec3", "Vec3", "3D Vector Type"), + ("Vec4", "Vec4", "4D Vector Type"), + ("Object", "Object", "Object Type"), + ("CameraObject", "Camera Object", "Camera Object Type"), + ("LightObject", "Light Object", "Light Object Type"), + ("MeshObject", "Mesh Object", "Mesh Object Type"), + ("SpeakerObject", "Speaker Object", "Speaker Object Type")), + name="Type", + description="The type of this property", + default="String", + override={"LIBRARY_OVERRIDABLE"} + ) + + # === VALUES === + value_string: StringProperty(name="Value", default="", override={"LIBRARY_OVERRIDABLE"}) + value_int: IntProperty(name="Value", default=0, override={"LIBRARY_OVERRIDABLE"}) + value_float: FloatProperty(name="Value", default=0.0, override={"LIBRARY_OVERRIDABLE"}) + value_bool: BoolProperty(name="Value", default=False, override={"LIBRARY_OVERRIDABLE"}) + value_vec2: FloatVectorProperty(name="Value", size=2, override={"LIBRARY_OVERRIDABLE"}) + value_vec3: FloatVectorProperty(name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) + value_vec4: FloatVectorProperty(name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) + value_object: PointerProperty( + name="Value", type=bpy.types.Object, poll=filter_objects, + override={"LIBRARY_OVERRIDABLE"} + ) + + def set_value(self, val): + # Would require way too much effort, so it's out of scope here. + if self.type.endswith("Object"): + return + + if self.type == "Int": + self.value_int = int(val) + elif self.type == "Float": + self.value_float = float(val) + elif self.type == "Bool": + self.value_bool = val == "true" + elif self.type in ("Vec2", "Vec3", "Vec4"): + if isinstance(val, str): + dimensions = int(self.type[-1]) + + # Parse "new VecX(...)" + val = val.split("(")[1].split(")")[0].split(",") + val = [value.strip() for value in val] + + # new VecX() without parameters + if len(val) == 1 and val[0] == "": + # Use default value + return + + # new VecX() with less parameters than its dimensions + while len(val) < dimensions: + val.append(0.0) + + val = [float(value) for value in val] + + setattr(self, "value_" + self.type.lower(), val) + else: + self.value_string = str(val) + + def get_value(self): + if self.type == "Int": + return self.value_int + if self.type == "Float": + return self.value_float + if self.type == "Bool": + return self.value_bool + if self.type in ("Vec2", "Vec3", "Vec4"): + return list(getattr(self, "value_" + self.type.lower())) + if self.type.endswith("Object"): + if self.value_object is not None: + return self.value_object.name + return "" + + return self.value_string + + +class ARM_UL_PropList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + item_value_ref = "value_" + item.type.lower() + custom_icon = PROP_TYPE_ICONS[item.type] + + sp = layout.split(factor=0.3) + sp.label(text=item.type, icon=custom_icon) + sp = sp.split(factor=0.6) + sp.label(text=item.name) + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if item.type.endswith("Object"): + sp.prop_search(item, "value_object", context.scene, "objects", text="", icon=custom_icon) + else: + use_emboss = item.type in ("Bool", "String") + sp.prop(item, item_value_ref, text="", emboss=use_emboss) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + + +def register(): + bpy.utils.register_class(ArmTraitPropWarning) + bpy.utils.register_class(ArmTraitPropListItem) + bpy.utils.register_class(ARM_UL_PropList) + + +def unregister(): + bpy.utils.unregister_class(ARM_UL_PropList) + bpy.utils.unregister_class(ArmTraitPropListItem) + bpy.utils.unregister_class(ArmTraitPropWarning) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py new file mode 100644 index 0000000000..5202af8e3a --- /dev/null +++ b/blender/arm/props_ui.py @@ -0,0 +1,2880 @@ +import json +import os +import sys +import shutil +import textwrap + +import bpy +from bpy.props import * + +from arm.lightmapper.panels import scene + +import arm.api +import arm.assets as assets +from arm.exporter import ArmoryExporter +import arm.log as log +import arm.logicnode.replacement +import arm.make as make +import arm.make_state as state +import arm.props as props +import arm.props_properties +import arm.props_traits +import arm.nodes_logic +import arm.ui_icons as ui_icons +import arm.utils +import arm.utils_vs +import arm.write_probes + +if arm.is_reload(__name__): + arm.api = arm.reload_module(arm.api) + assets = arm.reload_module(assets) + arm.exporter = arm.reload_module(arm.exporter) + from arm.exporter import ArmoryExporter + log = arm.reload_module(log) + arm.logicnode.replacement = arm.reload_module(arm.logicnode.replacement) + make = arm.reload_module(make) + state = arm.reload_module(state) + props = arm.reload_module(props) + arm.props_properties = arm.reload_module(arm.props_properties) + arm.props_traits = arm.reload_module(arm.props_traits) + arm.nodes_logic = arm.reload_module(arm.nodes_logic) + ui_icons = arm.reload_module(ui_icons) + arm.utils = arm.reload_module(arm.utils) + arm.utils_vs = arm.reload_module(arm.utils_vs) + arm.write_probes = arm.reload_module(arm.write_probes) +else: + arm.enable_reload(__name__) + +class ARM_PT_ObjectPropsPanel(bpy.types.Panel): + """Menu in object region.""" + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + obj = bpy.context.object + if obj == None: + return + + col = layout.column() + col.prop(obj, 'arm_export') + if not obj.arm_export: + return + col.prop(obj, 'arm_spawn') + col.prop(obj, 'arm_mobile') + col.prop(obj, 'arm_animation_enabled') + + if obj.type == 'MESH': + layout.prop(obj, 'arm_instanced') + wrd = bpy.data.worlds['Arm'] + layout.prop_search(obj, "arm_tilesheet", wrd, "arm_tilesheetlist", text="Tilesheet") + if obj.arm_tilesheet != '': + selected_ts = None + for ts in wrd.arm_tilesheetlist: + if ts.name == obj.arm_tilesheet: + selected_ts = ts + break + layout.prop_search(obj, "arm_tilesheet_action", selected_ts, "arm_tilesheetactionlist", text="Action") + + # Properties list + arm.props_properties.draw_properties(layout, obj) + + # Lightmapping props + if obj.type == "MESH": + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + row = layout.row() + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + else: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") + row = layout.row() + + + if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_filter_override: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") + row = layout.row(align=True) + if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + else: + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + + #If UV Packer installed + if "UV-Packer" in bpy.context.preferences.addons.keys(): + row.prop(obj.TLM_ObjectProperties, "tlm_use_uv_packer") + if obj.TLM_ObjectProperties.tlm_use_uv_packer: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_padding") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_packing_engine") + +class ARM_PT_ModifiersPropsPanel(bpy.types.Panel): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = bpy.context.object + if obj == None: + return + layout.operator("arm.invalidate_cache") + +class ARM_PT_ParticlesPropsPanel(bpy.types.Panel): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "particle" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = bpy.context.particle_system + if obj == None: + return + + layout.prop(obj.settings, 'arm_loop') + layout.prop(obj.settings, 'arm_count_mult') + +class ARM_PT_PhysicsPropsPanel(bpy.types.Panel): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "physics" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = bpy.context.object + if obj == None: + return + + rb = obj.rigid_body + if rb is not None: + col = layout.column() + row = col.row() + row.alignment = 'RIGHT' + + rb_type = 'Dynamic' + if ArmoryExporter.rigid_body_static(rb): + rb_type = 'Static' + if rb.kinematic: + rb_type = 'Kinematic' + row.label(text=(f'Rigid Body Export Type: {rb_type}'), icon='AUTO') + + layout.prop(obj, 'arm_rb_linear_factor') + layout.prop(obj, 'arm_rb_angular_factor') + layout.prop(obj, 'arm_rb_angular_friction') + layout.prop(obj, 'arm_rb_trigger') + layout.prop(obj, 'arm_rb_ccd') + + 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): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = bpy.context.object + if obj == None: + return + + wrd = bpy.data.worlds['Arm'] + if obj.type == 'CAMERA': + layout.prop(obj.data, 'arm_frustum_culling') + elif obj.type == 'MESH' or obj.type == 'FONT' or obj.type == 'META': + layout.prop(obj.data, 'arm_dynamic_usage') + layout.operator("arm.invalidate_cache") + elif obj.type == 'LIGHT': + layout.prop(obj.data, 'arm_clip_start') + layout.prop(obj.data, 'arm_clip_end') + layout.prop(obj.data, 'arm_fov') + layout.prop(obj.data, 'arm_shadows_bias') + layout.prop(wrd, 'arm_light_ies_texture') + layout.prop(wrd, 'arm_light_clouds_texture') + elif obj.type == 'SPEAKER': + layout.prop(obj.data, 'arm_play_on_start') + layout.prop(obj.data, 'arm_loop') + layout.prop(obj.data, 'arm_stream') + elif obj.type == 'ARMATURE': + layout.prop(obj.data, 'arm_autobake') + layout.prop(obj.data, 'arm_relative_bone_constraints') + pass + +class ARM_PT_WorldPropsPanel(bpy.types.Panel): + bl_label = "Armory World Properties" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + world = context.world + if world is None: + return + + layout.prop(world, 'arm_use_clouds') + col = layout.column(align=True) + col.enabled = world.arm_use_clouds + col.prop(world, 'arm_darken_clouds') + col.prop(world, 'arm_clouds_lower') + col.prop(world, 'arm_clouds_upper') + col.prop(world, 'arm_clouds_precipitation') + col.prop(world, 'arm_clouds_secondary') + col.prop(world, 'arm_clouds_wind') + col.prop(world, 'arm_clouds_steps') + +class ARM_PT_ScenePropsPanel(bpy.types.Panel): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + scene = bpy.context.scene + if scene == None: + return + row = layout.row() + column = row.column() + row.prop(scene, 'arm_export') + +class InvalidateCacheButton(bpy.types.Operator): + """Delete cached mesh data""" + bl_idname = "arm.invalidate_cache" + bl_label = "Invalidate Cache" + + def execute(self, context): + context.object.data.arm_cached = False + return{'FINISHED'} + +class InvalidateMaterialCacheButton(bpy.types.Operator): + """Delete cached material data""" + bl_idname = "arm.invalidate_material_cache" + bl_label = "Invalidate Cache" + + def execute(self, context): + context.material.arm_cached = False + context.material.signature = '' + return{'FINISHED'} + +class ARM_OT_NewCustomMaterial(bpy.types.Operator): + bl_idname = "arm.new_custom_material" + bl_label = "New Custom Material" + bl_description = "Add a new custom material. This will create all the necessary files and folders" + + def poll_mat_name(self, context): + project_dir = arm.utils.get_fp() + shader_dir_dst = os.path.join(project_dir, 'Shaders') + mat_name = arm.utils.safestr(self.mat_name) + + self.mat_exists = os.path.isdir(os.path.join(project_dir, 'Bundled', mat_name)) + + vert_exists = os.path.isfile(os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) + frag_exists = os.path.isfile(os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) + self.shader_exists = vert_exists or frag_exists + + mat_name: StringProperty( + name='Material Name', description='The name of the new material', + default='MyCustomMaterial', + update=poll_mat_name) + mode: EnumProperty( + name='Target RP', description='Choose for which render path mode the new material is created', + default='deferred', + items=[('deferred', 'Deferred', 'Create the material for a deferred render path'), + ('forward', 'Forward', 'Create the material for a forward render path')]) + mat_exists: BoolProperty( + name='Material Already Exists', + default=False, + options={'HIDDEN', 'SKIP_SAVE'}) + shader_exists: BoolProperty( + name='Shaders Already Exist', + default=False, + options={'HIDDEN', 'SKIP_SAVE'}) + + def invoke(self, context, event): + if not bpy.data.is_saved: + self.report({'INFO'}, "Please save your file first") + return {"CANCELLED"} + + # Try to set deferred/forward based on the selected render path + try: + self.mode = 'forward' if arm.utils.get_rp().rp_renderer == 'Forward' else 'deferred' + except IndexError: + # No render path, use default (deferred) + pass + + self.poll_mat_name(context) + wm = context.window_manager + return wm.invoke_props_dialog(self, width=300) + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + layout.prop(self, 'mat_name') + layout.prop(self, 'mode', expand=True) + + if self.mat_exists: + box = layout.box() + box.alert = True + col = box.column(align=True) + col.label(text='A custom material with that name already exists,', icon='ERROR') + col.label(text='clicking on \'OK\' will override the material!', icon='BLANK1') + + if self.shader_exists: + box = layout.box() + box.alert = True + col = box.column(align=True) + col.label(text='Shader file(s) with that name already exists,', icon='ERROR') + col.label(text='clicking on \'OK\' will override the shader(s)!', icon='BLANK1') + + def execute(self, context): + if self.mat_name == '': + return {'CANCELLED'} + + project_dir = arm.utils.get_fp() + shader_dir_src = os.path.join(arm.utils.get_sdk_path(), 'armory', 'Shaders', 'custom_mat_presets') + shader_dir_dst = os.path.join(project_dir, 'Shaders') + mat_name = arm.utils.safestr(self.mat_name) + mat_dir = os.path.join(project_dir, 'Bundled', mat_name) + + os.makedirs(mat_dir, exist_ok=True) + os.makedirs(shader_dir_dst, exist_ok=True) + + # Shader data + if self.mode == 'forward': + col_attachments = ['RGBA64'] + constants = [{'link': '_worldViewProjectionMatrix', 'name': 'WVP', 'type': 'mat4'}] + vertex_elems = [{'name': 'pos', 'data': 'short4norm'}] + else: + col_attachments = ['RGBA64', 'RGBA64'] + constants = [ + {'link': '_worldViewProjectionMatrix', 'name': 'WVP', 'type': 'mat4'}, + {'link': '_normalMatrix', 'name': 'N', 'type': 'mat3'} + ] + vertex_elems = [ + {'name': 'pos', 'data': 'short4norm'}, + {'name': 'nor', 'data': 'short2norm'} + ] + + con = { + 'color_attachments': col_attachments, + 'compare_mode': 'less', + 'constants': constants, + 'cull_mode': 'clockwise', + 'depth_write': True, + 'fragment_shader': f'{mat_name}.frag', + 'name': 'mesh', + 'texture_units': [], + 'vertex_shader': f'{mat_name}.vert', + 'vertex_elements': vertex_elems + } + data = { + 'shader_datas': [{ + 'contexts': [con], + 'name': f'{mat_name}' + }] + } + + # Save shader data file + with open(os.path.join(mat_dir, f'{mat_name}.json'), 'w') as datafile: + json.dump(data, datafile, indent=4, sort_keys=True) + + # Copy preset shaders to project + if self.mode == 'forward': + shutil.copy(os.path.join(shader_dir_src, 'custom_mat_forward.frag.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) + shutil.copy(os.path.join(shader_dir_src, 'custom_mat_forward.vert.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) + else: + shutil.copy(os.path.join(shader_dir_src, 'custom_mat_deferred.frag.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) + shutil.copy(os.path.join(shader_dir_src, 'custom_mat_deferred.vert.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) + + # True if called from the material properties tab, else it was called from the search menu + if hasattr(context, 'material') and context.material is not None: + context.material.arm_custom_material = mat_name + + return{'FINISHED'} + +class ARM_PG_BindTexturesListItem(bpy.types.PropertyGroup): + uniform_name: StringProperty( + name='Uniform Name', + description='The name of the sampler uniform as used in the shader', + default='ImageTexture', + ) + + image: PointerProperty( + name='Image', + type=bpy.types.Image, + description='The image to attach to the texture unit', + ) + +class ARM_UL_BindTexturesList(bpy.types.UIList): + def draw_item(self, context, layout, data, item: ARM_PG_BindTexturesListItem, icon, active_data, active_propname, index): + row = layout.row(align=True) + + if item.image is not None: + row.label(text=item.uniform_name, icon_value=item.image.preview.icon_id) + else: + row.label(text='', icon='ERROR') + +class ARM_OT_BindTexturesListNewItem(bpy.types.Operator): + bl_idname = "arm_bind_textures_list.new_item" + bl_label = "Add Texture Binding" + bl_description = "Add a new texture binding to the list" + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + mat = context.material + if mat is None: + return False + return True + + def execute(self, context): + mat = context.material + mat.arm_bind_textures_list.add() + mat.arm_bind_textures_list_index = len(mat.arm_bind_textures_list) - 1 + return{'FINISHED'} + +class ARM_OT_BindTexturesListDeleteItem(bpy.types.Operator): + bl_idname = "arm_bind_textures_list.delete_item" + bl_label = "Remove Texture Binding" + bl_description = "Delete the selected texture binding from the list" + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + mat = context.material + if mat is None: + return False + return len(mat.arm_bind_textures_list) > 0 + + def execute(self, context): + mat = context.material + lst = mat.arm_bind_textures_list + index = mat.arm_bind_textures_list_index + + if len(lst) <= index: + return{'FINISHED'} + + lst.remove(index) + + if index > 0: + index = index - 1 + mat.arm_bind_textures_list_index = index + + return{'FINISHED'} + +class ARM_PT_MaterialPropsPanel(bpy.types.Panel): + bl_label = "Armory Props" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + mat = bpy.context.material + if mat is None: + return + + layout.prop(mat, 'arm_cast_shadow') + columnb = layout.column() + wrd = bpy.data.worlds['Arm'] + columnb.enabled = len(wrd.arm_rplist) > 0 and arm.utils.get_rp().rp_renderer == 'Forward' + columnb.prop(mat, 'arm_receive_shadow') + layout.prop(mat, 'arm_ignore_irradiance') + layout.prop(mat, 'arm_two_sided') + columnb = layout.column() + columnb.enabled = not mat.arm_two_sided + columnb.prop(mat, 'arm_cull_mode') + layout.prop(mat, 'arm_material_id') + layout.prop(mat, 'arm_depth_read') + layout.prop(mat, 'arm_overlay') + layout.prop(mat, 'arm_decal') + layout.prop(mat, 'arm_discard') + columnb = layout.column() + columnb.enabled = mat.arm_discard + columnb.prop(mat, 'arm_discard_opacity') + columnb.prop(mat, 'arm_discard_opacity_shadows') + row = layout.row(align=True) + row.prop(mat, 'arm_custom_material') + row.operator('arm.new_custom_material', text='', icon='ADD') + layout.prop(mat, 'arm_skip_context') + layout.prop(mat, 'arm_particle_fade') + layout.prop(mat, 'arm_billboard') + + layout.operator("arm.invalidate_material_cache") + +class ARM_PT_BindTexturesPropsPanel(bpy.types.Panel): + bl_label = "Bind Textures" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_MaterialPropsPanel" + + @classmethod + def poll(cls, context): + mat = context.material + if mat is None: + return False + + return mat.arm_custom_material != '' + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + mat = bpy.context.material + if mat is None: + return + + row = layout.row(align=True) + col = row.column(align=True) + col.template_list('ARM_UL_BindTexturesList', '', mat, 'arm_bind_textures_list', mat, 'arm_bind_textures_list_index') + + if mat.arm_bind_textures_list_index >= 0 and len(mat.arm_bind_textures_list) > 0: + item = mat.arm_bind_textures_list[mat.arm_bind_textures_list_index] + box = col.box() + + if item.image is None: + _row = box.row() + _row.alert = True + _row.alignment = 'RIGHT' + _row.label(text="No image selected, skipping export") + + box.prop(item, 'uniform_name') + box.prop(item, 'image') + + col = row.column(align=True) + col.operator("arm_bind_textures_list.new_item", icon='ADD', text="") + col.operator("arm_bind_textures_list.delete_item", icon='REMOVE', text="") + +class ARM_PT_MaterialDriverPropsPanel(bpy.types.Panel): + """Per-material properties for custom render path drivers""" + bl_label = "Armory Driver Properties" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + + @classmethod + def poll(cls, context): + mat = context.material + if mat is None: + return False + + wrd = bpy.data.worlds['Arm'] + if wrd.arm_rplist_index < 0 or len(wrd.arm_rplist) == 0: + return False + + if len(arm.api.drivers) == 0: + return False + + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + return rpdat.rp_driver != 'Armory' and arm.api.drivers[rpdat.rp_driver]['draw_mat_props'] is not None + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + wrd = bpy.data.worlds['Arm'] + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + arm.api.drivers[rpdat.rp_driver]['draw_mat_props'](layout, context.material) + +class ARM_PT_MaterialBlendingPropsPanel(bpy.types.Panel): + bl_label = "Blending" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_MaterialPropsPanel" + + def draw_header(self, context): + if context.material is None: + return + self.layout.prop(context.material, 'arm_blending', text="") + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + mat = bpy.context.material + if mat is None: + return + + flow = layout.grid_flow() + flow.enabled = mat.arm_blending + col = flow.column(align=True) + col.prop(mat, 'arm_blending_source') + col.prop(mat, 'arm_blending_destination') + col.prop(mat, 'arm_blending_operation') + flow.separator() + + col = flow.column(align=True) + col.prop(mat, 'arm_blending_source_alpha') + col.prop(mat, 'arm_blending_destination_alpha') + col.prop(mat, 'arm_blending_operation_alpha') + +class ARM_PT_ArmoryPlayerPanel(bpy.types.Panel): + bl_label = "Armory Player" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.scale_y = 1.3 + if state.proc_play is None and state.proc_build is None: + row.operator("arm.play", icon="PLAY") + else: + row.operator("arm.stop", icon="MESH_PLANE") + row.operator("arm.clean_menu", icon="BRUSH_DATA") + + col = layout.box().column() + col.prop(wrd, 'arm_runtime') + col.prop(wrd, 'arm_play_camera') + col.prop(wrd, 'arm_play_scene') + col.prop_search(wrd, 'arm_play_renderpath', wrd, 'arm_rplist', text='Render Path') + + if log.num_warnings > 0: + box = layout.box() + box.alert = True + + col = box.column(align=True) + warnings = 'warnings' if log.num_warnings > 1 else 'warning' + col.label(text=f'{log.num_warnings} {warnings} occurred during compilation!', icon='ERROR') + # Blank icon to achieve the same indentation as the line before + # prevent showing "open console" twice: + if log.num_errors == 0: + col.label(text='Please open the console to get more information.', icon='BLANK1') + + if log.num_errors > 0: + box = layout.box() + box.alert = True + # Less spacing between lines + col = box.column(align=True) + errors = 'errors' if log.num_errors > 1 else 'error' + col.label(text=f'{log.num_errors} {errors} occurred during compilation!', icon='CANCEL') + # Blank icon to achieve the same indentation as the line before + col.label(text='Please open the console to get more information.', icon='BLANK1') + +class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): + bl_label = "Armory Exporter" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.scale_y = 1.3 + row.operator("arm.build_project", icon="MOD_BUILD") + # row.operator("arm.patch_project") + row.operator("arm.publish_project", icon="EXPORT") + + rows = 2 + if len(wrd.arm_exporterlist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_ExporterList", "The_List", wrd, "arm_exporterlist", wrd, "arm_exporterlist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_exporterlist.new_item", icon='ADD', text="") + col.operator("arm_exporterlist.delete_item", icon='REMOVE', text="") + col.menu("ARM_MT_ExporterListSpecials", icon='DOWNARROW_HLT', text="") + + if len(wrd.arm_exporterlist) > 1: + col.separator() + op = col.operator("arm_exporterlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_exporterlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0: + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + box = layout.box().column() + box.prop(item, 'arm_project_target') + box.prop(item, 'arm_project_khamake') + box.prop(item, arm.utils.target_to_gapi(item.arm_project_target)) + box.prop_search(item, "arm_project_rp", wrd, "arm_rplist", text="Render Path") + box.prop_search(item, 'arm_project_scene', bpy.data, 'scenes', text='Scene') + layout.separator() + + col = layout.column(align=True) + col.prop(wrd, 'arm_project_name') + col.prop(wrd, 'arm_project_package') + col.prop(wrd, 'arm_project_bundle') + + col = layout.column(align=True) + col.prop(wrd, 'arm_project_version') + col.prop(wrd, 'arm_project_version_autoinc') + + col = layout.column() + col.prop(wrd, 'arm_project_icon') + + col = layout.column(heading='Code Output', align=True) + col.prop(wrd, 'arm_dce') + col.prop(wrd, 'arm_compiler_inline') + col.prop(wrd, 'arm_minify_js') + col.prop(wrd, 'arm_no_traces') + + col = layout.column(heading='Data', align=True) + col.prop(wrd, 'arm_minimize') + col.prop(wrd, 'arm_optimize_data') + col.prop(wrd, 'arm_asset_compression') + col.prop(wrd, 'arm_single_data_file') + +class ExporterTargetSettingsMixin: + """Mixin for common exporter setting subpanel functionality. + + Panels that inherit from this mixin need to have a arm_target + variable for polling.""" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'render' + bl_parent_id = 'ARM_PT_ArmoryExporterPanel' + + # Override this in sub classes + arm_panel = '' + + @classmethod + def poll(cls, context): + wrd = bpy.data.worlds['Arm'] + if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + return item.arm_project_target == cls.arm_target + return False + + def draw_header(self, context): + self.layout.label(text='', icon='SETTINGS') + +class ARM_PT_ArmoryExporterAndroidSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "Android Settings" + arm_target = 'android-hl' # See ExporterTargetSettingsMixin + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + col = layout.column() + col.prop(wrd, 'arm_winorient') + col.prop(wrd, 'arm_project_android_sdk_min') + col.prop(wrd, 'arm_project_android_sdk_target') + col.prop(wrd, 'arm_project_android_sdk_compile') + +class ARM_PT_ArmoryExporterAndroidPermissionsPanel(bpy.types.Panel): + bl_label = "Permissions" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + # Permission + row = layout.row() + rows = 2 + if len(wrd.arm_exporter_android_permission_list) > 1: + rows = 4 + row.template_list("ARM_UL_Exporter_AndroidPermissionList", "The_List", wrd, "arm_exporter_android_permission_list", wrd, "arm_exporter_android_permission_list_index", rows=rows) + col = row.column(align=True) + col.operator("arm_exporter_android_permission_list.new_item", icon='ADD', text="") + col.operator("arm_exporter_android_permission_list.delete_item", icon='REMOVE', text="") + row = layout.row() + + if wrd.arm_exporter_android_permission_list_index >= 0 and len(wrd.arm_exporter_android_permission_list) > 0: + item = wrd.arm_exporter_android_permission_list[wrd.arm_exporter_android_permission_list_index] + row = layout.row() + row.prop(item, 'arm_android_permissions') + +class ARM_PT_ArmoryExporterAndroidAbiPanel(bpy.types.Panel): + bl_label = "Android ABI Filters" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = { 'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + # ABIs + row = layout.row() + rows = 2 + if len(wrd.arm_exporter_android_abi_list) > 1: + rows = 4 + row.template_list("ARM_UL_Exporter_AndroidAbiList", "The_List", wrd, "arm_exporter_android_abi_list", wrd, "arm_exporter_android_abi_list_index", rows=rows) + col = row.column(align=True) + col.operator("arm_exporter_android_abi_list.new_item", icon='ADD', text="") + col.operator("arm_exporter_android_abi_list.delete_item", icon='REMOVE', text="") + row = layout.row() + + if wrd.arm_exporter_android_abi_list_index >= 0 and len(wrd.arm_exporter_android_abi_list) > 0: + item = wrd.arm_exporter_android_abi_list[wrd.arm_exporter_android_abi_list_index] + row = layout.row() + row.prop(item, 'arm_android_abi') + +class ARM_PT_ArmoryExporterAndroidBuildAPKPanel(bpy.types.Panel): + bl_label = "Building APK" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + path = arm.utils.get_android_sdk_root_path() + + col = layout.column() + + row = col.row() + row.enabled = len(path) > 0 + row.prop(wrd, 'arm_project_android_build_apk') + + row = col.row() + row.enabled = wrd.arm_project_android_build_apk + row.prop(wrd, 'arm_project_android_rename_apk') + row = col.row() + row.enabled = wrd.arm_project_android_build_apk and len(arm.utils.get_android_apk_copy_path()) > 0 + row.prop(wrd, 'arm_project_android_copy_apk') + + row = col.row(align=True) + row.prop(wrd, 'arm_project_android_list_avd') + sub = row.column(align=True) + sub.enabled = len(path) > 0 + sub.operator('arm.update_list_android_emulator', text='', icon='FILE_REFRESH') + sub = row.column(align=True) + sub.enabled = len(path) > 0 and len(arm.utils.get_android_emulator_name()) > 0 + sub.operator('arm.run_android_emulator', text='', icon='PLAY') + + row = col.row() + row.enabled = arm.utils.get_project_android_build_apk() and len(arm.utils.get_android_emulator_name()) > 0 + row.prop(wrd, 'arm_project_android_run_avd') + +class ARM_PT_ArmoryExporterHTML5SettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "HTML5 Settings" + arm_target = 'html5' # See ExporterTargetSettingsMixin + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + col = layout.column() + col.prop(wrd, 'arm_project_html5_popupmenu_in_browser') + row = col.row() + row.enabled = len(arm.utils.get_html5_copy_path()) > 0 + row.prop(wrd, 'arm_project_html5_copy') + row = col.row() + row.enabled = len(arm.utils.get_html5_copy_path()) > 0 and wrd.arm_project_html5_copy and len(arm.utils.get_link_web_server()) > 0 + row.prop(wrd, 'arm_project_html5_start_browser') + + +class ARM_PT_ArmoryExporterWindowsSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "Windows Settings" + arm_target = 'windows-hl' # See ExporterTargetSettingsMixin + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + is_windows = arm.utils.get_os_is_windows() + + col = layout.column() + col.prop(wrd, 'arm_project_win_list_vs') + row = col.row() + row.enabled = is_windows + row.prop(wrd, 'arm_project_win_build', text='After Publish') + + layout = layout.column() + layout.enabled = is_windows + + if is_windows and wrd.arm_project_win_build != 'nothing' and not arm.utils_vs.is_version_installed(wrd.arm_project_win_list_vs): + box = draw_error_box( + layout, + 'The selected version of Visual Studio could not be found and' + ' may not be installed. The "After Publish" action may not work' + ' as intended.' + ) + box.operator('arm.update_list_installed_vs', icon='FILE_REFRESH') + + layout.separator() + + col = layout.column() + col.enabled = wrd.arm_project_win_build.startswith('compile') + col.prop(wrd, 'arm_project_win_build_mode') + col.prop(wrd, 'arm_project_win_build_arch') + col.prop(wrd, 'arm_project_win_build_log') + col.prop(wrd, 'arm_project_win_build_cpu') + col.prop(wrd, 'arm_project_win_build_open') + +class ARM_PT_ArmoryProjectPanel(bpy.types.Panel): + bl_label = "Armory Project" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + row = layout.row(align=True) + row.operator("arm.open_editor", icon="DESKTOP") + row.operator("arm.open_project_folder", icon="FILE_FOLDER") + +class ARM_PT_ProjectFlagsPanel(bpy.types.Panel): + bl_label = "Flags" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_parent_id = "ARM_PT_ArmoryProjectPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + col = layout.column(heading='Debug', align=True) + col.prop(wrd, 'arm_verbose_output') + col.prop(wrd, 'arm_cache_build') + col.prop(wrd, 'arm_clear_on_compile') + col.prop(wrd, 'arm_assert_level') + col.prop(wrd, 'arm_assert_quit') + + col = layout.column(heading='Runtime', align=True) + col.prop(wrd, 'arm_live_patch') + col.prop(wrd, 'arm_stream_scene') + col.prop(wrd, 'arm_loadscreen') + col.prop(wrd, 'arm_write_config') + + col = layout.column(heading='Renderer', align=True) + col.prop(wrd, 'arm_batch_meshes') + col.prop(wrd, 'arm_batch_materials') + col.prop(wrd, 'arm_deinterleaved_buffers') + col.prop(wrd, 'arm_export_tangents') + + col = layout.column(heading='Quality') + col.prop(wrd, 'arm_texture_quality') + col.prop(wrd, 'arm_sound_quality') + + col = layout.column(heading='External Assets') + col.prop(wrd, 'arm_copy_override') + col.operator('arm.copy_to_bundled', icon='IMAGE_DATA') + +class ARM_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): + bl_label = "Debug Console" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ProjectFlagsPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + self.layout.prop(wrd, 'arm_debug_console', text='') + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + col = layout.column() + col.enabled = wrd.arm_debug_console + col.prop(wrd, 'arm_debug_console_position') + col.prop(wrd, 'arm_debug_console_scale') + col.prop(wrd, 'arm_debug_console_visible') + col.prop(wrd, 'arm_debug_console_trace_pos') + +class ARM_PT_ProjectWindowPanel(bpy.types.Panel): + bl_label = "Window" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ArmoryProjectPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + layout.prop(wrd, 'arm_winmode') + + col = layout.column(align=True) + col.prop(wrd, 'arm_winresize') + sub = col.column() + sub.enabled = wrd.arm_winresize + sub.prop(wrd, 'arm_winmaximize') + col.enabled = True + col.prop(wrd, 'arm_winminimize') + + layout.prop(wrd, 'arm_vsync') + +class ARM_PT_ProjectModulesPanel(bpy.types.Panel): + bl_label = "Modules" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_ArmoryProjectPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + layout.prop(wrd, 'arm_audio') + layout.prop(wrd, 'arm_physics') + if wrd.arm_physics != 'Disabled': + layout.prop(wrd, 'arm_physics_engine') + layout.prop(wrd, 'arm_navigation') + if wrd.arm_navigation != 'Disabled': + layout.prop(wrd, 'arm_navigation_engine') + layout.prop(wrd, 'arm_ui') + layout.prop(wrd, 'arm_network') + + layout.prop_search(wrd, 'arm_khafile', bpy.data, 'texts') + layout.prop(wrd, 'arm_project_root') + +class ArmVirtualInputPanel(bpy.types.Panel): + bl_label = "Armory Virtual Input" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + +class ArmoryPlayButton(bpy.types.Operator): + '''Launch player in new window''' + bl_idname = 'arm.play' + bl_label = 'Play' + + def invoke(self, context, event): + if event.shift: + state.is_play = True + make.build_success() + return{'FINISHED'} + return self.execute(context) + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + + if state.proc_build is not None: + return {"CANCELLED"} + + arm.utils.check_blender_version(self) + + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + arm.utils.check_projectpath(None) + + arm.utils.check_default_props() + + assets.invalidate_enabled = False + if wrd.arm_clear_on_compile: + os.system("cls") + make.play() + assets.invalidate_enabled = True + return{'FINISHED'} + +class ArmoryStopButton(bpy.types.Operator): + '''Stop currently running player''' + bl_idname = 'arm.stop' + bl_label = 'Stop' + + def execute(self, context): + if state.proc_play != None: + state.proc_play.terminate() + state.proc_play = None + elif state.proc_build != None: + state.proc_build.terminate() + state.proc_build = None + + arm.write_probes.check_last_cmft_time() + + return {'FINISHED'} + +class ArmoryBuildProjectButton(bpy.types.Operator): + """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): + arm.utils.check_blender_version(self) + + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + arm.utils.check_projectpath(self) + + arm.utils.check_default_props() + + wrd = bpy.data.worlds['Arm'] + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + if item.arm_project_rp == '': + item.arm_project_rp = wrd.arm_rplist[wrd.arm_rplist_index].name + if item.arm_project_scene == None: + item.arm_project_scene = context.scene + # Assume unique rp names + rplist_index = wrd.arm_rplist_index + for i in range(0, len(wrd.arm_rplist)): + if wrd.arm_rplist[i].name == item.arm_project_rp: + wrd.arm_rplist_index = i + break + assets.invalidate_shader_cache(None, None) + assets.invalidate_enabled = False + if wrd.arm_clear_on_compile: + os.system("cls") + make.build(item.arm_project_target, is_export=True) + make.compile() + wrd.arm_rplist_index = rplist_index + assets.invalidate_enabled = True + return{'FINISHED'} + +class ArmoryPublishProjectButton(bpy.types.Operator): + """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): + arm.utils.check_blender_version(self) + + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + self.report({'INFO'}, 'Publishing project, check console for details.') + + arm.utils.check_projectpath(self) + + arm.utils.check_default_props() + + wrd = bpy.data.worlds['Arm'] + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + if item.arm_project_rp == '': + item.arm_project_rp = wrd.arm_rplist[wrd.arm_rplist_index].name + if item.arm_project_scene == None: + item.arm_project_scene = context.scene + # Assume unique rp names + rplist_index = wrd.arm_rplist_index + for i in range(0, len(wrd.arm_rplist)): + if wrd.arm_rplist[i].name == item.arm_project_rp: + wrd.arm_rplist_index = i + break + + make.clean() + assets.invalidate_enabled = False + if wrd.arm_clear_on_compile: + os.system("cls") + make.build(item.arm_project_target, is_publish=True, is_export=True) + make.compile() + wrd.arm_rplist_index = rplist_index + assets.invalidate_enabled = True + return{'FINISHED'} + +class ArmoryOpenProjectFolderButton(bpy.types.Operator): + '''Open project folder''' + bl_idname = 'arm.open_project_folder' + bl_label = 'Project Folder' + + def execute(self, context): + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + arm.utils.open_folder(arm.utils.get_fp()) + return{'FINISHED'} + +class ArmoryOpenEditorButton(bpy.types.Operator): + '''Launch this project in the IDE''' + bl_idname = 'arm.open_editor' + bl_label = 'Code Editor' + bl_description = 'Open Project in IDE' + + def execute(self, context): + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + arm.utils.check_default_props() + + if not os.path.exists(arm.utils.get_fp() + "/khafile.js"): + print('Generating Krom project for IDE build configuration') + make.build('krom') + + arm.utils.open_editor() + return{'FINISHED'} + +class CleanMenu(bpy.types.Menu): + bl_label = "Ok?" + bl_idname = "OBJECT_MT_clean_menu" + + def draw(self, context): + layout = self.layout + layout.operator("arm.clean_project") + +class CleanButtonMenu(bpy.types.Operator): + '''Clean cached data''' + bl_label = "Clean" + bl_idname = "arm.clean_menu" + + def execute(self, context): + bpy.ops.wm.call_menu(name=CleanMenu.bl_idname) + return {"FINISHED"} + +class ArmoryCleanProjectButton(bpy.types.Operator): + '''Delete all cached project data''' + bl_idname = 'arm.clean_project' + bl_label = 'Clean Project' + + def execute(self, context): + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + make.clean() + return{'FINISHED'} + +def draw_view3d_header(self, context): + if state.proc_build is not None: + self.layout.label(text='Compiling..') + elif log.info_text != '': + self.layout.label(text=log.info_text) + +def draw_view3d_object_menu(self, context): + self.layout.separator() + self.layout.operator_context = 'INVOKE_DEFAULT' + self.layout.operator('arm.copy_traits_to_active') + +class ARM_PT_TopbarPanel(bpy.types.Panel): + bl_label = "Armory Player" + bl_space_type = "VIEW_3D" + bl_region_type = "WINDOW" + bl_options = {'INSTANCED'} + + def draw_header(self, context): + row = self.layout.row(align=True) + if state.proc_play is None and state.proc_build is None: + row.operator("arm.play", icon="PLAY", text="") + else: + row.operator("arm.stop", icon="SEQUENCE_COLOR_01", text="") + row.operator("arm.open_editor", icon="DESKTOP", text="") + row.operator("arm.open_project_folder", icon="FILE_FOLDER", text="") + + def draw(self, context): + col = self.layout.column() + wrd = bpy.data.worlds['Arm'] + + col.label(text="Armory Launch") + col.separator() + + col.prop(wrd, 'arm_runtime') + col.prop(wrd, 'arm_play_camera') + col.prop(wrd, 'arm_play_scene') + col.prop_search(wrd, 'arm_play_renderpath', wrd, 'arm_rplist', text='Render Path') + col.prop(wrd, 'arm_debug_console', text="Debug Console") + +def draw_space_topbar(self, context): + # for some blender reasons, topbar is instanced twice. this avoids doubling the panel + if context.region.alignment == 'RIGHT': + self.layout.popover(panel="ARM_PT_TopbarPanel", text="") + +class ARM_PT_RenderPathPanel(bpy.types.Panel): + bl_label = "Armory Render Path" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + rows = 2 + if len(wrd.arm_rplist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_RPList", "The_List", wrd, "arm_rplist", wrd, "arm_rplist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_rplist.new_item", icon='ADD', text="") + col.operator("arm_rplist.delete_item", icon='REMOVE', text="") + + if len(wrd.arm_rplist) > 1: + col.separator() + op = col.operator("arm_rplist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_rplist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if wrd.arm_rplist_index < 0 or len(wrd.arm_rplist) == 0: + return + + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + if len(arm.api.drivers) > 0: + layout.prop_search(rpdat, "rp_driver", wrd, "rp_driver_list", text="Driver") + layout.separator() + if rpdat.rp_driver != 'Armory' and arm.api.drivers[rpdat.rp_driver]['draw_props'] != None: + arm.api.drivers[rpdat.rp_driver]['draw_props'](layout) + return + +class ARM_PT_RenderPathRendererPanel(bpy.types.Panel): + bl_label = "Renderer" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.prop(rpdat, 'rp_renderer') + if rpdat.rp_renderer == 'Forward': + layout.prop(rpdat, 'rp_depthprepass') + layout.prop(rpdat, 'arm_material_model') + layout.prop(rpdat, 'rp_translucency_state') + layout.prop(rpdat, 'rp_overlays_state') + layout.prop(rpdat, 'rp_decals_state') + layout.prop(rpdat, 'rp_blending_state') + layout.prop(rpdat, 'rp_depth_texture_state') + layout.prop(rpdat, 'rp_draw_order') + layout.prop(rpdat, 'arm_samples_per_pixel') + layout.prop(rpdat, 'arm_texture_filter') + layout.prop(rpdat, 'rp_sss_state') + col = layout.column() + col.enabled = rpdat.rp_sss_state != 'Off' + col.prop(rpdat, 'arm_sss_width') + layout.prop(rpdat, 'arm_rp_displacement') + if rpdat.arm_rp_displacement == 'Tessellation': + layout.label(text='Mesh') + layout.prop(rpdat, 'arm_tess_mesh_inner') + layout.prop(rpdat, 'arm_tess_mesh_outer') + layout.label(text='Shadow') + layout.prop(rpdat, 'arm_tess_shadows_inner') + layout.prop(rpdat, 'arm_tess_shadows_outer') + + layout.prop(rpdat, 'arm_particles') + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, 'arm_skin') + col = col.column() + col.enabled = rpdat.arm_skin == 'On' + col.prop(rpdat, 'arm_use_armature_deform_only') + col.prop(rpdat, 'arm_skin_max_bones_auto') + row = col.row() + row.enabled = not rpdat.arm_skin_max_bones_auto + row.prop(rpdat, 'arm_skin_max_bones') + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, 'arm_morph_target') + col = col.column() + col.enabled = rpdat.arm_morph_target == 'On' + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, "rp_hdr") + col.prop(rpdat, "rp_stereo") + col.prop(rpdat, 'arm_culling') + col.prop(rpdat, 'rp_pp') + + +class ARM_PT_RenderPathShadowsPanel(bpy.types.Panel): + bl_label = "Shadows" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + self.layout.prop(rpdat, "rp_shadows", text="") + + def compute_subdivs(self, max, subdivs): + l = [max] + for i in range(subdivs - 1): + l.append(int(max / 2)) + max = max / 2 + return l + + def tiles_per_light_type(self, rpdat: arm.props_renderpath.ArmRPListItem, light_type: str) -> int: + if light_type == 'point': + return 6 + elif light_type == 'spot': + return 1 + else: + return int(rpdat.rp_shadowmap_cascades) + + def lights_number_atlas(self, rpdat: arm.props_renderpath.ArmRPListItem, atlas_size: int, shadowmap_size: int, light_type: str) -> int: + '''Compute number lights that could fit in an atlas''' + lights = atlas_size / shadowmap_size + lights *= lights / self.tiles_per_light_type(rpdat, light_type) + return int(lights) + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.enabled = rpdat.rp_shadows + col = layout.column() + col.enabled = not rpdat.rp_shadowmap_atlas_single_map or not rpdat.rp_shadowmap_atlas + col.prop(rpdat, 'rp_shadowmap_cube') + layout.prop(rpdat, 'rp_shadowmap_cascade') + layout.prop(rpdat, 'rp_shadowmap_cascades') + col = layout.column() + col2 = col.column() + col2.enabled = rpdat.rp_shadowmap_cascades != '1' + col2.prop(rpdat, 'arm_shadowmap_split') + col.prop(rpdat, 'arm_shadowmap_bounds') + col.prop(rpdat, 'arm_pcfsize') + layout.separator() + + layout.prop(rpdat, 'rp_shadowmap_atlas') + colatlas = layout.column() + colatlas.enabled = rpdat.rp_shadowmap_atlas + colatlas.prop(rpdat, 'rp_max_lights') + colatlas.prop(rpdat, 'rp_max_lights_cluster') + colatlas.prop(rpdat, 'rp_shadowmap_atlas_lod') + + colatlas_lod = colatlas.column() + colatlas_lod.enabled = rpdat.rp_shadowmap_atlas_lod + colatlas_lod.prop(rpdat, 'rp_shadowmap_atlas_lod_subdivisions') + + colatlas_lod_info = colatlas_lod.row() + colatlas_lod_info.alignment = 'RIGHT' + subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cascade), int(rpdat.rp_shadowmap_atlas_lod_subdivisions)) + subdiv_text = "Subdivisions for spot lights: " + ', '.join(map(str, subdivs_list)) + colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH") + + if not rpdat.rp_shadowmap_atlas_single_map: + colatlas_lod_info = colatlas_lod.row() + colatlas_lod_info.alignment = 'RIGHT' + subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cube), int(rpdat.rp_shadowmap_atlas_lod_subdivisions)) + subdiv_text = "Subdivisions for point lights: " + ', '.join(map(str, subdivs_list)) + colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH") + + size_warning = int(rpdat.rp_shadowmap_cascade) > 2048 or int(rpdat.rp_shadowmap_cube) > 2048 + + colatlas.prop(rpdat, 'rp_shadowmap_atlas_single_map') + # show size for single texture + if rpdat.rp_shadowmap_atlas_single_map: + colatlas_single = colatlas.column() + colatlas_single.prop(rpdat, 'rp_shadowmap_atlas_max_size') + if rpdat.rp_shadowmap_atlas_max_size != '': + atlas_size = int(rpdat.rp_shadowmap_atlas_max_size) + shadowmap_size = int(rpdat.rp_shadowmap_cascade) + + if shadowmap_size > 2048: + size_warning = True + + point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') + spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') + dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') + + col = colatlas_single.row() + col.alignment = 'RIGHT' + col.label(text=f'Enough space for { point_lights } point lights or { spot_lights } spot lights or { dir_lights } directional lights.') + else: + # show size for all types + colatlas_mixed = colatlas.column() + colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_spot') + + if rpdat.rp_shadowmap_atlas_max_size_spot != '': + atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_spot) + shadowmap_size = int(rpdat.rp_shadowmap_cascade) + spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') + + if shadowmap_size > 2048: + size_warning = True + + col = colatlas_mixed.row() + col.alignment = 'RIGHT' + col.label(text=f'Enough space for {spot_lights} spot lights.') + + colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_point') + + if rpdat.rp_shadowmap_atlas_max_size_point != '': + atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_point) + shadowmap_size = int(rpdat.rp_shadowmap_cube) + point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') + + if shadowmap_size > 2048: + size_warning = True + + col = colatlas_mixed.row() + col.alignment = 'RIGHT' + col.label(text=f'Enough space for {point_lights} point lights.') + + colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_sun') + + if rpdat.rp_shadowmap_atlas_max_size_sun != '': + atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_sun) + shadowmap_size = int(rpdat.rp_shadowmap_cascade) + dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') + + if shadowmap_size > 2048: + size_warning = True + + col = colatlas_mixed.row() + col.alignment = 'RIGHT' + col.label(text=f'Enough space for {dir_lights} directional lights.') + + # show warning when user picks a size higher than 2048 (arbitrary number). + if size_warning: + col = layout.column() + row = col.row() + row.alignment = 'RIGHT' + row.label(text='Warning: Game will crash if texture size is higher than max texture size allowed by target.', icon='ERROR') + +class ARM_PT_RenderPathVoxelsPanel(bpy.types.Panel): + bl_label = "Voxel AO" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + self.layout.prop(rpdat, "rp_voxels", text="") + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.enabled = rpdat.rp_voxels + layout.prop(rpdat, 'arm_voxelgi_shadows') + layout.prop(rpdat, 'arm_voxelgi_cones') + layout.prop(rpdat, 'rp_voxelgi_resolution') + layout.prop(rpdat, 'rp_voxelgi_resolution_z') + layout.prop(rpdat, 'arm_voxelgi_dimensions') + layout.prop(rpdat, 'arm_voxelgi_revoxelize') + col2 = layout.column() + col2.enabled = rpdat.arm_voxelgi_revoxelize + col2.prop(rpdat, 'arm_voxelgi_camera') + col2.prop(rpdat, 'arm_voxelgi_temporal') + layout.prop(rpdat, 'arm_voxelgi_occ') + layout.prop(rpdat, 'arm_voxelgi_step') + layout.prop(rpdat, 'arm_voxelgi_range') + layout.prop(rpdat, 'arm_voxelgi_offset') + layout.prop(rpdat, 'arm_voxelgi_aperture') + +class ARM_PT_RenderPathWorldPanel(bpy.types.Panel): + bl_label = "World" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.prop(rpdat, "rp_background") + + col = layout.column() + col.prop(rpdat, 'arm_irradiance') + colb = col.column() + colb.enabled = rpdat.arm_irradiance + colb.prop(rpdat, 'arm_radiance') + sub = colb.row() + sub.enabled = rpdat.arm_radiance + sub.prop(rpdat, 'arm_radiance_size') + layout.separator() + + layout.prop(rpdat, 'arm_clouds') + + col = layout.column(align=True) + col.prop(rpdat, "rp_water") + col = col.column(align=True) + col.enabled = rpdat.rp_water + col.prop(rpdat, 'arm_water_level') + col.prop(rpdat, 'arm_water_density') + col.prop(rpdat, 'arm_water_displace') + col.prop(rpdat, 'arm_water_speed') + col.prop(rpdat, 'arm_water_freq') + col.prop(rpdat, 'arm_water_refract') + col.prop(rpdat, 'arm_water_reflect') + col.prop(rpdat, 'arm_water_color') + +class ARM_PT_RenderPathPostProcessPanel(bpy.types.Panel): + bl_label = "Post Process" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + self.layout.prop(rpdat, "rp_render_to_texture", text="") + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.enabled = rpdat.rp_render_to_texture + col = layout.column() + col.prop(rpdat, "rp_antialiasing") + col.prop(rpdat, "rp_supersampling") + + col = layout.column() + col.prop(rpdat, 'arm_rp_resolution') + if rpdat.arm_rp_resolution == 'Custom': + col.prop(rpdat, 'arm_rp_resolution_size') + col.prop(rpdat, 'arm_rp_resolution_filter') + col.prop(rpdat, 'rp_dynres') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_ssgi") + sub = col.column() + sub.enabled = rpdat.rp_ssgi != 'Off' + sub.prop(rpdat, 'arm_ssgi_half_res') + sub.prop(rpdat, 'arm_ssgi_rays') + sub.prop(rpdat, 'arm_ssgi_radius') + sub.prop(rpdat, 'arm_ssgi_strength') + sub.prop(rpdat, 'arm_ssgi_max_steps') + layout.separator() + + row = layout.row() + row.enabled = rpdat.arm_material_model == 'Full' + row.prop(rpdat, 'arm_micro_shadowing') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_ssr") + col.prop(rpdat, 'arm_ssr_half_res') + col = col.column() + col.enabled = rpdat.rp_ssr + col.prop(rpdat, 'arm_ssr_ray_step') + col.prop(rpdat, 'arm_ssr_search_dist') + col.prop(rpdat, 'arm_ssr_falloff_exp') + col.prop(rpdat, 'arm_ssr_jitter') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_ss_refraction") + col = col.column() + col.enabled = rpdat.rp_ss_refraction + col.prop(rpdat, 'arm_ss_refraction_ray_step') + col.prop(rpdat, 'arm_ss_refraction_search_dist') + col.prop(rpdat, 'arm_ss_refraction_falloff_exp') + col.prop(rpdat, 'arm_ss_refraction_jitter') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_ssrs') + col = col.column() + col.enabled = rpdat.arm_ssrs + col.prop(rpdat, 'arm_ssrs_ray_step') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_bloom") + _col = col.column() + _col.enabled = rpdat.rp_bloom + _col.prop(rpdat, 'arm_bloom_follow_blender') + if not rpdat.arm_bloom_follow_blender: + _col.prop(rpdat, 'arm_bloom_threshold') + _col.prop(rpdat, 'arm_bloom_knee') + _col.prop(rpdat, 'arm_bloom_radius') + _col.prop(rpdat, 'arm_bloom_strength') + _col.prop(rpdat, 'arm_bloom_quality') + _col.prop(rpdat, 'arm_bloom_anti_flicker') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_motionblur") + col = col.column() + col.enabled = rpdat.rp_motionblur != 'Off' + col.prop(rpdat, 'arm_motion_blur_intensity') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_volumetriclight") + col = col.column() + col.enabled = rpdat.rp_volumetriclight + col.prop(rpdat, 'arm_volumetric_light_air_color') + col.prop(rpdat, 'arm_volumetric_light_air_turbidity') + col.prop(rpdat, 'arm_volumetric_light_steps') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_chromatic_aberration") + col = col.column() + col.enabled = rpdat.rp_chromatic_aberration + col.prop(rpdat, 'arm_chromatic_aberration_type') + col.prop(rpdat, 'arm_chromatic_aberration_strength') + if rpdat.arm_chromatic_aberration_type == "Spectral": + col.prop(rpdat, 'arm_chromatic_aberration_samples') + +class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): + bl_label = "Compositor" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_RenderPathPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + self.layout.prop(rpdat, "rp_compositornodes", text="") + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) <= wrd.arm_rplist_index: + return + rpdat = wrd.arm_rplist[wrd.arm_rplist_index] + + layout.enabled = rpdat.rp_compositornodes + layout.prop(rpdat, 'arm_tonemap') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_letterbox') + col = col.column(align=True) + col.enabled = rpdat.arm_letterbox + col.prop(rpdat, 'arm_letterbox_color') + col.prop(rpdat, 'arm_letterbox_size') + layout.separator() + + col = layout.column() + draw_conditional_prop(col, 'Distort', rpdat, 'arm_distort', 'arm_distort_strength') + draw_conditional_prop(col, 'Film Grain', rpdat, 'arm_grain', 'arm_grain_strength') + draw_conditional_prop(col, 'Sharpen', rpdat, 'arm_sharpen', 'arm_sharpen_strength') + draw_conditional_prop(col, 'Vignette', rpdat, 'arm_vignette', 'arm_vignette_strength') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_fog') + col = col.column(align=True) + col.enabled = rpdat.arm_fog + col.prop(rpdat, 'arm_fog_color') + col.prop(rpdat, 'arm_fog_amounta') + col.prop(rpdat, 'arm_fog_amountb') + layout.separator() + + col = layout.column() + col.prop(rpdat, "rp_autoexposure") + sub = col.column(align=True) + sub.enabled = rpdat.rp_autoexposure + sub.prop(rpdat, 'arm_autoexposure_strength', text='Strength') + sub.prop(rpdat, 'arm_autoexposure_speed', text='Speed') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_fisheye') + col.prop(rpdat, 'arm_lensflare') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_lens') + col = col.column(align=True) + col.enabled = rpdat.arm_lens + col.prop(rpdat, 'arm_lens_texture') + if rpdat.arm_lens_texture != "": + col.prop(rpdat, 'arm_lens_texture_masking') + if rpdat.arm_lens_texture_masking: + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_centerMinClip') + sub.prop(rpdat, 'arm_lens_texture_masking_centerMaxClip') + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMin') + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMax') + col.prop(rpdat, 'arm_lens_texture_masking_brightnessExp') + layout.separator() + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_lut') + col = col.column(align=True) + col.enabled = rpdat.arm_lut + col.prop(rpdat, 'arm_lut_texture') + layout.separator() + +class ARM_PT_BakePanel(bpy.types.Panel): + bl_label = "Armory Bake" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + scn = bpy.data.scenes[context.scene.name] + + row = layout.row(align=True) + row.prop(scn, "arm_bakemode", expand=True) + + if scn.arm_bakemode == "Static Map": + + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("arm.bake_textures", icon="RENDER_STILL") + row.operator("arm.bake_apply") + + col = layout.column() + col.prop(scn, 'arm_bakelist_scale') + col.prop(scn.cycles, "samples") + + layout.prop(scn, 'arm_bakelist_unwrap') + + rows = 2 + if len(scn.arm_bakelist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_BakeList", "The_List", scn, "arm_bakelist", scn, "arm_bakelist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_bakelist.new_item", icon='ADD', text="") + col.operator("arm_bakelist.delete_item", icon='REMOVE', text="") + col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") + + if len(scn.arm_bakelist) > 1: + col.separator() + op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if scn.arm_bakelist_index >= 0 and len(scn.arm_bakelist) > 0: + item = scn.arm_bakelist[scn.arm_bakelist_index] + layout.prop_search(item, "obj", bpy.data, "objects", text="Object") + layout.prop(item, "res_x") + layout.prop(item, "res_y") + +class ArmGenLodButton(bpy.types.Operator): + """Automatically generate LoD levels.""" + bl_idname = 'arm.generate_lod' + bl_label = 'Auto Generate' + + def lod_name(self, name, level): + return name + '_LOD' + str(level + 1) + + def execute(self, context): + obj = context.object + if obj == None: + return{'CANCELLED'} + + # Clear + mdata = context.object.data + mdata.arm_lodlist_index = 0 + mdata.arm_lodlist.clear() + + # Lod levels + wrd = bpy.data.worlds['Arm'] + ratio = wrd.arm_lod_gen_ratio + num_levels = wrd.arm_lod_gen_levels + for level in range(0, num_levels): + new_obj = obj.copy() + for i in range(0, 3): + new_obj.location[i] = 0 + new_obj.rotation_euler[i] = 0 + new_obj.scale[i] = 1 + new_obj.data = obj.data.copy() + new_obj.name = self.lod_name(obj.name, level) + new_obj.parent = obj + new_obj.hide_viewport = True + new_obj.hide_render = True + mod = new_obj.modifiers.new('Decimate', 'DECIMATE') + mod.ratio = ratio + ratio *= wrd.arm_lod_gen_ratio + context.scene.collection.objects.link(new_obj) + + # Screen sizes + for level in range(0, num_levels): + mdata.arm_lodlist.add() + mdata.arm_lodlist[-1].name = self.lod_name(obj.name, level) + mdata.arm_lodlist[-1].screen_size_prop = (1 - (1 / (num_levels + 1)) * level) - (1 / (num_levels + 1)) + + return{'FINISHED'} + +class ARM_PT_LodPanel(bpy.types.Panel): + bl_label = "Armory Lod" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = bpy.context.object + + # Mesh only for now + if obj.type != 'MESH': + return + + mdata = obj.data + + rows = 2 + if len(mdata.arm_lodlist) > 1: + rows = 4 + + row = layout.row() + row.template_list("ARM_UL_LodList", "The_List", mdata, "arm_lodlist", mdata, "arm_lodlist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_lodlist.new_item", icon='ADD', text="") + col.operator("arm_lodlist.delete_item", icon='REMOVE', text="") + + if len(mdata.arm_lodlist) > 1: + col.separator() + op = col.operator("arm_lodlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_lodlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if mdata.arm_lodlist_index >= 0 and len(mdata.arm_lodlist) > 0: + item = mdata.arm_lodlist[mdata.arm_lodlist_index] + layout.prop_search(item, "name", bpy.data, "objects", text="Object") + layout.prop(item, "screen_size_prop") + layout.prop(mdata, "arm_lod_material") + + # Auto lod for meshes + if obj.type == 'MESH': + layout.separator() + layout.operator("arm.generate_lod") + wrd = bpy.data.worlds['Arm'] + layout.prop(wrd, 'arm_lod_gen_levels') + layout.prop(wrd, 'arm_lod_gen_ratio') + +class ArmGenTerrainButton(bpy.types.Operator): + '''Generate terrain sectors''' + bl_idname = 'arm.generate_terrain' + bl_label = 'Generate' + + def execute(self, context): + scn = context.scene + if scn == None: + return{'CANCELLED'} + sectors = scn.arm_terrain_sectors + size = scn.arm_terrain_sector_size + height_scale = scn.arm_terrain_height_scale + + # Create material + mat = bpy.data.materials.new(name="Terrain") + mat.use_nodes = True + nodes = mat.node_tree.nodes + links = mat.node_tree.links + node = nodes.new('ShaderNodeDisplacement') + node.location = (-200, 100) + node.inputs[2].default_value = height_scale + node.space = 'WORLD' + links.new(nodes['Material Output'].inputs[2], node.outputs[0]) + node = nodes.new('ShaderNodeTexImage') + node.location = (-600, 100) + node.interpolation = 'Closest' + node.extension = 'EXTEND' + node.arm_material_param = True + node.name = '_TerrainHeight' + node.label = '_TerrainHeight' # Height-map texture link for this sector + links.new(nodes['Displacement'].inputs[0], nodes['_TerrainHeight'].outputs[0]) + node = nodes.new('ShaderNodeBump') + node.location = (-200, -200) + node.inputs[0].default_value = 5.0 + links.new(nodes['Bump'].inputs[2], nodes['_TerrainHeight'].outputs[0]) + links.new(nodes['Principled BSDF'].inputs[20], nodes['Bump'].outputs[0]) + + # Create sectors + root_obj = bpy.data.objects.new("Terrain", None) + root_obj.location[0] = 0 + root_obj.location[1] = 0 + root_obj.location[2] = 0 + root_obj.arm_export = False + scn.collection.objects.link(root_obj) + scn.arm_terrain_object = root_obj + + for i in range(sectors[0] * sectors[1]): + j = str(i + 1).zfill(2) + x = i % sectors[0] + y = int(i / sectors[0]) + bpy.ops.mesh.primitive_plane_add(location=(x * size, -y * size, 0)) + slice_obj = bpy.context.active_object + slice_obj.scale[0] = size / 2 + slice_obj.scale[1] = -(size / 2) + slice_obj.scale[2] = height_scale + slice_obj.data.materials.append(mat) + for p in slice_obj.data.polygons: + p.use_smooth = True + slice_obj.name = 'Terrain.' + j + slice_obj.parent = root_obj + sub_mod = slice_obj.modifiers.new('Subdivision', 'SUBSURF') + sub_mod.subdivision_type = 'SIMPLE' + disp_mod = slice_obj.modifiers.new('Displace', 'DISPLACE') + disp_mod.texture_coords = 'UV' + disp_mod.texture = bpy.data.textures.new(name='Terrain.' + j, type='IMAGE') + disp_mod.texture.extension = 'EXTEND' + disp_mod.texture.use_interpolation = False + disp_mod.texture.use_mipmap = False + disp_mod.texture.image = bpy.data.images.load(filepath=scn.arm_terrain_textures+'/heightmap_' + j + '.png') + f = 1 + levels = 0 + while f < disp_mod.texture.image.size[0]: + f *= 2 + levels += 1 + sub_mod.levels = sub_mod.render_levels = levels + + return{'FINISHED'} + +class ARM_PT_TerrainPanel(bpy.types.Panel): + bl_label = "Armory Terrain" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + scn = bpy.context.scene + if scn == None: + return + layout.prop(scn, 'arm_terrain_textures') + layout.prop(scn, 'arm_terrain_sectors') + layout.prop(scn, 'arm_terrain_sector_size') + layout.prop(scn, 'arm_terrain_height_scale') + layout.operator('arm.generate_terrain') + layout.prop(scn, 'arm_terrain_object') + +class ARM_PT_TilesheetPanel(bpy.types.Panel): + bl_label = "Armory Tilesheet" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + rows = 2 + if len(wrd.arm_tilesheetlist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_TilesheetList", "The_List", wrd, "arm_tilesheetlist", wrd, "arm_tilesheetlist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_tilesheetlist.new_item", icon='ADD', text="") + col.operator("arm_tilesheetlist.delete_item", icon='REMOVE', text="") + + if len(wrd.arm_tilesheetlist) > 1: + col.separator() + op = col.operator("arm_tilesheetlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_tilesheetlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if wrd.arm_tilesheetlist_index >= 0 and len(wrd.arm_tilesheetlist) > 0: + dat = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] + layout.prop(dat, "tilesx_prop") + layout.prop(dat, "tilesy_prop") + layout.prop(dat, "framerate_prop") + + layout.label(text='Actions') + rows = 2 + if len(dat.arm_tilesheetactionlist) > 1: + rows = 4 + row = layout.row() + row.template_list("ARM_UL_TilesheetList", "The_List", dat, "arm_tilesheetactionlist", dat, "arm_tilesheetactionlist_index", rows=rows) + col = row.column(align=True) + col.operator("arm_tilesheetactionlist.new_item", icon='ADD', text="") + col.operator("arm_tilesheetactionlist.delete_item", icon='REMOVE', text="") + + if len(dat.arm_tilesheetactionlist) > 1: + col.separator() + op = col.operator("arm_tilesheetactionlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("arm_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + if dat.arm_tilesheetactionlist_index >= 0 and len(dat.arm_tilesheetactionlist) > 0: + adat = dat.arm_tilesheetactionlist[dat.arm_tilesheetactionlist_index] + layout.prop(adat, "start_prop") + layout.prop(adat, "end_prop") + layout.prop(adat, "loop_prop") + +class ArmPrintTraitsButton(bpy.types.Operator): + bl_idname = 'arm.print_traits' + bl_label = 'Print All Scenes Traits' + bl_description = 'Returns all traits in current blend' + + def execute(self, context): + for s in bpy.data.scenes: + print('Scene: {0}'.format(s.name)) + for o in s.objects: + for t in o.arm_traitlist: + if not t.enabled_prop: + continue + tname = "undefined" + if t.type_prop == 'Haxe Script' or "Bundled": + tname = t.class_name_prop + if t.type_prop == 'Logic Nodes': + tname = t.node_tree_prop.name + if t.type_prop == 'UI Canvas': + tname = t.canvas_name_prop + if t.type_prop == 'WebAssembly': + tname = t.webassembly_prop + print('Trait: {0} ("{1}")'.format(o.name, tname)) + return{'FINISHED'} + +class ARM_PT_MaterialNodePanel(bpy.types.Panel): + bl_label = 'Armory Material Node' + bl_idname = 'ARM_PT_MaterialNodePanel' + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = 'Armory' + + @classmethod + def poll(cls, context): + return (context.space_data.tree_type == 'ShaderNodeTree' + and context.space_data.edit_tree + and context.space_data.shader_type == 'OBJECT') + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + n = context.active_node + if n != None and (n.bl_idname == 'ShaderNodeRGB' or n.bl_idname == 'ShaderNodeValue' or n.bl_idname == 'ShaderNodeTexImage'): + layout.prop(context.active_node, 'arm_material_param') + +class ARM_OT_CopyToBundled(bpy.types.Operator): + bl_label = 'Copy To Bundled' + bl_idname = 'arm.copy_to_bundled' + bl_description = ('Copies and repaths external image assets to project Bundled folder') + + def execute(self, context): + self.copy_to_bundled(bpy.data.images) + return {'FINISHED'} + + @classmethod + def copy_to_bundled(self, data): + wrd = bpy.data.worlds['Arm'] + project_path = arm.utils.get_fp() + + # Blend - Images + for asset in data: + # File is saved + if asset.filepath_from_user() != '': + bundled_filepath = project_path + '/Bundled/' + asset.name + try: + # Exists -> Yes + if os.path.isfile(bundled_filepath): + # Override -> Yes + if (wrd.arm_copy_override): + # Valid file + if asset.has_data: + asset.filepath_raw = bundled_filepath + asset.save() + asset.reload() + # Syntax - Yellow + print(log.colorize(f'Asset name "{asset.name}" already exists, overriding the original', 33), file=sys.stderr) + # Invalid file or corrupted + else: + # Syntax - Red + log.error(f'Asset name "{asset.name}" has no data to save or copy, skipping') + continue + # Override -> No + else: + # Syntax - Yellow + print(log.colorize(f'Asset name "{asset.name}" already exists, skipping', 33), file=sys.stderr) + continue + # Exists -> No + else: + # Valid file + if asset.has_data: + asset.filepath_raw = bundled_filepath + asset.save() + asset.reload() + # Syntax - Green + print(log.colorize(f'Asset name "{asset.name}" was successfully copied', 32), file=sys.stderr) + # Invalid file or corrupted + else: + # Syntax - Red + log.error(f'Asset name "{asset.name}" has no data to save or copy, skipping') + continue + except: + # Syntax - Red + log.error(f'Insufficient write permissions or other issues occurred') + continue + # File is unsaved + else: + # Syntax - Purple + log.warn(f'Asset name "{asset.name}" is either packed or unsaved, skipping') + continue + +class ARM_OT_ShowFileVersionInfo(bpy.types.Operator): + bl_label = 'Show old file version info' + bl_idname = 'arm.show_old_file_version_info' + bl_description = ('Displays an info panel that warns about opening a file' + 'which was created in a previous version of Armory') + bl_options = {'INTERNAL'} + + wrd = None + + def draw_message_box(self, context): + file_version = ARM_OT_ShowFileVersionInfo.wrd.arm_version + current_version = props.arm_version + + + layout = self.layout + layout = layout.column(align=True) + layout.alignment = 'EXPAND' + + if current_version == file_version: + layout.label('This file was saved in', icon='INFO') + layout.label('the current Armory version', icon='BLANK1') + layout.separator() + layout.label(f'(version: {current_version}') + row = layout.row(align=True) + row.active_default = True + row.operator('arm.discard_popup', text='Ok') + + # this will help order versions better, somewhat. + # note: this is NOT complete + current_version = tuple( current_version.split('.') ) + file_version = tuple( file_version.split('.') ) + + if current_version > file_version: + layout.label(text='Warning: This file was saved in a', icon='ERROR') + layout.label(text='previous version of Armory!', icon='BLANK1') + layout.separator() + + layout.label(text='Please inform yourself about breaking changes!', icon='BLANK1') + layout.label(text=f'File saved in: {file_version}', icon='BLANK1') + layout.label(text=f'Current version: {current_version}', icon='BLANK1') + layout.separator() + layout.separator() + layout.label(text='Should Armory try to automatically update', icon='BLANK1') + layout.label(text='the file to the current SDK version?', icon='BLANK1') + layout.separator() + + row = layout.row(align=True) + row.active_default = True + row.operator('arm.update_file_sdk', text='Yes') + row.active_default = False + row.operator('arm.discard_popup', text='No') + else: + layout.label(text='Warning: This file was saved in a', icon='ERROR') + layout.label(text='future version of Armory!', icon='BLANK1') + layout.separator() + + layout.label(text='It is impossible to downgrade a file,', icon='BLANK1') + layout.label(text='Something will probably be broken here.', icon='BLANK1') + layout.label(text=f'File saved in: {file_version}', icon='BLANK1') + layout.label(text=f'Current version: {current_version}', icon='BLANK1') + layout.separator() + layout.separator() + layout.label(text='Please check how this file was created', icon='BLANK1') + layout.separator() + + row = layout.row(align=True) + row.active_default = True + row.operator('arm.discard_popup', text='Ok') + + def execute(self, context): + ARM_OT_ShowFileVersionInfo.wrd = bpy.data.worlds['Arm'] + context.window_manager.popover(ARM_OT_ShowFileVersionInfo.draw_message_box, ui_units_x=16) + + return {"FINISHED"} + +class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator): + bl_label = 'Show upgrade failure details' + bl_idname = 'arm.show_node_update_errors' + bl_description = ('Displays an info panel that shows the different errors that occurred when upgrading nodes') + + wrd = None # a helper internal variable + + def draw_message_box(self, context): + list_of_errors = arm.logicnode.replacement.replacement_errors.copy() + # note: list_of_errors is a set of tuples: `(error_type, node_class, tree_name)` + # where `error_type` can be "unregistered", "update failed", "future version", "bad version", or "misc." + + file_version = ARM_OT_ShowNodeUpdateErrors.wrd.arm_version + current_version = props.arm_version + + # this will help order versions better, somewhat. + # note: this is NOT complete + current_version_2 = tuple(current_version.split('.')) + file_version_2 = tuple(file_version.split('.')) + is_armory_upgrade = (current_version_2 > file_version_2) + + error_types = set() + errored_trees = set() + errored_nodes = set() + for error_entry in list_of_errors: + error_types.add(error_entry[0]) + errored_nodes.add(error_entry[1]) + errored_trees.add(error_entry[2]) + + layout = self.layout + layout = layout.column(align=True) + layout.alignment = 'EXPAND' + + layout.label(text="Some nodes failed to be updated to the current Armory version", icon="ERROR") + if current_version == file_version: + layout.label(text="(This might be because you are using a development snapshot, or a homemade version ;) )", icon='BLANK1') + elif not is_armory_upgrade: + layout.label(text="(Please note that it is not possible do downgrade nodes to a previous version either.", icon='BLANK1') + layout.label(text="This might be the cause of your problem.)", icon='BLANK1') + + layout.label(text=f'File saved in: {file_version}', icon='BLANK1') + layout.label(text=f'Current version: {current_version}', icon='BLANK1') + layout.separator() + + if 'update failed' in error_types: + layout.label(text="Some nodes do not have an update procedure to deal with the version saved in this file.", icon='BLANK1') + if current_version == file_version: + layout.label(text="(if you are a developer, this might be because you didn't implement it yet.)", icon='BLANK1') + if 'bad version' in error_types: + layout.label(text="Some nodes do not have version information attached to them.", icon='BLANK1') + if 'unregistered' in error_types: + if is_armory_upgrade: + layout.label(text='Some nodes seem to be too old to be understood by armory anymore', icon='BLANK1') + else: + layout.label(text="Some nodes are unknown to armory, either because they are too new or too old.", icon='BLANK1') + if 'future version' in error_types: + if is_armory_upgrade: + layout.label(text='Somehow, some nodes seem to have been created with a future version of armory.', icon='BLANK1') + else: + layout.label(text='Some nodes seem to have been created with a future version of armory.', icon='BLANK1') + if 'misc.' in error_types: + layout.label(text="Some nodes' update procedure failed to complete") + + layout.separator() + layout.label(text='the nodes impacted are the following:', icon='BLANK1') + for node in errored_nodes: + layout.label(text=f' {node}', icon='BLANK1') + layout.separator() + layout.label(text='the node trees impacted are the following:', icon='BLANK1') + for tree in errored_trees: + layout.label(text=f' "{tree}"', icon='BLANK1') + + layout.separator() + layout.label(text="A detailed error report has been saved next to the blender file.", icon='BLANK1') + layout.label(text="the file name is \"node_update_failure\", followed by the current time.", icon='BLANK1') + layout.separator() + + row = layout.row(align=True) + row.active_default = False + row.operator('arm.discard_popup', text='Ok') + row.operator('arm.open_project_folder', text='Open Project Folder', icon="FILE_FOLDER") + + def execute(self, context): + ARM_OT_ShowNodeUpdateErrors.wrd = bpy.data.worlds['Arm'] + context.window_manager.popover(ARM_OT_ShowNodeUpdateErrors.draw_message_box, ui_units_x=32) + return {"FINISHED"} + +class ARM_OT_UpdateFileSDK(bpy.types.Operator): + bl_idname = 'arm.update_file_sdk' + bl_label = 'Update file to current SDK version' + bl_description = bl_label + bl_options = {'INTERNAL'} + + def execute(self, context): + wrd = bpy.data.worlds['Arm'] + # This allows for seamless migration from earlier versions of Armory + for rp in wrd.arm_rplist: # TODO: deprecated + if rp.rp_gi != 'Off': + rp.rp_gi = 'Off' + rp.rp_voxelao = True + + # Replace deprecated nodes + arm.logicnode.replacement.replace_all() + + wrd.arm_version = props.arm_version + wrd.arm_commit = props.arm_commit + + arm.make.clean() + print(f'Project updated to SDK {props.arm_version}. Please save the .blend file.') + + return {'FINISHED'} + +class ARM_OT_DiscardPopup(bpy.types.Operator): + """Empty operator for discarding dialogs.""" + bl_idname = 'arm.discard_popup' + bl_label = 'OK' + bl_description = 'Discard' + bl_options = {'INTERNAL'} + + def execute(self, context): + return {'FINISHED'} + +class ArmoryUpdateListAndroidEmulatorButton(bpy.types.Operator): + '''Updating the list of emulators for the Android platform''' + bl_idname = 'arm.update_list_android_emulator' + bl_label = 'Update List Emulators' + + def execute(self, context): + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + if len(arm.utils.get_android_sdk_root_path()) == 0: + return {"CANCELLED"} + + os.environ['ANDROID_SDK_ROOT'] = arm.utils.get_android_sdk_root_path() + items, err = arm.utils.get_android_emulators_list() + if len(err) > 0: + print('Update List Emulators Warning: File "'+ arm.utils.get_android_emulator_file() +'" not found. Check that the variable ANDROID_SDK_ROOT is correct in environment variables or in "Android SDK Path" setting: \n- If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender;\n- If you specify the setting "Android SDK Path", then repeat operation "Publish"') + return{'FINISHED'} + if len(items) > 0: + items_enum = [] + for i in items: + items_enum.append((i, i, i)) + bpy.types.World.arm_project_android_list_avd = EnumProperty(items=items_enum, name="Emulator", update=assets.invalidate_compiler_cache) + return{'FINISHED'} + +class ArmoryUpdateListAndroidEmulatorRunButton(bpy.types.Operator): + '''Launch Android emulator selected from the list''' + bl_idname = 'arm.run_android_emulator' + bl_label = 'Launch Emulator' + + def execute(self, context): + if not arm.utils.check_saved(self): + return {"CANCELLED"} + + if not arm.utils.check_sdkpath(self): + return {"CANCELLED"} + + if len(arm.utils.get_android_sdk_root_path()) == 0: + return {"CANCELLED"} + + make.run_android_emulators(arm.utils.get_android_emulator_name()) + return{'FINISHED'} + + +class ArmoryUpdateListInstalledVSButton(bpy.types.Operator): + """Update the list of installed Visual Studio versions for the Windows platform""" + bl_idname = 'arm.update_list_installed_vs' + bl_label = '(Re-)Fetch Installed Visual Studio Versions' + + def execute(self, context): + if not arm.utils.get_os_is_windows(): + return {"CANCELLED"} + + success = arm.utils_vs.fetch_installed_vs() + if not success: + self.report({"ERROR"}, "Could not fetch installed Visual Studio versions, check console for details.") + return {'CANCELLED'} + + return {'FINISHED'} + +def draw_custom_node_menu(self, context): + """Extension of the node context menu. + + https://blender.stackexchange.com/questions/150101/python-how-to-add-items-in-context-menu-in-2-8 + """ + if context.selected_nodes is None or len(context.selected_nodes) != 1: + return + + if context.space_data.tree_type == 'ArmLogicTreeType': + if context.selected_nodes[0].bl_idname.startswith('LN'): + layout = self.layout + layout.separator() + layout.operator("arm.open_node_documentation", text="Show documentation for this node", icon='HELP') + layout.operator("arm.open_node_haxe_source", text="Open .hx source in the browser", icon_value=ui_icons.get_id("haxe")) + layout.operator("arm.open_node_python_source", text="Open .py source in the browser", icon='FILE_SCRIPT') + + elif context.space_data.tree_type == 'ShaderNodeTree': + if context.active_node.bl_idname in ('ShaderNodeRGB', 'ShaderNodeValue', 'ShaderNodeTexImage'): + layout = self.layout + layout.separator() + layout.prop(context.active_node, 'arm_material_param', text='Armory: Material Parameter') + +def draw_conditional_prop(layout: bpy.types.UILayout, heading: str, data: bpy.types.AnyType, prop_condition: str, prop_value: str) -> None: + """Draws a property row with a checkbox that enables a value field. + The function fails when prop_condition is not a boolean property. + """ + col = layout.column(heading=heading) + row = col.row() + row.prop(data, prop_condition, text='') + sub = row.row() + sub.enabled = getattr(data, prop_condition) + sub.prop(data, prop_value, expand=True) + + +def draw_error_box(layout: bpy.types.UILayout, text: str) -> bpy.types.UILayout: + """Draw an error box in the given UILayout and return it for + further optional modification. The text is wrapped automatically + according to the current region's width. + """ + textwrap_width = max(0, int((bpy.context.region.width - 25) / 6)) + lines = textwrap.wrap(text, width=textwrap_width, break_long_words=True) + + box = layout.box() + col = box.column(align=True) + col.alert = True + for idx, line in enumerate(lines): + col.label(text=line, icon='ERROR' if idx == 0 else 'BLANK1') + + return box + + +def draw_multiline_with_icon(layout: bpy.types.UILayout, layout_width_px: int, icon: str, text: str) -> bpy.types.UILayout: + """Draw a multiline string with an icon in the given UILayout + and return it for further optional modification. + The text is wrapped according to the given layout width. + """ + textwrap_width = max(0, layout_width_px // 6) + lines = textwrap.wrap(text, width=textwrap_width, break_long_words=True) + + col = layout.column(align=True) + col.scale_y = 0.8 + for idx, line in enumerate(lines): + col.label(text=line, icon=icon if idx == 0 else 'BLANK1') + + return col + + +def register(): + bpy.utils.register_class(ARM_PT_ObjectPropsPanel) + bpy.utils.register_class(ARM_PT_ModifiersPropsPanel) + bpy.utils.register_class(ARM_PT_ParticlesPropsPanel) + bpy.utils.register_class(ARM_PT_PhysicsPropsPanel) + bpy.utils.register_class(ARM_PT_DataPropsPanel) + bpy.utils.register_class(ARM_PT_ScenePropsPanel) + bpy.utils.register_class(ARM_PT_WorldPropsPanel) + bpy.utils.register_class(InvalidateCacheButton) + bpy.utils.register_class(InvalidateMaterialCacheButton) + bpy.utils.register_class(ARM_OT_NewCustomMaterial) + bpy.utils.register_class(ARM_PG_BindTexturesListItem) + bpy.utils.register_class(ARM_UL_BindTexturesList) + bpy.utils.register_class(ARM_OT_BindTexturesListNewItem) + bpy.utils.register_class(ARM_OT_BindTexturesListDeleteItem) + bpy.utils.register_class(ARM_PT_MaterialPropsPanel) + bpy.utils.register_class(ARM_PT_BindTexturesPropsPanel) + bpy.utils.register_class(ARM_PT_MaterialBlendingPropsPanel) + bpy.utils.register_class(ARM_PT_MaterialDriverPropsPanel) + bpy.utils.register_class(ARM_PT_ArmoryPlayerPanel) + bpy.utils.register_class(ARM_PT_TopbarPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidAbiPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) + bpy.utils.register_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) + bpy.utils.register_class(ARM_PT_ArmoryProjectPanel) + bpy.utils.register_class(ARM_PT_ProjectFlagsPanel) + bpy.utils.register_class(ARM_PT_ProjectFlagsDebugConsolePanel) + bpy.utils.register_class(ARM_PT_ProjectWindowPanel) + bpy.utils.register_class(ARM_PT_ProjectModulesPanel) + bpy.utils.register_class(ARM_PT_RenderPathPanel) + bpy.utils.register_class(ARM_PT_RenderPathRendererPanel) + bpy.utils.register_class(ARM_PT_RenderPathShadowsPanel) + bpy.utils.register_class(ARM_PT_RenderPathVoxelsPanel) + bpy.utils.register_class(ARM_PT_RenderPathWorldPanel) + bpy.utils.register_class(ARM_PT_RenderPathPostProcessPanel) + bpy.utils.register_class(ARM_PT_RenderPathCompositorPanel) + bpy.utils.register_class(ARM_PT_BakePanel) + # bpy.utils.register_class(ArmVirtualInputPanel) + bpy.utils.register_class(ArmoryPlayButton) + bpy.utils.register_class(ArmoryStopButton) + bpy.utils.register_class(ArmoryBuildProjectButton) + bpy.utils.register_class(ArmoryOpenProjectFolderButton) + bpy.utils.register_class(ArmoryOpenEditorButton) + bpy.utils.register_class(CleanMenu) + bpy.utils.register_class(CleanButtonMenu) + bpy.utils.register_class(ArmoryCleanProjectButton) + bpy.utils.register_class(ArmoryPublishProjectButton) + bpy.utils.register_class(ArmGenLodButton) + bpy.utils.register_class(ARM_PT_LodPanel) + bpy.utils.register_class(ArmGenTerrainButton) + bpy.utils.register_class(ARM_PT_TerrainPanel) + bpy.utils.register_class(ARM_PT_TilesheetPanel) + bpy.utils.register_class(ArmPrintTraitsButton) + bpy.utils.register_class(ARM_PT_MaterialNodePanel) + bpy.utils.register_class(ARM_OT_UpdateFileSDK) + bpy.utils.register_class(ARM_OT_CopyToBundled) + bpy.utils.register_class(ARM_OT_ShowFileVersionInfo) + bpy.utils.register_class(ARM_OT_ShowNodeUpdateErrors) + bpy.utils.register_class(ARM_OT_DiscardPopup) + bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorButton) + bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) + bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) + + bpy.utils.register_class(scene.TLM_PT_Settings) + bpy.utils.register_class(scene.TLM_PT_Denoise) + bpy.utils.register_class(scene.TLM_PT_Filtering) + bpy.utils.register_class(scene.TLM_PT_Encoding) + bpy.utils.register_class(scene.TLM_PT_Utility) + bpy.utils.register_class(scene.TLM_PT_Additional) + + bpy.types.VIEW3D_HT_header.append(draw_view3d_header) + bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) + bpy.types.NODE_MT_context_menu.append(draw_custom_node_menu) + bpy.types.TOPBAR_HT_upper_bar.prepend(draw_space_topbar) + + bpy.types.Material.arm_bind_textures_list = CollectionProperty(type=ARM_PG_BindTexturesListItem) + bpy.types.Material.arm_bind_textures_list_index = IntProperty(name='Index for arm_bind_textures_list', default=0) + +def unregister(): + bpy.types.NODE_MT_context_menu.remove(draw_custom_node_menu) + bpy.types.VIEW3D_MT_object.remove(draw_view3d_object_menu) + bpy.types.VIEW3D_HT_header.remove(draw_view3d_header) + bpy.types.TOPBAR_HT_upper_bar.remove(draw_space_topbar) + + bpy.utils.unregister_class(ArmoryUpdateListInstalledVSButton) + bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorRunButton) + bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorButton) + bpy.utils.unregister_class(ARM_OT_DiscardPopup) + bpy.utils.unregister_class(ARM_OT_ShowNodeUpdateErrors) + bpy.utils.unregister_class(ARM_OT_CopyToBundled) + bpy.utils.unregister_class(ARM_OT_ShowFileVersionInfo) + bpy.utils.unregister_class(ARM_OT_UpdateFileSDK) + bpy.utils.unregister_class(ARM_PT_ObjectPropsPanel) + bpy.utils.unregister_class(ARM_PT_ModifiersPropsPanel) + bpy.utils.unregister_class(ARM_PT_ParticlesPropsPanel) + bpy.utils.unregister_class(ARM_PT_PhysicsPropsPanel) + bpy.utils.unregister_class(ARM_PT_DataPropsPanel) + bpy.utils.unregister_class(ARM_PT_WorldPropsPanel) + bpy.utils.unregister_class(ARM_PT_ScenePropsPanel) + bpy.utils.unregister_class(InvalidateCacheButton) + bpy.utils.unregister_class(InvalidateMaterialCacheButton) + bpy.utils.unregister_class(ARM_OT_NewCustomMaterial) + bpy.utils.unregister_class(ARM_PT_MaterialDriverPropsPanel) + bpy.utils.unregister_class(ARM_PT_MaterialBlendingPropsPanel) + bpy.utils.unregister_class(ARM_PT_BindTexturesPropsPanel) + bpy.utils.unregister_class(ARM_PT_MaterialPropsPanel) + bpy.utils.unregister_class(ARM_OT_BindTexturesListDeleteItem) + bpy.utils.unregister_class(ARM_OT_BindTexturesListNewItem) + bpy.utils.unregister_class(ARM_UL_BindTexturesList) + bpy.utils.unregister_class(ARM_PG_BindTexturesListItem) + bpy.utils.unregister_class(ARM_PT_ArmoryPlayerPanel) + bpy.utils.unregister_class(ARM_PT_TopbarPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidAbiPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryExporterPanel) + bpy.utils.unregister_class(ARM_PT_ArmoryProjectPanel) + bpy.utils.unregister_class(ARM_PT_ProjectFlagsDebugConsolePanel) + bpy.utils.unregister_class(ARM_PT_ProjectFlagsPanel) + bpy.utils.unregister_class(ARM_PT_ProjectWindowPanel) + bpy.utils.unregister_class(ARM_PT_ProjectModulesPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathRendererPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathShadowsPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathVoxelsPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathWorldPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathPostProcessPanel) + bpy.utils.unregister_class(ARM_PT_RenderPathCompositorPanel) + bpy.utils.unregister_class(ARM_PT_BakePanel) + # bpy.utils.unregister_class(ArmVirtualInputPanel) + bpy.utils.unregister_class(ArmoryPlayButton) + bpy.utils.unregister_class(ArmoryStopButton) + bpy.utils.unregister_class(ArmoryBuildProjectButton) + bpy.utils.unregister_class(ArmoryOpenProjectFolderButton) + bpy.utils.unregister_class(ArmoryOpenEditorButton) + bpy.utils.unregister_class(CleanMenu) + bpy.utils.unregister_class(CleanButtonMenu) + bpy.utils.unregister_class(ArmoryCleanProjectButton) + bpy.utils.unregister_class(ArmoryPublishProjectButton) + bpy.utils.unregister_class(ArmGenLodButton) + bpy.utils.unregister_class(ARM_PT_LodPanel) + bpy.utils.unregister_class(ArmGenTerrainButton) + bpy.utils.unregister_class(ARM_PT_TerrainPanel) + bpy.utils.unregister_class(ARM_PT_TilesheetPanel) + bpy.utils.unregister_class(ArmPrintTraitsButton) + bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) + + bpy.utils.unregister_class(scene.TLM_PT_Settings) + bpy.utils.unregister_class(scene.TLM_PT_Denoise) + bpy.utils.unregister_class(scene.TLM_PT_Filtering) + bpy.utils.unregister_class(scene.TLM_PT_Encoding) + bpy.utils.unregister_class(scene.TLM_PT_Utility) + bpy.utils.unregister_class(scene.TLM_PT_Additional) diff --git a/blender/arm/ui_icons.py b/blender/arm/ui_icons.py new file mode 100644 index 0000000000..74cc4da325 --- /dev/null +++ b/blender/arm/ui_icons.py @@ -0,0 +1,55 @@ +""" +Blender user interface icon handling. +""" +import os.path +from typing import Optional + +import bpy.utils.previews + +import arm + +if arm.is_reload(__name__): + # _unload_icons is not available in the module scope yet + def __unload(): + _unload_icons() + + # Refresh icons after reload + __unload() +else: + arm.enable_reload(__name__) + + +__all__ = ["get_id"] + +_ICONS_DICT: Optional[bpy.utils.previews.ImagePreviewCollection] = None +"""Dictionary of all loaded icons, or `None` if not loaded""" + +_ICONS_DIR = os.path.join(os.path.dirname(__file__), "custom_icons") +"""Directory of the icon files""" + + +def _load_icons(): + """(Re)loads all icons.""" + global _ICONS_DICT + + _unload_icons() + + _ICONS_DICT = bpy.utils.previews.new() + _ICONS_DICT.load("bundle", os.path.join(_ICONS_DIR, "bundle.png"), 'IMAGE', force_reload=True) + _ICONS_DICT.load("haxe", os.path.join(_ICONS_DIR, "haxe.png"), 'IMAGE', force_reload=True) + _ICONS_DICT.load("wasm", os.path.join(_ICONS_DIR, "wasm.png"), 'IMAGE', force_reload=True) + + +def _unload_icons(): + """Unloads all icons.""" + global _ICONS_DICT + if _ICONS_DICT is not None: + bpy.utils.previews.remove(_ICONS_DICT) + _ICONS_DICT = None + + +def get_id(identifier: str) -> int: + """Returns the icon ID from the given identifier.""" + if _ICONS_DICT is None: + _load_icons() + return _ICONS_DICT[identifier].icon_id diff --git a/blender/arm/utils.py b/blender/arm/utils.py new file mode 100644 index 0000000000..521718c18d --- /dev/null +++ b/blender/arm/utils.py @@ -0,0 +1,1212 @@ +from enum import Enum, unique +import glob +import itertools +import json +import locale +import os +import platform +import random +import re +import shlex +import shutil +import subprocess +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union +import webbrowser + +import numpy as np + +import bpy + +import arm.lib.armpack +from arm.lib.lz4 import LZ4 +import arm.log as log +import arm.make_state as state +import arm.props_renderpath + +if arm.is_reload(__name__): + arm.lib.armpack = arm.reload_module(arm.lib.armpack) + arm.lib.lz4 = arm.reload_module(arm.lib.lz4) + from arm.lib.lz4 import LZ4 + log = arm.reload_module(log) + state = arm.reload_module(state) + arm.props_renderpath = arm.reload_module(arm.props_renderpath) +else: + arm.enable_reload(__name__) + + +class NumpyEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + return json.JSONEncoder.default(self, obj) + +class WorkingDir: + """Context manager for safely changing the current working directory.""" + def __init__(self, cwd: str): + self.cwd = cwd + self.prev_cwd = os.getcwd() + + def __enter__(self): + os.chdir(self.cwd) + + def __exit__(self, exc_type, exc_val, exc_tb): + os.chdir(self.prev_cwd) + +def write_arm(filepath, output): + if filepath.endswith('.lz4'): + with open(filepath, 'wb') as f: + packed = arm.lib.armpack.packb(output) + # Prepend packed data size for decoding. Haxe can't unpack + # an unsigned int64 so we use a signed int64 here + f.write(np.int64(LZ4.encode_bound(len(packed))).tobytes()) + + f.write(LZ4.encode(packed)) + else: + if bpy.data.worlds['Arm'].arm_minimize: + with open(filepath, 'wb') as f: + f.write(arm.lib.armpack.packb(output)) + else: + filepath_json = filepath.split('.arm')[0] + '.json' + with open(filepath_json, 'w') as f: + f.write(json.dumps(output, sort_keys=True, indent=4, cls=NumpyEncoder)) + +def unpack_image(image, path, file_format='JPEG'): + print('Armory Info: Unpacking to ' + path) + image.filepath_raw = path + image.file_format = file_format + image.save() + +def convert_image(image, path, file_format='JPEG'): + # Convert image to compatible format + print('Armory Info: Converting to ' + path) + ren = bpy.context.scene.render + orig_quality = ren.image_settings.quality + orig_file_format = ren.image_settings.file_format + orig_color_mode = ren.image_settings.color_mode + ren.image_settings.quality = get_texture_quality_percentage() + ren.image_settings.file_format = file_format + if file_format == 'PNG': + ren.image_settings.color_mode = 'RGBA' + orig_image_colorspace = image.colorspace_settings.name + image.colorspace_settings.name = 'Non-Color' + image.save_render(path, scene=bpy.context.scene) + image.colorspace_settings.name = orig_image_colorspace + ren.image_settings.quality = orig_quality + ren.image_settings.file_format = orig_file_format + ren.image_settings.color_mode = orig_color_mode + + +def get_random_color_rgb() -> list[float]: + """Return a random RGB color with values in range [0, 1].""" + return [random.random(), random.random(), random.random()] + + +def is_livepatch_enabled(): + """Returns whether live patch is enabled and can be used.""" + wrd = bpy.data.worlds['Arm'] + # If the game is published, the target is krom-[OS] and not krom, + # so there is no live patch when publishing + return wrd.arm_live_patch and state.target == 'krom' + + +def blend_name(): + return bpy.path.basename(bpy.context.blend_data.filepath).rsplit('.', 1)[0] + +def build_dir(): + return 'build_' + safestr(blend_name()) + + +def get_fp() -> str: + wrd = bpy.data.worlds['Arm'] + if wrd.arm_project_root != '': + return bpy.path.abspath(wrd.arm_project_root) + else: + s = None + if use_local_sdk and bpy.data.filepath == '': + s = os.getcwd() + else: + s = bpy.data.filepath.split(os.path.sep) + s.pop() + s = os.path.sep.join(s) + if get_os_is_windows() and len(s) == 2 and s[1] == ':': + # If the project is located at a drive root (C:/ for example), + # then s = "C:". If joined later with another path, no path + # separator is added by default because C:some_path is valid + # Windows path syntax (some_path is then relative to the CWD on the + # C drive). We prevent this by manually adding the path separator + # in these cases. Please refer to the Python doc of os.path.join() + # for more details. + s += os.path.sep + return s + + +def get_fp_build(): + return os.path.join(get_fp(), build_dir()) + + +def to_absolute_path(path: str, from_library: Optional[bpy.types.Library] = None) -> str: + """Convert the given absolute or relative path into an absolute path. + + - If `from_library` is not set (default), a given relative path will be + interpreted as relative to the project directory. + - If `from_library` is set, a given relative path will be interpreted as + relative to the filepath of the specified library. + """ + return os.path.normpath(bpy.path.abspath(path, start=get_fp(), library=from_library)) + + +def get_os() -> str: + s = platform.system() + if s == 'Windows': + return 'win' + elif s == 'Darwin': + return 'mac' + else: + return 'linux' + + +def get_os_is_windows() -> bool: + return get_os() == 'win' + + +def get_os_is_windows_64() -> bool: + if platform.machine().endswith('64'): + return True + # Checks if Python (32 bit) is running on Windows (64 bit) + if 'PROCESSOR_ARCHITEW6432' in os.environ: + return True + if os.environ['PROCESSOR_ARCHITECTURE'].endswith('64'): + return True + if 'PROGRAMFILES(X86)' in os.environ: + if os.environ['PROGRAMW6432'] is not None: + return True + else: + return False + + +def get_gapi(): + wrd = bpy.data.worlds['Arm'] + if state.is_export: + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + return getattr(item, target_to_gapi(item.arm_project_target)) + if wrd.arm_runtime == 'Browser': + return 'webgl' + return 'direct3d11' if get_os() == 'win' else 'opengl' + + +def is_gapi_gl_es() -> bool: + """Return whether the currently targeted graphics API is using OpenGL ES.""" + wrd = bpy.data.worlds['Arm'] + + if state.is_export: + item_exporter = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + + # See Khamake's ShaderCompiler.findType() and krafix::Target.es in krafix.cpp ("target.es") + if state.target == 'android-hl': + return item_exporter.arm_gapi_android == 'opengl' + if state.target == 'ios-hl': + return item_exporter.arm_gapi_ios == 'opengl' + elif state.target == 'html5': + return True + return False + + else: + return wrd.arm_runtime == 'Browser' + + +def get_rp() -> arm.props_renderpath.ArmRPListItem: + wrd = bpy.data.worlds['Arm'] + if not state.is_export and wrd.arm_play_renderpath != '': + return arm.props_renderpath.ArmRPListItem.get_by_name(wrd.arm_play_renderpath) + else: + return wrd.arm_rplist[wrd.arm_rplist_index] + + +# Passed by load_post handler when armsdk is found in project folder +use_local_sdk = False +def get_sdk_path(): + addon_prefs = get_arm_preferences() + if use_local_sdk: + return os.path.normpath(get_fp() + '/armsdk/') + else: + return os.path.normpath(addon_prefs.sdk_path) + +def get_last_commit(): + p = get_sdk_path() + 'armory/.git/refs/heads/main' + + try: + file = open(p, 'r') + commit = file.readline() + except: + commit = '' + return commit + + +def get_arm_preferences() -> bpy.types.AddonPreferences: + preferences = bpy.context.preferences + return preferences.addons["armory"].preferences + + +def get_ide_bin(): + addon_prefs = get_arm_preferences() + return '' if not hasattr(addon_prefs, 'ide_bin') else addon_prefs.ide_bin + +def get_ffmpeg_path(): + path = get_arm_preferences().ffmpeg_path + if path == "": path = shutil.which("ffmpeg") + return path + +def get_renderdoc_path(): + p = get_arm_preferences().renderdoc_path + if p == '' and get_os() == 'win': + pdefault = 'C:\\Program Files\\RenderDoc\\qrenderdoc.exe' + if os.path.exists(pdefault): + p = pdefault + return p + +def get_code_editor(): + addon_prefs = get_arm_preferences() + return 'kodestudio' if not hasattr(addon_prefs, 'code_editor') else addon_prefs.code_editor + +def get_ui_scale(): + addon_prefs = get_arm_preferences() + return 1.0 if not hasattr(addon_prefs, 'ui_scale') else addon_prefs.ui_scale + +def get_khamake_threads() -> int: + addon_prefs = get_arm_preferences() + if hasattr(addon_prefs, 'khamake_threads_use_auto') and addon_prefs.khamake_threads_use_auto: + return -1 + return 1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads + +def get_compilation_server(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'compilation_server') else addon_prefs.compilation_server + +def get_save_on_build(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'save_on_build') else addon_prefs.save_on_build + +def get_debug_console_auto(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'debug_console_auto') else addon_prefs.debug_console_auto + +def get_debug_console_visible_sc(): + addon_prefs = get_arm_preferences() + return 192 if not hasattr(addon_prefs, 'debug_console_visible_sc') else addon_prefs.debug_console_visible_sc + +def get_debug_console_scale_in_sc(): + addon_prefs = get_arm_preferences() + return 219 if not hasattr(addon_prefs, 'debug_console_scale_in_sc') else addon_prefs.debug_console_scale_in_sc + +def get_debug_console_scale_out_sc(): + addon_prefs = get_arm_preferences() + return 221 if not hasattr(addon_prefs, 'debug_console_scale_out_sc') else addon_prefs.debug_console_scale_out_sc + +def get_viewport_controls(): + addon_prefs = get_arm_preferences() + return 'qwerty' if not hasattr(addon_prefs, 'viewport_controls') else addon_prefs.viewport_controls + +def get_legacy_shaders(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'legacy_shaders') else addon_prefs.legacy_shaders + +def get_relative_paths(): + """Whether to convert absolute paths to relative""" + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'relative_paths') else addon_prefs.relative_paths + +def get_pref_or_default(prop_name: str, default: Any) -> Any: + """Return the preference setting for prop_name, or the value given as default if the property does not exist.""" + addon_prefs = get_arm_preferences() + return getattr(addon_prefs, prop_name, default) + +def get_node_path(): + if get_os() == 'win': + return get_sdk_path() + '/nodejs/node.exe' + elif get_os() == 'mac': + return get_sdk_path() + '/nodejs/node-osx' + else: + return get_sdk_path() + '/nodejs/node-linux64' + +def get_kha_path(): + if os.path.exists('Kha'): + return 'Kha' + return get_sdk_path() + '/Kha' + +def get_haxe_path(): + if get_os() == 'win': + return get_kha_path() + '/Tools/windows_x64/haxe.exe' + elif get_os() == 'mac': + return get_kha_path() + '/Tools/macos/haxe' + else: + return get_kha_path() + '/Tools/linux_x64/haxe' + +def get_khamake_path(): + return get_kha_path() + '/make' + +def krom_paths(): + sdk_path = get_sdk_path() + if arm.utils.get_os() == 'win': + krom_location = sdk_path + '/Krom' + krom_path = krom_location + '/Krom.exe' + elif arm.utils.get_os() == 'mac': + krom_location = sdk_path + '/Krom/Krom.app/Contents/MacOS' + krom_path = krom_location + '/Krom' + else: + krom_location = sdk_path + '/Krom' + krom_path = krom_location + '/Krom' + return krom_location, krom_path + +def fetch_bundled_script_names(): + wrd = bpy.data.worlds['Arm'] + wrd.arm_bundled_scripts_list.clear() + + with WorkingDir(get_sdk_path() + '/armory/Sources/armory/trait'): + for file in glob.glob('*.hx'): + wrd.arm_bundled_scripts_list.add().name = file.rsplit('.', 1)[0] + + +script_props = {} +script_props_defaults = {} +script_warnings: Dict[str, List[Tuple[str, str]]] = {} # Script name -> List of (identifier, warning message) + +# 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*:\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};' +PROP_REGEX = re.compile(PROP_REGEX_RAW, re.IGNORECASE) +def fetch_script_props(filename: str): + """Parses @prop declarations from the given Haxe script.""" + with open(filename, 'r', encoding='utf-8') as sourcefile: + source = sourcefile.read() + + if source == '': + return + + name = filename.rsplit('.', 1)[0] + + # Convert the name into a package path relative to the "Sources" dir + if 'Sources' in name: + name = name[name.index('Sources') + 8:] + if '/' in name: + name = name.replace('/', '.') + if '\\' in filename: + name = name.replace('\\', '.') + + script_props[name] = [] + script_props_defaults[name] = [] + script_warnings[name] = [] + + for match in re.finditer(PROP_REGEX, source): + + p_modifiers: Optional[str] = match.group('modifiers') + p_identifier: str = match.group('identifier') + p_type: Optional[str] = match.group('type') + p_default_val: Optional[str] = match.group('value') + + if p_modifiers is not None: + if 'static' in p_modifiers: + script_warnings[name].append((p_identifier, '`static` modifier might cause unwanted behaviour!')) + if 'inline' in p_modifiers: + script_warnings[name].append((p_identifier, '`inline` modifier is not supported!')) + continue + if 'final' in p_modifiers or match.group('attr_type') == 'final': + script_warnings[name].append((p_identifier, '`final` properties are not supported!')) + continue + + # Property type is annotated + if p_type is not None: + if p_type.startswith("iron.object."): + p_type = p_type[12:] + elif p_type.startswith("iron.math."): + p_type = p_type[10:] + + type_default_val = get_type_default_value(p_type) + if type_default_val is None: + script_warnings[name].append((p_identifier, f'unsupported type `{p_type}`!')) + continue + + # Default value exists + if p_default_val is not None: + # Remove string quotes + p_default_val = p_default_val.replace('\'', '').replace('"', '') + else: + p_default_val = type_default_val + + # Default value is given instead, try to infer the properties type from it + elif p_default_val is not None: + p_type = get_prop_type_from_value(p_default_val) + + # Type is not recognized + if p_type is None: + script_warnings[name].append((p_identifier, 'could not infer property type from given value!')) + continue + if p_type == "String": + p_default_val = p_default_val.replace('\'', '').replace('"', '') + + else: + script_warnings[name].append((p_identifier, 'missing type or default value!')) + continue + + # Register prop + prop = (p_identifier, p_type) + script_props[name].append(prop) + script_props_defaults[name].append(p_default_val) + + +def get_prop_type_from_value(value: str): + """ + Returns the property type based on its representation in the code. + + If the type is not supported, `None` is returned. + """ + # Maybe ast.literal_eval() is better here? + try: + int(value) + return "Int" + except ValueError: + try: + float(value) + return "Float" + except ValueError: + # "" is required, " alone will not work + if len(value) > 1 and value.startswith(("\"", "'")) and value.endswith(("\"", "'")): + return "String" + if value in ("true", "false"): + return "Bool" + if value.startswith("new "): + value = value.split()[1].split("(")[0] + if value.startswith("Vec"): + return value + if value.startswith("iron.math.Vec"): + return value[10:] + + return None + +def get_type_default_value(prop_type: str): + """ + Returns the default value of the given Haxe type. + + If the type is not supported, `None` is returned: + """ + if prop_type == "Int": + return 0 + if prop_type == "Float": + return 0.0 + if prop_type == "String" or prop_type in ( + "Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"): + return "" + if prop_type == "Bool": + return False + if prop_type == "Vec2": + return [0.0, 0.0] + if prop_type == "Vec3": + return [0.0, 0.0, 0.0] + if prop_type == "Vec4": + return [0.0, 0.0, 0.0, 0.0] + + return None + +def fetch_script_names(): + if bpy.data.filepath == "": + return + wrd = bpy.data.worlds['Arm'] + # Sources + wrd.arm_scripts_list.clear() + sources_path = os.path.join(get_fp(), 'Sources', safestr(wrd.arm_project_package)) + if os.path.isdir(sources_path): + with WorkingDir(sources_path): + # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python + for file in glob.glob('**/*.hx', recursive=True): + mod = file.rsplit('.', 1)[0] + mod = mod.replace('\\', '/') + mod_parts = mod.rsplit('/') + if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]): + wrd.arm_scripts_list.add().name = mod.replace('/', '.') + fetch_script_props(file) + + # Canvas + wrd.arm_canvas_list.clear() + canvas_path = get_fp() + '/Bundled/canvas' + if os.path.isdir(canvas_path): + with WorkingDir(canvas_path): + for file in glob.glob('*.json'): + if file == "_themes.json": + continue + wrd.arm_canvas_list.add().name = file.rsplit('.', 1)[0] + +def fetch_wasm_names(): + if bpy.data.filepath == "": + return + wrd = bpy.data.worlds['Arm'] + # WASM modules + wrd.arm_wasm_list.clear() + sources_path = get_fp() + '/Bundled' + if os.path.isdir(sources_path): + with WorkingDir(sources_path): + for file in glob.glob('*.wasm'): + name = file.rsplit('.', 1)[0] + wrd.arm_wasm_list.add().name = name + + +def fetch_trait_props(): + for o in bpy.data.objects: + if o.override_library is None: + # We can't update the list of trait properties for linked + # objects because Blender doesn't allow to remove items from + # overridden lists + fetch_prop(o) + + for s in bpy.data.scenes: + fetch_prop(s) + + +def fetch_prop(o: Union[bpy.types.Object, bpy.types.Scene]): + for item in o.arm_traitlist: + if item.type_prop == 'Bundled Script': + name = 'armory.trait.' + item.name + else: + name = item.name + if name not in script_props: + continue + props = script_props[name] + defaults = script_props_defaults[name] + warnings = script_warnings[name] + + # Remove old props + for i in range(len(item.arm_traitpropslist) - 1, -1, -1): + ip = item.arm_traitpropslist[i] + if ip.name not in [p[0] for p in props]: + item.arm_traitpropslist.remove(i) + + # Add new props + for index, p in enumerate(props): + found_prop = False + for i_prop in item.arm_traitpropslist: + if i_prop.name == p[0]: + if i_prop.type == p[1]: + found_prop = i_prop + else: + item.arm_traitpropslist.remove(item.arm_traitpropslist.find(i_prop.name)) + break + + # Not in list + if not found_prop: + prop = item.arm_traitpropslist.add() + prop.name = p[0] + prop.type = p[1] + prop.set_value(defaults[index]) + + if found_prop: + prop = item.arm_traitpropslist[found_prop.name] + + # Default value added and current value is blank (no override) + if (found_prop.get_value() is None + or found_prop.get_value() == "") and defaults[index]: + prop.set_value(defaults[index]) + # Type has changed, update displayed name + if len(found_prop.name) == 1 or (len(found_prop.name) > 1 and found_prop.name[1] != p[1]): + prop.name = p[0] + prop.type = p[1] + + item.arm_traitpropswarnings.clear() + for warning in warnings: + entry = item.arm_traitpropswarnings.add() + entry.propName = warning[0] + entry.warning = warning[1] + + +def fetch_bundled_trait_props(): + # Bundled script props + for o in bpy.data.objects: + for t in o.arm_traitlist: + if t.type_prop == 'Bundled Script': + file_path = get_sdk_path() + '/armory/Sources/armory/trait/' + t.name + '.hx' + if os.path.exists(file_path): + fetch_script_props(file_path) + fetch_prop(o) + +def update_trait_collections(): + for col in bpy.data.collections: + if col.name.startswith('Trait|'): + bpy.data.collections.remove(col) + for o in bpy.data.objects: + for t in o.arm_traitlist: + if 'Trait|' + t.name not in bpy.data.collections: + col = bpy.data.collections.new('Trait|' + t.name) + else: + col = bpy.data.collections['Trait|' + t.name] + col.objects.link(o) + + +def to_hex(val): + return '#%02x%02x%02x%02x' % (int(val[3] * 255), int(val[0] * 255), int(val[1] * 255), int(val[2] * 255)) + + +def color_to_int(val) -> int: + # Clamp values, otherwise the return value might not fit in 32 bit + # (and later cause problems, e.g. in the .arm file reader) + val = [max(0.0, min(v, 1.0)) for v in val] + return (int(val[3] * 255) << 24) + (int(val[0] * 255) << 16) + (int(val[1] * 255) << 8) + int(val[2] * 255) + + +def unique_name_in_lists(item_lists: Iterable[list], name_attr: str, wanted_name: str, ignore_item: Optional[Any] = None) -> str: + """Creates a unique name that no item in the given lists already has. + The format follows Blender's behaviour when handling duplicate + object names. + + @param item_lists An iterable of item lists (any type). + @param name_attr The attribute of the items that holds the name. + @param wanted_name The name that should be preferably returned, if + no name collision occurs. + @param ignore_item (Optional) Ignore this item in the list when + comparing names. + """ + def _has_collision(name: str) -> bool: + for item in itertools.chain(*item_lists): + if item == ignore_item: + continue + if getattr(item, name_attr) == name: + return True + return False + + # Check this once at the beginning to make sure the user can use + # a wanted name like "XY.001" if they want, even if "XY" alone does + # not collide + if not _has_collision(wanted_name): + return wanted_name + + # Get base name without numeric suffix + base_name = wanted_name + dot_pos = base_name.rfind('.') + if dot_pos != -1: + if base_name[dot_pos + 1:].isdecimal(): + base_name = base_name[:dot_pos] + + num_collisions = 0 + out_name = base_name + while _has_collision(out_name): + num_collisions += 1 + out_name = f'{base_name}.{num_collisions:03d}' + + return out_name + + +def merge_into_collection(col_src, col_dst, clear_dst=True): + """Merges the items of the `col_src` collection property into the + `col_dst` collection property. + + If `clear_dst` is true, the destination collection is cleared before + merging. Otherwise, new items are added on top of the existing items + in `col_dst`. There is no check for duplicates. + """ + if clear_dst: + col_dst.clear() + + for item_src in col_src: + item_dst = col_dst.add() + + # collect names of writable properties + prop_names = [p.identifier for p in item_src.bl_rna.properties + if not p.is_readonly] + + # copy those properties + for prop_name in prop_names: + setattr(item_dst, prop_name, getattr(item_src, prop_name)) + + +def safesrc(s): + s = safestr(s).replace('.', '_').replace('-', '_').replace(' ', '') + if s[0].isdigit(): + s = '_' + s + return s + +def safestr(s: str) -> str: + """Outputs a string where special characters have been replaced with + '_', which can be safely used in file and path names.""" + for c in r'''[]/\;,><&*:%=+@!#^()|?^'"''': + s = s.replace(c, '_') + return ''.join([i if ord(i) < 128 else '_' for i in s]) + +def get_haxe_json_string(d: dict) -> str: + s = str(d) + s = s.replace('True', 'true') + s = s.replace('False', 'false') + s = s.replace("'", '"') + return s + +def asset_name(bdata): + if bdata == None: + return None + s = bdata.name + # Append library name if linked + if bdata.library is not None: + s += '_' + bdata.library.name + return s + +def asset_path(s): + """Remove leading '//'""" + return s[2:] if s[:2] == '//' else s + +def extract_filename(s): + return os.path.basename(asset_path(s)) + +def get_render_resolution(scene): + render = scene.render + scale = render.resolution_percentage / 100 + return int(render.resolution_x * scale), int(render.resolution_y * scale) + +def get_texture_quality_percentage() -> int: + return int(bpy.data.worlds["Arm"].arm_texture_quality * 100) + +def get_project_scene_name(): + return get_active_scene().name + +def get_active_scene() -> bpy.types.Scene: + wrd = bpy.data.worlds['Arm'] + if not state.is_export: + if wrd.arm_play_scene is None: + return bpy.context.scene + return wrd.arm_play_scene + else: + item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] + return item.arm_project_scene + +def logic_editor_space(context_screen=None): + if context_screen == None: + context_screen = bpy.context.screen + if context_screen != None: + areas = context_screen.areas + for area in areas: + for space in area.spaces: + if space.type == 'NODE_EDITOR': + if space.node_tree != None and space.node_tree.bl_idname == 'ArmLogicTreeType': + return space + return None + +def voxel_support(): + # macos does not support opengl 4.5, needs metal + return state.target != 'html5' and get_os() != 'mac' + +def get_cascade_size(rpdat): + cascade_size = int(rpdat.rp_shadowmap_cascade) + # Clamp to 4096 per cascade + if int(rpdat.rp_shadowmap_cascades) > 1 and cascade_size > 4096: + cascade_size = 4096 + return cascade_size + + +def check_blender_version(op: bpy.types.Operator): + """Check whether the user uses the correct Blender version, if not + report in UI. + """ + if not compare_version_blender_arm(): + op.report({'INFO'}, 'For Armory to work correctly, you need Blender 3.3 LTS.') + + +def check_saved(self): + if bpy.data.filepath == "": + msg = "Save blend file first" + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) + return False + return True + +def check_path(s): + for c in r'[];><&*%=+@!#^()|?^': + if c in s: + return False + for c in s: + if ord(c) > 127: + return False + return True + +def check_sdkpath(self): + s = get_sdk_path() + if not check_path(s): + msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now." + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) + return False + else: + return True + +def check_projectpath(self): + s = get_fp() + if not check_path(s): + msg = f"Project path '{s}' contains special characters, build process may fail." + self.report({"ERROR"}, msg) if self is not None else log.warn(msg) + return False + else: + return True + +def disp_enabled(target): + rpdat = get_rp() + if rpdat.arm_rp_displacement == 'Tessellation': + return target == 'krom' or target == 'native' + return rpdat.arm_rp_displacement != 'Off' + +def is_object_animation_enabled(bobject): + # Checks if animation is present and enabled + if bobject.arm_animation_enabled == False or bobject.type == 'BONE' or bobject.type == 'ARMATURE': + return False + if bobject.animation_data and bobject.animation_data.action: + return True + return False + +def is_bone_animation_enabled(bobject): + # Checks if animation is present and enabled for parented armature + if bobject.parent and bobject.parent.type == 'ARMATURE': + if bobject.parent.arm_animation_enabled == False: + return False + # Check for present actions + adata = bobject.parent.animation_data + has_actions = adata != None and adata.action != None + if not has_actions and adata != None: + if hasattr(adata, 'nla_tracks') and adata.nla_tracks != None: + for track in adata.nla_tracks: + if track.strips == None: + continue + for strip in track.strips: + if strip.action == None: + continue + has_actions = True + break + if has_actions: + break + if adata != None and has_actions: + return True + return False + + +def export_bone_data(bobject: bpy.types.Object) -> bool: + """Returns whether the bone data of the given object should be exported.""" + return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().arm_skin == 'On' + +def export_morph_targets(bobject: bpy.types.Object) -> bool: + if get_rp().arm_morph_target != 'On': + return False + + if not hasattr(bobject.data, 'shape_keys'): + return False + + shape_keys = bobject.data.shape_keys + if not shape_keys: + return False + if len(shape_keys.key_blocks) < 2: + return False + for shape_key in shape_keys.key_blocks[1:]: + if(not shape_key.mute): + return True + return False + +def export_vcols(bobject: bpy.types.Object) -> bool: + for material in bobject.data.materials: + if material is not None and material.export_vcols: + return True + return False + +def open_editor(hx_path=None): + ide_bin = get_ide_bin() + + if hx_path is None: + hx_path = arm.utils.get_fp() + + if get_code_editor() == 'default': + # Get editor environment variables + # https://unix.stackexchange.com/q/4859 + env_v_editor = os.environ.get('VISUAL') + env_editor = os.environ.get('EDITOR') + + if env_v_editor is not None: + ide_bin = env_v_editor + elif env_editor is not None: + ide_bin = env_editor + + # No environment variables set -> Let the system decide how to + # open the file + else: + webbrowser.open('file://' + hx_path) + return + + if os.path.exists(ide_bin): + args = [ide_bin, arm.utils.get_fp()] + + # Sublime Text + if get_code_editor() == 'sublime': + project_name = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_name) + subl_project_path = arm.utils.get_fp() + f'/{project_name}.sublime-project' + + if not os.path.exists(subl_project_path): + generate_sublime_project(subl_project_path) + + args += ['--project', subl_project_path] + + args.append('--add') + + args.append(hx_path) + + if arm.utils.get_os() == 'mac': + argstr = "" + + for arg in args: + if not (arg.startswith('-') or arg.startswith('--')): + argstr += '"' + arg + '"' + argstr += ' ' + + subprocess.Popen(argstr[:-1], shell=True) + else: + subprocess.Popen(args) + + else: + raise FileNotFoundError(f'Code editor executable not found: {ide_bin}. You can change the path in the Armory preferences.') + +def open_folder(folder_path: str): + if arm.utils.get_os() == 'win': + subprocess.run(['explorer', folder_path]) + elif arm.utils.get_os() == 'mac': + subprocess.run(['open', folder_path]) + elif arm.utils.get_os() == 'linux': + subprocess.run(['xdg-open', folder_path]) + else: + webbrowser.open('file://' + folder_path) + +def generate_sublime_project(subl_project_path): + """Generates a [project_name].sublime-project file.""" + print('Generating Sublime Text project file') + + project_data = { + "folders": [ + { + "path": ".", + "file_exclude_patterns": ["*.blend*", "*.arm"] + }, + ], + } + + with open(subl_project_path, 'w', encoding='utf-8') as project_file: + json.dump(project_data, project_file, ensure_ascii=False, indent=4) + +def def_strings_to_array(strdefs): + defs = strdefs.split('_') + defs = defs[1:] + defs = ['_' + d for d in defs] # Restore _ + return defs + +def get_kha_target(target_name): # TODO: remove + if target_name == 'macos-hl': + return 'osx-hl' + elif target_name.startswith('krom'): # krom-windows + return 'krom' + elif target_name == 'custom': + return '' + return target_name + + +def target_to_gapi(arm_project_target: str) -> str: + # TODO: align target names + if arm_project_target == 'krom': + return 'arm_gapi_' + arm.utils.get_os() + elif arm_project_target == 'krom-windows': + return 'arm_gapi_win' + elif arm_project_target == 'windows-hl': + return 'arm_gapi_win' + elif arm_project_target == 'krom-linux': + return 'arm_gapi_linux' + elif arm_project_target == 'linux-hl': + return 'arm_gapi_linux' + elif arm_project_target == 'krom-macos': + return 'arm_gapi_mac' + elif arm_project_target == 'macos-hl': + return 'arm_gapi_mac' + elif arm_project_target == 'android-hl': + return 'arm_gapi_android' + elif arm_project_target == 'ios-hl': + return 'arm_gapi_ios' + elif arm_project_target == 'node': + return 'arm_gapi_html5' + else: # html5, custom + return 'arm_gapi_' + arm_project_target + + +def check_default_props(): + wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_rplist) == 0: + wrd.arm_rplist.add() + wrd.arm_rplist_index = 0 + + if wrd.arm_project_name == '': + # Take blend file name + wrd.arm_project_name = arm.utils.blend_name() + +# Enum Permissions Name +class PermissionName(Enum): + ACCESS_COARSE_LOCATION = 'ACCESS_COARSE_LOCATION' + ACCESS_NETWORK_STATE = 'ACCESS_NETWORK_STATE' + ACCESS_FINE_LOCATION = 'ACCESS_FINE_LOCATION' + ACCESS_WIFI_STATE = 'ACCESS_WIFI_STATE' + BLUETOOTH = 'BLUETOOTH' + BLUETOOTH_ADMIN = 'BLUETOOTH_ADMIN' + CAMERA = 'CAMERA' + EXPAND_STATUS_BAR = 'EXPAND_STATUS_BAR' + FOREGROUND_SERVICE = 'FOREGROUND_SERVICE' + GET_ACCOUNTS = 'GET_ACCOUNTS' + INTERNET = 'INTERNET' + READ_EXTERNAL_STORAGE = 'READ_EXTERNAL_STORAGE' + VIBRATE = 'VIBRATE' + WRITE_EXTERNAL_STORAGE = 'WRITE_EXTERNAL_STORAGE' + +# Add permission for target android +def add_permission_target_android(permission_name_enum): + wrd = bpy.data.worlds['Arm'] + check = False + for item in wrd.arm_exporter_android_permission_list: + if (item.arm_android_permissions.upper() == str(permission_name_enum.value).upper()): + check = True + break + if not check: + wrd.arm_exporter_android_permission_list.add() + wrd.arm_exporter_android_permission_list[len(wrd.arm_exporter_android_permission_list) - 1].arm_android_permissions = str(permission_name_enum.value).upper() + +def get_project_android_build_apk(): + wrd = bpy.data.worlds['Arm'] + return wrd.arm_project_android_build_apk + +def get_android_sdk_root_path(): + if os.getenv('ANDROID_SDK_ROOT') == None: + addon_prefs = get_arm_preferences() + return '' if not hasattr(addon_prefs, 'android_sdk_root_path') else addon_prefs.android_sdk_root_path + else: + return os.getenv('ANDROID_SDK_ROOT') + +def get_android_apk_copy_path(): + addon_prefs = get_arm_preferences() + return '' if not hasattr(addon_prefs, 'android_apk_copy_path') else addon_prefs.android_apk_copy_path + +def get_android_apk_copy_open_directory(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'android_apk_copy_open_directory') else addon_prefs.android_apk_copy_open_directory + +def get_android_emulators_list(): + err = '' + items = [] + path_file = get_android_emulator_file() + if len(path_file) > 0: + cmd = path_file + " -list-avds" + if get_os_is_windows(): + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + else: + process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE) + while True: + output = process.stdout.readline().decode("utf-8") + if len(output.strip()) == 0 and process.poll() is not None: + break + if output: + items.append(output.strip()) + else: + err = 'File "'+ path_file +'" not found.' + return items, err + +def get_android_emulator_path(): + return os.path.join(get_android_sdk_root_path(), "emulator") + +def get_android_emulator_file(): + path_file = '' + if get_os_is_windows(): + path_file = os.path.join(get_android_emulator_path(), "emulator.exe") + else: + path_file = os.path.join(get_android_emulator_path(), "emulator") + # File Exists + return '' if not os.path.isfile(path_file) else path_file + +def get_android_emulator_name(): + wrd = bpy.data.worlds['Arm'] + return '' if not len(wrd.arm_project_android_list_avd.strip()) > 0 else wrd.arm_project_android_list_avd.strip() + +def get_android_open_build_apk_directory(): + addon_prefs = get_arm_preferences() + return False if not hasattr(addon_prefs, 'android_open_build_apk_directory') else addon_prefs.android_open_build_apk_directory + +def get_html5_copy_path(): + addon_prefs = get_arm_preferences() + return '' if not hasattr(addon_prefs, 'html5_copy_path') else addon_prefs.html5_copy_path + +def get_link_web_server(): + addon_prefs = get_arm_preferences() + return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server + +def compare_version_blender_arm(): + return not (bpy.app.version[0] != 3 or bpy.app.version[1] != 3) + +def get_file_arm_version_tuple() -> tuple[int]: + wrd = bpy.data.worlds['Arm'] + return tuple(map(int, wrd.arm_version.split('.'))) + +def type_name_to_type(name: str) -> bpy.types.bpy_struct: + """Return the Blender type given by its name, if registered.""" + return bpy.types.bpy_struct.bl_rna_get_subclass_py(name) + +def change_version_project(version: str) -> str: + ver = version.strip().replace(' ', '').split('.') + v_i = int(ver[len(ver) - 1]) + 1 + ver[len(ver) - 1] = str(v_i) + version = '' + for i in ver: + if len(version) > 0: + version += '.' + version += i + return version + + +def cpu_count(*, physical_only=False) -> Optional[int]: + """Returns the number of logical (default) or physical CPUs. + The result can be `None` if `os.cpu_count()` was not able to get the + correct count of logical CPUs. + """ + if not physical_only: + return os.cpu_count() + + err_reason = '' + command = [] + + _os = get_os() + try: + if _os == 'win': + sysroot = os.environ.get("SYSTEMROOT", default="C:\\WINDOWS") + command = [f'{sysroot}\\System32\\wbem\\wmic.exe', 'cpu', 'get', 'NumberOfCores'] + result = subprocess.check_output(command) + result = result.decode('utf-8').splitlines() + result = int(result[2]) + if result > 0: + return result + + elif _os == 'linux': + command = ["grep -P '^core id' /proc/cpuinfo | sort -u | wc -l"] + result = subprocess.check_output(command[0], shell=True) + result = result.decode('utf-8').splitlines() + result = int(result[0]) + if result > 0: + return result + + # macOS + else: + command = ['sysctl', '-n', 'hw.physicalcpu'] + return int(subprocess.check_output(command)) + + except subprocess.CalledProcessError as e: + err_reason = f'Reason: command {command} exited with code {e.returncode}.' + except FileNotFoundError as e: + err_reason = f'Reason: couldn\'t open file from command {command} ({e.errno=}).' + + # Last resort even though it can be wrong + log.warn("Could not retrieve count of physical CPUs, using logical CPU count instead.\n\t" + err_reason) + return os.cpu_count() + + +def register(local_sdk=False): + global use_local_sdk + use_local_sdk = local_sdk + +def unregister(): + pass diff --git a/blender/arm/utils_vs.py b/blender/arm/utils_vs.py new file mode 100644 index 0000000000..39329042db --- /dev/null +++ b/blender/arm/utils_vs.py @@ -0,0 +1,327 @@ +""" +Various utilities for interacting with Visual Studio on Windows. +""" +import json +import os +import re +import subprocess +from typing import Any, Optional, Callable + +import bpy + +import arm.log as log +import arm.make +import arm.make_state as state +import arm.utils + +if arm.is_reload(__name__): + log = arm.reload_module(log) + arm.make = arm.reload_module(arm.make) + state = arm.reload_module(state) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +# VS versions supported by khamake. Keep in mind that this list is also +# used for the wrd.arm_project_win_list_vs EnumProperty! +supported_versions = [ + ('10', '2010', 'Visual Studio 2010 (version 10)'), + ('11', '2012', 'Visual Studio 2012 (version 11)'), + ('12', '2013', 'Visual Studio 2013 (version 12)'), + ('14', '2015', 'Visual Studio 2015 (version 14)'), + ('15', '2017', 'Visual Studio 2017 (version 15)'), + ('16', '2019', 'Visual Studio 2019 (version 16)'), + ('17', '2022', 'Visual Studio 2022 (version 17)') +] + +# version_major to --visualstudio parameter +version_to_khamake_id = { + '10': 'vs2010', + '11': 'vs2012', + '12': 'vs2013', + '14': 'vs2015', + '15': 'vs2017', + '16': 'vs2019', + '17': 'vs2022', +} + +# VS versions found with fetch_installed_vs() +_installed_versions = [] + +_REGEX_SLN_MIN_VERSION = re.compile(r'MinimumVisualStudioVersion\s*=\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)') + + +def is_version_installed(version_major: str) -> bool: + return any(v['version_major'] == version_major for v in _installed_versions) + + +def get_installed_version(version_major: str, re_fetch=False) -> Optional[dict[str, str]]: + for installed_version in _installed_versions: + if installed_version['version_major'] == version_major: + return installed_version + + # No installation was found. If re_fetch is True, fetch and try again + # (the user may not have fetched installations before for example) + if re_fetch: + if not fetch_installed_vs(): + return None + return get_installed_version(version_major, False) + + return None + + +def get_supported_version(version_major: str) -> Optional[dict[str, str]]: + for version in supported_versions: + if version[0] == version_major: + return { + 'version_major': version[0], + 'year': version[1], + 'name': version[2] + } + return None + + +def fetch_installed_vs(silent=False) -> bool: + global _installed_versions + + data_instances = _vswhere_get_instances(silent) + if data_instances is None: + return False + + items = [] + + for inst in data_instances: + name = _vswhere_get_display_name(inst) + versions = _vswhere_get_version(inst) + path = _vswhere_get_path(inst) + + if name is None or versions is None or path is None: + if not silent: + log.warn( + f'Found a Visual Studio installation with incomplete information, skipping\n' + f' ({name=}, {versions=}, {path=})' + ) + continue + + items.append({ + 'version_major': versions[0], + 'version_full': versions[1], + 'version_full_ints': versions[2], + 'name': name, + 'path': path + }) + + # Store in descending order + items.sort(key=lambda x: x['version_major'], reverse=True) + + _installed_versions = items + return True + + +def open_project_in_vs(version_major: str, version_min_full: Optional[str] = None) -> bool: + installation = get_installed_version(version_major, re_fetch=True) + if installation is None: + if version_min_full is not None: + # Try whether other installed versions are supported, versions + # are already sorted in descending order + for installed_version in _installed_versions: + if (installed_version['version_full_ints'] >= version_full_to_ints(version_min_full) + and int(installed_version['version_major']) < int(version_major)): + installation = installed_version + break + + # Still nothing found, warn for version_major + if installation is None: + vs_info = get_supported_version(version_major) + log.warn(f'Could not open project in Visual Studio, {vs_info["name"]} was not found.') + return False + + sln_path = get_sln_path() + devenv_path = os.path.join(installation['path'], 'Common7', 'IDE', 'devenv.exe') + cmd = ['start', devenv_path, sln_path] + + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + log.warn_called_process_error(e) + return False + + return True + + +def enable_vsvars_env(version_major: str, done: Callable[[], None]) -> bool: + installation = get_installed_version(version_major, re_fetch=True) + if installation is None: + vs_info = get_supported_version(version_major) + log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.') + return False + + wrd = bpy.data.worlds['Arm'] + arch_bits = '64' if wrd.arm_project_win_build_arch == 'x64' else '32' + vcvars_path = os.path.join(installation['path'], 'VC', 'Auxiliary', 'Build', 'vcvars' + arch_bits + '.bat') + + if not os.path.isfile(vcvars_path): + log.error( + 'Could not compile project in Visual Studio\n' + f' File "{vcvars_path}" not found. Please verify that {installation["name"]} was installed correctly.' + ) + return False + + state.proc_publish_build = arm.make.run_proc(vcvars_path, done) + return True + + +def compile_in_vs(version_major: str, done: Callable[[], None]) -> bool: + installation = get_installed_version(version_major, re_fetch=True) + if installation is None: + vs_info = get_supported_version(version_major) + log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.') + return False + + wrd = bpy.data.worlds['Arm'] + + msbuild_path = os.path.join(installation['path'], 'MSBuild', 'Current', 'Bin', 'MSBuild.exe') + if not os.path.isfile(msbuild_path): + log.error( + 'Could not compile project in Visual Studio\n' + f' File "{msbuild_path}" not found. Please verify that {installation["name"]} was installed correctly.' + ) + return False + + projectfile_path = get_vcxproj_path() + + cmd = [msbuild_path, projectfile_path] + + # Arguments + platform = 'x64' if wrd.arm_project_win_build_arch == 'x64' else 'win32' + log_param = wrd.arm_project_win_build_log + if log_param == 'WarningsAndErrorsOnly': + log_param = 'WarningsOnly;ErrorsOnly' + + cmd.extend([ + '-m:' + str(wrd.arm_project_win_build_cpu), + '-clp:' + log_param, + '/p:Configuration=' + wrd.arm_project_win_build_mode, + '/p:Platform=' + platform + ]) + + print('\nCompiling the project ' + projectfile_path) + state.proc_publish_build = arm.make.run_proc(cmd, done) + state.redraw_ui = True + return True + + +def _vswhere_get_display_name(instance_data: dict[str, Any]) -> Optional[str]: + name_raw = instance_data.get('displayName', None) + if name_raw is None: + return None + return arm.utils.safestr(name_raw).replace('_', ' ').strip() + + +def _vswhere_get_version(instance_data: dict[str, Any]) -> Optional[tuple[str, str, tuple[int, ...]]]: + version_raw = instance_data.get('installationVersion', None) + if version_raw is None: + return None + + version_full = version_raw.strip() + version_full_ints = version_full_to_ints(version_full) + version_major = version_full.split('.')[0] + return version_major, version_full, version_full_ints + + +def _vswhere_get_path(instance_data: dict[str, Any]) -> Optional[str]: + return instance_data.get('installationPath', None) + + +def _vswhere_get_instances(silent=False) -> Optional[list[dict[str, Any]]]: + # vswhere.exe only exists at that location since VS2017 v15.2, for + # earlier versions we'd need to package vswhere with Armory + exe_path = os.path.join(os.environ["ProgramFiles(x86)"], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') + command = [exe_path, '-format', 'json', '-utf8'] + + try: + result = subprocess.check_output(command) + except subprocess.CalledProcessError as e: + # Do not silence this warning, if this exception is caught there + # likely is an issue in the command above + log.warn_called_process_error(e) + return None + except FileNotFoundError as e: + if not silent: + log.warn(f'Could not open file "{exe_path}", make sure the file exists (errno {e.errno}).') + return None + + result = json.loads(result.decode('utf-8')) + return result + + +def version_full_to_ints(version_full: str) -> tuple[int, ...]: + return tuple(int(i) for i in version_full.split('.')) + + +def get_project_path() -> str: + return os.path.join(arm.utils.get_fp_build(), 'windows-hl-build') + + +def get_project_name(): + wrd = bpy.data.worlds['Arm'] + return arm.utils.safesrc(wrd.arm_project_name + '-' + wrd.arm_project_version) + + +def get_sln_path() -> str: + project_path = get_project_path() + project_name = get_project_name() + return os.path.join(project_path, project_name + '.sln') + + +def get_vcxproj_path() -> str: + project_name = get_project_name() + project_path = get_project_path() + return os.path.join(project_path, project_name + '.vcxproj') + + +def fetch_project_version() -> tuple[Optional[str], Optional[str], Optional[str]]: + version_major = None + version_min_full = None + + try: + # References: + # https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header + # https://github.com/Kode/kmake/blob/a104a89b55218054ceed761d5bc75d6e5cd60573/kmake/src/Exporters/VisualStudioExporter.ts#L188-L225 + with open(get_sln_path(), 'r') as file: + for linenum, line in enumerate(file): + line = line.strip() + + if linenum == 1: + if line == '# Visual Studio Version 17': + version_major = 17 + elif line == '# Visual Studio Version 16': + version_major = 16 + elif line == '# Visual Studio 15': + version_major = 15 + elif line == '# Visual Studio 14': + version_major = 14 + elif line == '# Visual Studio 2013': + version_major = 12 + elif line == '# Visual Studio 2012': + version_major = 11 + elif line == '# Visual Studio 2010': + version_major = 10 + else: + log.warn(f'Could not parse Visual Studio version. Invalid major version, parsed {line}') + return None, None, 'err_invalid_version_major' + + elif linenum == 3 and version_major >= 12: + match = _REGEX_SLN_MIN_VERSION.match(line) + if match: + version_min_full = match.group(1) + break + + log.warn(f'Could not parse Visual Studio version. Invalid full version, parsed {line}') + return None, None, 'err_invalid_version_full' + + except FileNotFoundError: + return None, None, 'err_file_not_found' + + return str(version_major), version_min_full, None diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py new file mode 100644 index 0000000000..6e6fdd5b13 --- /dev/null +++ b/blender/arm/write_data.py @@ -0,0 +1,838 @@ +import glob +import json +import os +import shutil +import stat +import html +from typing import List + +import bpy + +import arm.assets as assets +import arm.make_renderpath as make_renderpath +import arm.make_state as state +import arm.utils + +if arm.is_reload(__name__): + import arm + assets = arm.reload_module(assets) + make_renderpath = arm.reload_module(make_renderpath) + state = arm.reload_module(state) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +def on_same_drive(path1: str, path2: str) -> bool: + drive_path1, _ = os.path.splitdrive(path1) + drive_path2, _ = os.path.splitdrive(path2) + return drive_path1 == drive_path2 + + +def add_armory_library(sdk_path: str, name: str, rel_path=False) -> str: + if rel_path: + sdk_path = '../' + os.path.relpath(sdk_path, arm.utils.get_fp()).replace('\\', '/') + + return ('project.addLibrary("' + sdk_path + '/' + name + '");\n').replace('\\', '/').replace('//', '/') + + +def add_assets(path: str, quality=1.0, use_data_dir=False, rel_path=False) -> str: + if not bpy.data.worlds['Arm'].arm_minimize and path.endswith('.arm'): + path = path[:-4] + '.json' + + if rel_path: + path = os.path.relpath(path, arm.utils.get_fp()).replace('\\', '/') + + notinlist = not path.endswith('.ttf') # TODO + s = 'project.addAssets("' + path + '", { notinlist: ' + str(notinlist).lower() + ' ' + if quality < 1.0: + s += ', quality: ' + str(quality) + if use_data_dir: + s += ', destination: "data/{name}"' + s += '});\n' + return s + + +def add_shaders(path: str, rel_path=False) -> str: + if rel_path: + path = os.path.relpath(path, arm.utils.get_fp()) + return 'project.addShaders("' + path.replace('\\', '/').replace('//', '/') + '");\n' + + +def remove_readonly(func, path, excinfo): + os.chmod(path, stat.S_IWRITE) + func(path) + + +def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, is_publish: bool, + import_traits: List[str]) -> None: + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + sdk_path = arm.utils.get_sdk_path() + rel_path = arm.utils.get_relative_paths() # Convert absolute paths to relative + project_path = arm.utils.get_fp() + build_dir = arm.utils.build_dir() + + # Whether to use relative paths for paths inside the SDK + do_relpath_sdk = rel_path and on_same_drive(sdk_path, project_path) + + with open('khafile.js', 'w', encoding="utf-8") as khafile: + khafile.write( +"""// Auto-generated +let project = new Project('""" + arm.utils.safesrc(wrd.arm_project_name + '-' + wrd.arm_project_version) + """'); + +project.addSources('Sources'); +""") + + # Auto-add assets located in Bundled directory + if os.path.exists('Bundled'): + for file in glob.glob("Bundled/**", recursive=True): + if os.path.isfile(file): + assets.add(file) + + # Auto-add shape key textures if exists + if os.path.exists('MorphTargets'): + for file in glob.glob("MorphTargets/**", recursive=True): + if os.path.isfile(file): + assets.add(file) + + # Add project shaders + if os.path.exists('Shaders'): + # Copy to enable includes + shader_path = os.path.join(build_dir, 'compiled', 'Shaders', 'Project') + if os.path.exists(shader_path): + shutil.rmtree(shader_path, onerror=remove_readonly) + shutil.copytree('Shaders', shader_path) + + khafile.write("project.addShaders('" + build_dir + "/compiled/Shaders/Project/**');\n") + # for file in glob.glob("Shaders/**", recursive=True): + # if os.path.isfile(file): + # assets.add_shader(file) + + # Add engine sources if the project does not use its own armory/iron versions + if not os.path.exists(os.path.join('Libraries', 'armory')): + khafile.write(add_armory_library(sdk_path, 'armory', rel_path=do_relpath_sdk)) + if not os.path.exists(os.path.join('Libraries', 'iron')): + khafile.write(add_armory_library(sdk_path, 'iron', rel_path=do_relpath_sdk)) + + # Project libraries + if os.path.exists('Libraries'): + libs = os.listdir('Libraries') + for lib in libs: + if os.path.isdir('Libraries/' + lib): + khafile.write('project.addLibrary("{0}");\n'.format(lib.replace('//', '/'))) + + # Subprojects, merge this with libraries + if os.path.exists('Subprojects'): + libs = os.listdir('Subprojects') + for lib in libs: + if os.path.isdir('Subprojects/' + lib): + khafile.write('await project.addProject("Subprojects/{0}");\n'.format(lib)) + + if state.target.startswith('krom'): + assets.add_khafile_def('js-es=6') + + if export_physics: + assets.add_khafile_def('arm_physics') + if wrd.arm_physics_engine == 'Bullet': + assets.add_khafile_def('arm_bullet') + if not os.path.exists('Libraries/haxebullet'): + khafile.write(add_armory_library(sdk_path + '/lib/', 'haxebullet', rel_path=do_relpath_sdk)) + if state.target.startswith('krom'): + ammojs_path = sdk_path + '/lib/haxebullet/ammo/ammo.wasm.js' + ammojs_path = ammojs_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(ammojs_path, rel_path=do_relpath_sdk)) + ammojs_wasm_path = sdk_path + '/lib/haxebullet/ammo/ammo.wasm.wasm' + ammojs_wasm_path = ammojs_wasm_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(ammojs_wasm_path, rel_path=do_relpath_sdk)) + elif state.target == 'html5' or state.target == 'node': + ammojs_path = sdk_path + '/lib/haxebullet/ammo/ammo.js' + ammojs_path = ammojs_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(ammojs_path, rel_path=do_relpath_sdk)) + elif wrd.arm_physics_engine == 'Oimo': + assets.add_khafile_def('arm_oimo') + if not os.path.exists('Libraries/oimo'): + khafile.write(add_armory_library(sdk_path + '/lib/', 'oimo', rel_path=do_relpath_sdk)) + + if export_navigation: + assets.add_khafile_def('arm_navigation') + if not os.path.exists('Libraries/haxerecast'): + khafile.write(add_armory_library(sdk_path + '/lib/', 'haxerecast', rel_path=do_relpath_sdk)) + if state.target.startswith('krom') or state.target == 'html5': + recastjs_path = sdk_path + '/lib/haxerecast/js/recast/recast.js' + recastjs_path = recastjs_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(recastjs_path, rel_path=do_relpath_sdk)) + + if is_publish: + assets.add_khafile_def('arm_published') + if wrd.arm_dce: + khafile.write("project.addParameter('-dce full');\n") + if wrd.arm_no_traces: + khafile.write("project.addParameter('--no-traces');\n") + if wrd.arm_asset_compression: + assets.add_khafile_def('arm_compress') + + else: + assets.add_khafile_def(f'arm_assert_level={wrd.arm_assert_level}') + if wrd.arm_assert_quit: + assets.add_khafile_def('arm_assert_quit') + # khafile.write("""project.addParameter("--macro include('armory.trait')");\n""") + # khafile.write("""project.addParameter("--macro include('armory.trait.internal')");\n""") + # if export_physics: + # khafile.write("""project.addParameter("--macro include('armory.trait.physics')");\n""") + # if wrd.arm_physics_engine == 'Bullet': + # khafile.write("""project.addParameter("--macro include('armory.trait.physics.bullet')");\n""") + # else: + # khafile.write("""project.addParameter("--macro include('armory.trait.physics.oimo')");\n""") + # if export_navigation: + # khafile.write("""project.addParameter("--macro include('armory.trait.navigation')");\n""") + + if not wrd.arm_compiler_inline: + khafile.write("project.addParameter('--no-inline');\n") + + use_live_patch = arm.utils.is_livepatch_enabled() + if wrd.arm_debug_console or use_live_patch: + import_traits.append('armory.trait.internal.Bridge') + if use_live_patch: + assets.add_khafile_def('arm_patch') + # Include all logic node classes so that they can later + # get instantiated + khafile.write("""project.addParameter("--macro include('armory.logicnode')");\n""") + + import_traits = list(set(import_traits)) + for i in range(0, len(import_traits)): + khafile.write("project.addParameter('" + import_traits[i] + "');\n") + khafile.write("""project.addParameter("--macro keep('""" + import_traits[i] + """')");\n""") + + noembed = wrd.arm_cache_build and not is_publish and state.target == 'krom' + if noembed: + # Load shaders manually + assets.add_khafile_def('arm_noembed') + + noembed = False # TODO: always embed shaders for now, check compatibility with haxe compile server + + shaders_path = build_dir + '/compiled/Shaders/*.glsl' + if rel_path: + shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/') + khafile.write('project.addShaders("' + shaders_path + '", { noembed: ' + str(noembed).lower() + '});\n') + + if arm.utils.get_gapi() == 'direct3d11': + # noprocessing flag - gets renamed to .d3d11 + shaders_path = build_dir + '/compiled/Hlsl/*.glsl' + if rel_path: + shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/') + khafile.write('project.addShaders("' + shaders_path + '", { noprocessing: true, noembed: ' + str(noembed).lower() + ' });\n') + + # Move assets for published game to /data folder + use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl') + if use_data_dir: + assets.add_khafile_def('arm_data_dir') + + ext = 'arm' if wrd.arm_minimize else 'json' + assets_path = build_dir + '/compiled/Assets/**' + assets_path_sh = build_dir + '/compiled/Shaders/*.' + ext + if rel_path: + assets_path = os.path.relpath(assets_path, project_path).replace('\\', '/') + assets_path_sh = os.path.relpath(assets_path_sh, project_path).replace('\\', '/') + dest = '' + if use_data_dir: + dest += ', destination: "data/{name}"' + khafile.write('project.addAssets("' + assets_path + '", { notinlist: true ' + dest + '});\n') + khafile.write('project.addAssets("' + assets_path_sh + '", { notinlist: true ' + dest + '});\n') + + shader_data_references = sorted(list(set(assets.shader_datas))) + for ref in shader_data_references: + ref = ref.replace('\\', '/').replace('//', '/') + if '/compiled/' in ref: # Asset already included + continue + do_relpath_shaders = rel_path and on_same_drive(ref, project_path) + khafile.write(add_assets(ref, use_data_dir=use_data_dir, rel_path=do_relpath_shaders)) + + asset_references = sorted(list(set(assets.assets))) + for ref in asset_references: + ref = ref.replace('\\', '/').replace('//', '/') + if '/compiled/' in ref: # Asset already included + continue + quality = 1.0 + s = ref.lower() + if s.endswith('.wav'): + quality = wrd.arm_sound_quality + elif s.endswith('.png') or s.endswith('.jpg'): + quality = wrd.arm_texture_quality + + do_relpath_assets = rel_path and on_same_drive(ref, project_path) + khafile.write(add_assets(ref, quality=quality, use_data_dir=use_data_dir, rel_path=do_relpath_assets)) + + if wrd.arm_sound_quality < 1.0 or state.target == 'html5': + assets.add_khafile_def('arm_soundcompress') + + if wrd.arm_audio == 'Disabled': + assets.add_khafile_def('kha_no_ogg') + else: + assets.add_khafile_def('arm_audio') + + if wrd.arm_texture_quality < 1.0: + assets.add_khafile_def('arm_texcompress') + + if wrd.arm_debug_console: + assets.add_khafile_def('arm_debug') + + if rpdat.rp_renderer == 'Forward': + # deferred line frag shader is currently handled in make.py, + # only add forward shader here + khafile.write(add_shaders(sdk_path + "/armory/Shaders/debug_draw/line.frag.glsl", rel_path=do_relpath_sdk)) + khafile.write(add_shaders(sdk_path + "/armory/Shaders/debug_draw/line.vert.glsl", rel_path=do_relpath_sdk)) + + if not is_publish and state.target == 'html5': + khafile.write("project.addParameter('--debug');\n") + + if arm.utils.get_pref_or_default('haxe_times', False): + khafile.write("project.addParameter('--times');\n") + + if export_ui: + if not os.path.exists('Libraries/zui'): + khafile.write(add_armory_library(sdk_path, 'lib/zui', rel_path=do_relpath_sdk)) + p = sdk_path + '/armory/Assets/font_default.ttf' + p = p.replace('//', '/') + khafile.write(add_assets(p.replace('\\', '/'), use_data_dir=use_data_dir, rel_path=do_relpath_sdk)) + assets.add_khafile_def('arm_ui') + + if export_network: + if not os.path.exists('Libraries/network'): + khafile.write(add_armory_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk)) + assets.add_khafile_def('arm_network') + + if not wrd.arm_minimize: + assets.add_khafile_def('arm_json') + + if wrd.arm_deinterleaved_buffers: + assets.add_khafile_def('arm_deinterleaved') + + if wrd.arm_batch_meshes: + assets.add_khafile_def('arm_batch') + + if wrd.arm_stream_scene: + assets.add_khafile_def('arm_stream') + + rpdat = arm.utils.get_rp() + if rpdat.arm_skin != 'Off': + assets.add_khafile_def('arm_skin') + + if rpdat.arm_morph_target != 'Off': + assets.add_khafile_def('arm_morph_target') + + if rpdat.arm_particles != 'Off': + assets.add_khafile_def('arm_particles') + + if rpdat.rp_draw_order == 'Shader': + assets.add_khafile_def('arm_draworder_shader') + + if arm.utils.get_viewport_controls() == 'azerty': + assets.add_khafile_def('arm_azerty') + + if os.path.exists(project_path + '/Bundled/config.arm'): + assets.add_khafile_def('arm_config') + + if is_publish and wrd.arm_loadscreen: + assets.add_khafile_def('arm_loadscreen') + + if wrd.arm_winresize or state.target == 'html5': + assets.add_khafile_def('arm_resizable') + + # if bpy.data.scenes[0].unit_settings.system_rotation == 'DEGREES': + # assets.add_khafile_def('arm_degrees') + + # Allow libraries to recognize Armory + assets.add_khafile_def('armory') + + for d in assets.khafile_defs: + khafile.write("project.addDefine('" + d + "');\n") + + for p in assets.khafile_params: + khafile.write("project.addParameter('" + p + "');\n") + + if state.target.startswith('android'): + bundle = 'org.armory3d.' + wrd.arm_project_package if wrd.arm_project_bundle == '' else wrd.arm_project_bundle + khafile.write("project.targetOptions.android_native.package = '{0}';\n".format(arm.utils.safestr(bundle))) + if wrd.arm_winorient != 'Multi': + khafile.write("project.targetOptions.android_native.screenOrientation = '{0}';\n".format(wrd.arm_winorient.lower())) + # Android SDK Versions + khafile.write("project.targetOptions.android_native.compileSdkVersion = '{0}';\n".format(wrd.arm_project_android_sdk_compile)) + khafile.write("project.targetOptions.android_native.minSdkVersion = '{0}';\n".format(wrd.arm_project_android_sdk_min)) + khafile.write("project.targetOptions.android_native.targetSdkVersion = '{0}';\n".format(wrd.arm_project_android_sdk_target)) + # Permissions + if len(wrd.arm_exporter_android_permission_list) > 0: + perms = '' + for item in wrd.arm_exporter_android_permission_list: + perm = "'android.permission."+ item.arm_android_permissions + "'" + # Checking In + if perms.find(perm) == -1: + if len(perms) > 0: + perms = perms + ', ' + perm + else: + perms = perm + if len(perms) > 0: + khafile.write("project.targetOptions.android_native.permissions = [{0}];\n".format(perms)) + # Android ABI Filters + if len(wrd.arm_exporter_android_abi_list) > 0: + abis = '' + for item in wrd.arm_exporter_android_abi_list: + abi = "'"+ item.arm_android_abi + "'" + # Checking In + if abis.find(abi) == -1: + if len(abis) > 0: + abis = abis + ', ' + abi + else: + abis = abi + if len(abis) > 0: + khafile.write("project.targetOptions.android_native.abiFilters = [{0}];\n".format(abis)) + elif state.target.startswith('ios'): + bundle = 'org.armory3d.' + wrd.arm_project_package if wrd.arm_project_bundle == '' else wrd.arm_project_bundle + khafile.write("project.targetOptions.ios.bundle = '{0}';\n".format(arm.utils.safestr(bundle))) + + if wrd.arm_project_icon != '': + shutil.copy(bpy.path.abspath(wrd.arm_project_icon), project_path + '/icon.png') + + if wrd.arm_khafile is not None: + khafile.write(wrd.arm_khafile.as_string()) + + khafile.write("\n\nresolve(project);\n") + + +def get_winmode(arm_winmode): + if arm_winmode == 'Window': + return 0 + else: # Fullscreen + return 1 + + +def write_config(resx, resy): + wrd = bpy.data.worlds['Arm'] + p = os.path.join(arm.utils.get_fp(), 'Bundled') + if not os.path.exists(p): + os.makedirs(p) + + rpdat = arm.utils.get_rp() + rp_shadowmap_cube = int(rpdat.rp_shadowmap_cube) if rpdat.rp_shadows else 0 + rp_shadowmap_cascade = arm.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0 + + output = { + 'window_mode': get_winmode(wrd.arm_winmode), + 'window_w': int(resx), + 'window_h': int(resy), + 'window_resizable': wrd.arm_winresize, + 'window_maximizable': wrd.arm_winresize and wrd.arm_winmaximize, + 'window_minimizable': wrd.arm_winminimize, + 'window_vsync': wrd.arm_vsync, + 'window_msaa': int(rpdat.arm_samples_per_pixel), + 'window_scale': 1.0, + 'rp_supersample': float(rpdat.rp_supersampling), + 'rp_shadowmap_cube': rp_shadowmap_cube, + 'rp_shadowmap_cascade': rp_shadowmap_cascade, + 'rp_ssgi': rpdat.rp_ssgi != 'Off', + 'rp_ssr': rpdat.rp_ssr != 'Off', + 'rp_ss_refraction': rpdat.rp_ss_refraction != 'Off', + 'rp_bloom': rpdat.rp_bloom != 'Off', + 'rp_motionblur': rpdat.rp_motionblur != 'Off', + 'rp_voxels': rpdat.rp_voxels, + 'rp_dynres': rpdat.rp_dynres + } + + with open(os.path.join(p, 'config.arm'), 'w') as configfile: + configfile.write(json.dumps(output, sort_keys=True, indent=4)) + + +def write_mainhx(scene_name, resx, resy, is_play, is_publish): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + scene_ext = '.lz4' if (wrd.arm_asset_compression and is_publish) else '' + if scene_ext == '' and not wrd.arm_minimize: + scene_ext = '.json' + winmode = get_winmode(wrd.arm_winmode) + # Detect custom render path + pathpack = 'armory' + if os.path.isfile(arm.utils.get_fp() + '/Sources/' + wrd.arm_project_package + '/renderpath/RenderPathCreator.hx'): + pathpack = wrd.arm_project_package + elif rpdat.rp_driver != 'Armory': + pathpack = rpdat.rp_driver.lower() + + with open('Sources/Main.hx', 'w', encoding="utf-8") as f: + f.write( +"""// Auto-generated +package ; +class Main { + public static inline var projectName = '""" + arm.utils.safestr(wrd.arm_project_name) + """'; + public static inline var projectVersion = '""" + arm.utils.safestr(wrd.arm_project_version) + """'; + public static inline var projectPackage = '""" + arm.utils.safestr(wrd.arm_project_package) + """';""") + + if rpdat.rp_voxels: + f.write(""" + public static inline var voxelgiVoxelSize = """ + str(rpdat.arm_voxelgi_dimensions) + " / " + str(rpdat.rp_voxelgi_resolution) + """; + public static inline var voxelgiHalfExtents = """ + str(round(rpdat.arm_voxelgi_dimensions / 2.0)) + """;""") + + if rpdat.rp_bloom: + f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if rpdat.arm_bloom_follow_blender else rpdat.arm_bloom_radius};") + + if rpdat.arm_rp_resolution == 'Custom': + f.write(""" + public static inline var resolutionSize = """ + str(rpdat.arm_rp_resolution_size) + """;""") + + f.write(""" + public static function main() {""") + if rpdat.arm_skin != 'Off': + f.write(""" + iron.object.BoneAnimation.skinMaxBones = """ + str(rpdat.arm_skin_max_bones) + """;""") + if rpdat.rp_shadows: + if rpdat.rp_shadowmap_cascades != '1': + f.write(""" + iron.object.LightObject.cascadeCount = """ + str(rpdat.rp_shadowmap_cascades) + """; + iron.object.LightObject.cascadeSplitFactor = """ + str(rpdat.arm_shadowmap_split) + """;""") + if rpdat.arm_shadowmap_bounds != 1.0: + f.write(""" + iron.object.LightObject.cascadeBounds = """ + str(rpdat.arm_shadowmap_bounds) + """;""") + if is_publish and wrd.arm_loadscreen: + asset_references = list(set(assets.assets)) + loadscreen_class = 'armory.trait.internal.LoadingScreen' + if os.path.isfile(arm.utils.get_fp() + '/Sources/' + wrd.arm_project_package + '/LoadingScreen.hx'): + loadscreen_class = wrd.arm_project_package + '.LoadingScreen' + f.write(""" + armory.system.Starter.numAssets = """ + str(len(asset_references)) + """; + armory.system.Starter.drawLoading = """ + loadscreen_class + """.render;""") + f.write(""" + armory.system.Starter.main( + '""" + arm.utils.safestr(scene_name) + scene_ext + """', + """ + str(winmode) + """, + """ + ('true' if wrd.arm_winresize else 'false') + """, + """ + ('true' if wrd.arm_winminimize else 'false') + """, + """ + ('true' if (wrd.arm_winresize and wrd.arm_winmaximize) else 'false') + """, + """ + str(resx) + """, + """ + str(resy) + """, + """ + str(int(rpdat.arm_samples_per_pixel)) + """, + """ + ('true' if wrd.arm_vsync else 'false') + """, + """ + pathpack + """.renderpath.RenderPathCreator.get + ); + } +} +""") + +def write_indexhtml(w, h, is_publish): + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + dest = '/html5' if is_publish else '/debug/html5' + if not os.path.exists(arm.utils.build_dir() + dest): + os.makedirs(arm.utils.build_dir() + dest) + popupmenu_in_browser = '' + if wrd.arm_project_html5_popupmenu_in_browser: + popupmenu_in_browser = ' oncontextmenu="return false"' + with open(arm.utils.build_dir() + dest + '/index.html', 'w') as f: + f.write( +""" + + + """) + if rpdat.rp_stereo or wrd.arm_winmode == 'Fullscreen': + f.write(""" + +""") + f.write(""" + """+html.escape( wrd.arm_project_name)+""" + + +""") + if rpdat.rp_stereo or wrd.arm_winmode == 'Fullscreen': + f.write(""" + +""") + else: + f.write(""" +

+""") + f.write(""" + + + +""") + +add_compiledglsl = '' +def write_compiledglsl(defs, make_variants): + rpdat = arm.utils.get_rp() + wrd = bpy.data.worlds['Arm'] + shadowmap_size = arm.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0 + with open(arm.utils.build_dir() + '/compiled/Shaders/compiled.inc', 'w') as f: + f.write( +"""#ifndef _COMPILED_GLSL_ +#define _COMPILED_GLSL_ +""") + for d in defs: + if make_variants and d.endswith('var'): + continue # Write a shader variant instead + f.write("#define " + d + "\n") + + if rpdat.rp_renderer == 'Deferred': + gbuffer_size = make_renderpath.get_num_gbuffer_rts_deferred() + f.write(f'#define GBUF_SIZE {gbuffer_size}\n') + + # Write indices of G-Buffer render targets + f.write('#define GBUF_IDX_0 0\n') + f.write('#define GBUF_IDX_1 1\n') + + idx_emission = 2 + idx_refraction = 2 + if '_gbuffer2' in wrd.world_defs: + f.write('#define GBUF_IDX_2 2\n') + idx_emission += 1 + idx_refraction += 1 + + if '_EmissionShaded' in wrd.world_defs: + f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') + idx_refraction += 1 + + if '_SSRefraction' in wrd.world_defs: + f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') + + f.write("""#if defined(HLSL) || defined(METAL) +#define _InvY +#endif +""") + + if state.target == 'html5' or arm.utils.get_gapi() == 'direct3d11': + f.write("#define _FlipY\n") + + f.write("""const float PI = 3.1415926535; +const float PI2 = PI * 2.0; +const vec2 shadowmapSize = vec2(""" + str(shadowmap_size) + """, """ + str(shadowmap_size) + """); +const float shadowmapCubePcfSize = """ + str((round(rpdat.arm_pcfsize * 100) / 100) / 1000) + """; +const int shadowmapCascades = """ + str(rpdat.rp_shadowmap_cascades) + """; +""") + + if rpdat.rp_water: + f.write( +"""const float waterLevel = """ + str(round(rpdat.arm_water_level * 100) / 100) + """; +const float waterDisplace = """ + str(round(rpdat.arm_water_displace * 100) / 100) + """; +const float waterSpeed = """ + str(round(rpdat.arm_water_speed * 100) / 100) + """; +const float waterFreq = """ + str(round(rpdat.arm_water_freq * 100) / 100) + """; +const vec3 waterColor = vec3(""" + str(round(rpdat.arm_water_color[0] * 100) / 100) + """, """ + str(round(rpdat.arm_water_color[1] * 100) / 100) + """, """ + str(round(rpdat.arm_water_color[2] * 100) / 100) + """); +const float waterDensity = """ + str(round(rpdat.arm_water_density * 100) / 100) + """; +const float waterRefract = """ + str(round(rpdat.arm_water_refract * 100) / 100) + """; +const float waterReflect = """ + str(round(rpdat.arm_water_reflect * 100) / 100) + """; +""") + if rpdat.rp_ssgi == 'SSAO' or rpdat.rp_ssgi == 'RTAO' or rpdat.rp_volumetriclight: + f.write( +"""const float ssaoRadius = """ + str(round(rpdat.arm_ssgi_radius * 100) / 100) + """; +const float ssaoStrength = """ + str(round(rpdat.arm_ssgi_strength * 100) / 100) + """; +const float ssaoScale = """ + ("2.0" if rpdat.arm_ssgi_half_res else "20.0") + """; +""") + + if rpdat.rp_ssgi == 'RTGI' or rpdat.rp_ssgi == 'RTAO': + f.write( +"""const int ssgiMaxSteps = """ + str(rpdat.arm_ssgi_max_steps) + """; +const float ssgiRayStep = 0.005 * """ + str(round(rpdat.arm_ssgi_step * 100) / 100) + """; +const float ssgiStrength = """ + str(round(rpdat.arm_ssgi_strength * 100) / 100) + """; +""") + + if rpdat.rp_bloom: + follow_blender = rpdat.arm_bloom_follow_blender + eevee_settings = bpy.context.scene.eevee + + threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.arm_bloom_threshold + strength = eevee_settings.bloom_intensity if follow_blender else rpdat.arm_bloom_strength + knee = eevee_settings.bloom_knee if follow_blender else rpdat.arm_bloom_knee + + f.write( +"""const float bloomThreshold = """ + str(round(threshold * 100) / 100) + """; +const float bloomStrength = """ + str(round(strength * 100) / 100) + """; +const float bloomKnee = """ + str(round(knee * 100) / 100) + """; +const float bloomRadius = """ + str(round(rpdat.arm_bloom_radius * 100) / 100) + """; +""") # TODO remove radius if old bloom pass is removed + + if rpdat.rp_motionblur != 'Off': + f.write( +"""const float motionBlurIntensity = """ + str(round(rpdat.arm_motion_blur_intensity * 100) / 100) + """; +""") + if rpdat.rp_ssr: + f.write( +"""const float ssrRayStep = """ + str(round(rpdat.arm_ssr_ray_step * 100) / 100) + """; +const float ssrSearchDist = """ + str(round(rpdat.arm_ssr_search_dist * 100) / 100) + """; +const float ssrFalloffExp = """ + str(round(rpdat.arm_ssr_falloff_exp * 100) / 100) + """; +const float ssrJitter = """ + str(round(rpdat.arm_ssr_jitter * 100) / 100) + """; +""") + if rpdat.rp_ss_refraction: + f.write( +"""const float ss_refractionRayStep = """ + str(round(rpdat.arm_ss_refraction_ray_step * 100) / 100) + """; +const float ss_refractionSearchDist = """ + str(round(rpdat.arm_ss_refraction_search_dist * 100) / 100) + """; +const float ss_refractionFalloffExp = """ + str(round(rpdat.arm_ss_refraction_falloff_exp * 100) / 100) + """; +const float ss_refractionJitter = """ + str(round(rpdat.arm_ss_refraction_jitter * 100) / 100) + """; +""") + + if rpdat.arm_ssrs: + f.write( +"""const float ssrsRayStep = """ + str(round(rpdat.arm_ssrs_ray_step * 100) / 100) + """; +""") + + if rpdat.rp_volumetriclight: + f.write( +"""const float volumAirTurbidity = """ + str(round(rpdat.arm_volumetric_light_air_turbidity * 100) / 100) + """; +const vec3 volumAirColor = vec3(""" + str(round(rpdat.arm_volumetric_light_air_color[0] * 100) / 100) + """, """ + str(round(rpdat.arm_volumetric_light_air_color[1] * 100) / 100) + """, """ + str(round(rpdat.arm_volumetric_light_air_color[2] * 100) / 100) + """); +const int volumSteps = """ + str(rpdat.arm_volumetric_light_steps) + """; +""") + + if rpdat.rp_autoexposure: + f.write( +"""const float autoExposureStrength = """ + str(rpdat.arm_autoexposure_strength) + """; +const float autoExposureSpeed = """ + str(rpdat.arm_autoexposure_speed) + """; +""") + + # Compositor + if rpdat.arm_letterbox: + f.write( +"""const float compoLetterboxSize = """ + str(round(rpdat.arm_letterbox_size * 100) / 100) + """; +const vec3 compoLetterboxColor = vec3(""" + str(round(rpdat.arm_letterbox_color[0] * 100) / 100) + """, """ + str(round(rpdat.arm_letterbox_color[1] * 100) / 100) + """, """ + str(round(rpdat.arm_letterbox_color[2] * 100) / 100) + """); +""") + + if rpdat.arm_distort: + f.write( +"""const float compoDistortStrength = """ + str(round(rpdat.arm_distort_strength * 100) / 100) + """; +""") + + if rpdat.arm_grain: + f.write( +"""const float compoGrainStrength = """ + str(round(rpdat.arm_grain_strength * 100) / 100) + """; +""") + + if rpdat.arm_vignette: + f.write( +"""const float compoVignetteStrength = """ + str(round(rpdat.arm_vignette_strength * 100) / 100) + """; +""") + + if rpdat.arm_sharpen: + f.write( +"""const float compoSharpenStrength = """ + str(round(rpdat.arm_sharpen_strength * 100) / 100) + """; +""") + + if bpy.data.scenes[0].view_settings.exposure != 0.0: + f.write( +"""const float compoExposureStrength = """ + str(round(bpy.data.scenes[0].view_settings.exposure * 100) / 100) + """; +""") + + if rpdat.arm_fog: + f.write( +"""const float compoFogAmountA = """ + str(round(rpdat.arm_fog_amounta * 100) / 100) + """; +const float compoFogAmountB = """ + str(round(rpdat.arm_fog_amountb * 100) / 100) + """; +const vec3 compoFogColor = vec3(""" + str(round(rpdat.arm_fog_color[0] * 100) / 100) + """, """ + str(round(rpdat.arm_fog_color[1] * 100) / 100) + """, """ + str(round(rpdat.arm_fog_color[2] * 100) / 100) + """); +""") + + if rpdat.arm_lens_texture_masking: + f.write( +"""const float compoCenterMinClip = """ + str(round(rpdat.arm_lens_texture_masking_centerMinClip * 100) / 100) + """; +const float compoCenterMaxClip = """ + str(round(rpdat.arm_lens_texture_masking_centerMaxClip * 100) / 100) + """; +const float compoLuminanceMin = """ + str(round(rpdat.arm_lens_texture_masking_luminanceMin * 100) / 100) + """; +const float compoLuminanceMax = """ + str(round(rpdat.arm_lens_texture_masking_luminanceMax * 100) / 100) + """; +const float compoBrightnessExponent = """ + str(round(rpdat.arm_lens_texture_masking_brightnessExp * 100) / 100) + """; +""") + + if rpdat.rp_chromatic_aberration: + f.write( +f"""const float compoChromaticStrength = {round(rpdat.arm_chromatic_aberration_strength * 100) / 100}; +const int compoChromaticSamples = {rpdat.arm_chromatic_aberration_samples}; +""") + + if rpdat.arm_chromatic_aberration_type == "Spectral": + f.write("const int compoChromaticType = 1;") + else: + f.write("const int compoChromaticType = 0;") + + focus_distance = 0.0 + fstop = 0.0 + if len(bpy.data.cameras) > 0 and bpy.data.cameras[0].dof.use_dof: + focus_distance = bpy.data.cameras[0].dof.focus_distance + fstop = bpy.data.cameras[0].dof.aperture_fstop + + if focus_distance > 0.0: + f.write( +"""const float compoDOFDistance = """ + str(round(focus_distance * 100) / 100) + """; +const float compoDOFFstop = """ + str(round(fstop * 100) / 100) + """; +const float compoDOFLength = 160.0; +""") # str(round(bpy.data.cameras[0].lens * 100) / 100) + + if rpdat.rp_voxels: + halfext = round(rpdat.arm_voxelgi_dimensions / 2.0) + f.write( +"""const ivec3 voxelgiResolution = ivec3(""" + str(rpdat.rp_voxelgi_resolution) + """, """ + str(rpdat.rp_voxelgi_resolution) + """, """ + str(int(int(rpdat.rp_voxelgi_resolution) * float(rpdat.rp_voxelgi_resolution_z))) + """); +const vec3 voxelgiHalfExtents = vec3(""" + str(halfext) + """, """ + str(halfext) + """, """ + str(round(halfext * float(rpdat.rp_voxelgi_resolution_z))) + """); +const float voxelgiOcc = """ + str(round(rpdat.arm_voxelgi_occ * 100) / 100) + """; +const float voxelgiEnv = """ + str(round(rpdat.arm_voxelgi_env * 100) / 100) + """ / 10.0; +const float voxelgiStep = """ + str(round(rpdat.arm_voxelgi_step * 100) / 100) + """; +const float voxelgiRange = """ + str(round(rpdat.arm_voxelgi_range * 100) / 100) + """; +const float voxelgiOffset = """ + str(round(rpdat.arm_voxelgi_offset * 100) / 100) + """; +const float voxelgiAperture = """ + str(round(rpdat.arm_voxelgi_aperture * 100) / 100) + """; +""") + + if rpdat.rp_sss: + f.write(f"const float sssWidth = {rpdat.arm_sss_width / 10.0};\n") + + # Skinning + if rpdat.arm_skin == 'On': + f.write( +"""const int skinMaxBones = """ + str(rpdat.arm_skin_max_bones) + """; +""") + + if '_Clusters' in wrd.world_defs: + max_lights = "4" + max_lights_clusters = "4" + if rpdat.rp_shadowmap_atlas: + max_lights = str(rpdat.rp_max_lights) + max_lights_clusters = str(rpdat.rp_max_lights_cluster) + # prevent max lights cluster being higher than max lights + if (int(max_lights_clusters) > int(max_lights)): + max_lights_clusters = max_lights + + f.write( +"""const int maxLights = """ + max_lights + """; +const int maxLightsCluster = """ + max_lights_clusters + """; +const float clusterNear = 3.0; +""") + + f.write(add_compiledglsl + '\n') # External defined constants + + f.write("""#endif // _COMPILED_GLSL_ +""") + +def write_traithx(class_path): + wrd = bpy.data.worlds['Arm'] + # Split the haxe package syntax in components that will compose the path + path_components = class_path.split('.') + # extract the full file name (file + ext) from the components + class_name = path_components[-1] + # Create the absolute trait path (os-safe) + package_path = os.sep.join([arm.utils.get_fp(), 'Sources', arm.utils.safestr(wrd.arm_project_package)] + path_components[:-1]) + if not os.path.exists(package_path): + os.makedirs(package_path) + package = '.'.join([arm.utils.safestr(wrd.arm_project_package)] + path_components[:-1]); + with open(package_path + '/' + class_name + '.hx', 'w') as f: + f.write( +"""package """ + package + """; + +class """ + class_name + """ extends iron.Trait { +\tpublic function new() { +\t\tsuper(); + +\t\t// notifyOnInit(function() { +\t\t// }); + +\t\t// notifyOnUpdate(function() { +\t\t// }); + +\t\t// notifyOnRemove(function() { +\t\t// }); +\t} +} +""") + +def write_canvasjson(canvas_name): + canvas_path = arm.utils.get_fp() + '/Bundled/canvas' + if not os.path.exists(canvas_path): + os.makedirs(canvas_path) + with open(canvas_path + '/' + canvas_name + '.json', 'w') as f: + f.write( +"""{ "name": "untitled", "x": 0.0, "y": 0.0, "width": 1280, "height": 720, "theme": "Default Light", "elements": [], "assets": [] }""") diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py new file mode 100644 index 0000000000..36001e5491 --- /dev/null +++ b/blender/arm/write_probes.py @@ -0,0 +1,478 @@ +from contextlib import contextmanager +import math +import multiprocessing +import os +import re +import subprocess +import time + +import bpy + +import arm.assets as assets +import arm.log as log +import arm.utils + +if arm.is_reload(__name__): + import arm + assets = arm.reload_module(assets) + log = arm.reload_module(log) + arm.utils = arm.reload_module(arm.utils) +else: + arm.enable_reload(__name__) + + +# The format used for rendering the environment. Choose HDR or JPEG. +ENVMAP_FORMAT = 'JPEG' +ENVMAP_EXT = 'hdr' if ENVMAP_FORMAT == 'HDR' else 'jpg' + +__cmft_start_time_seconds = 0.0 +__cmft_end_time_seconds = 0.0 + + +def add_irr_assets(output_file_irr): + assets.add(output_file_irr + '.arm') + + +def add_rad_assets(output_file_rad, rad_format, num_mips): + assets.add(output_file_rad + '.' + rad_format) + for i in range(0, num_mips): + assets.add(output_file_rad + '_' + str(i) + '.' + rad_format) + + +@contextmanager +def setup_envmap_render(): + """Creates a background scene for rendering environment textures. + Use it as a context manager to automatically clean up on errors. + """ + rpdat = arm.utils.get_rp() + radiance_size = int(rpdat.arm_radiance_size) + + # Render worlds in a different scene so that there are no other + # objects. The actual scene might be called differently if the name + # is already taken + scene = bpy.data.scenes.new("_arm_envmap_render") + scene.render.engine = "CYCLES" + scene.render.image_settings.file_format = ENVMAP_FORMAT + if ENVMAP_FORMAT == 'HDR': + scene.render.image_settings.color_depth = '32' + + # Export in linear space and with default color management settings + scene.display_settings.display_device = "None" + scene.view_settings.view_transform = "Standard" + scene.view_settings.look = "None" + scene.view_settings.exposure = 0 + scene.view_settings.gamma = 1 + + scene.render.image_settings.quality = 100 + scene.render.resolution_x = radiance_size + scene.render.resolution_y = radiance_size // 2 + + # Set GPU as rendering device if the user enabled it + if bpy.context.preferences.addons["cycles"].preferences.compute_device_type == "CUDA": + scene.cycles.device = "GPU" + else: + log.info('Using CPU for environment render (might be slow). Enable CUDA if possible.') + + # Those settings are sufficient for rendering only the world background + scene.cycles.samples = 1 + scene.cycles.max_bounces = 1 + scene.cycles.diffuse_bounces = 1 + scene.cycles.glossy_bounces = 1 + scene.cycles.transmission_bounces = 1 + scene.cycles.volume_bounces = 1 + scene.cycles.transparent_max_bounces = 1 + scene.cycles.caustics_reflective = False + scene.cycles.caustics_refractive = False + scene.cycles.use_denoising = False + + # Setup scene + cam = bpy.data.cameras.new("_arm_cam_envmap_render") + cam_obj = bpy.data.objects.new("_arm_cam_envmap_render", cam) + scene.collection.objects.link(cam_obj) + scene.camera = cam_obj + + cam_obj.location = [0.0, 0.0, 0.0] + cam.type = "PANO" + cam.cycles.panorama_type = "EQUIRECTANGULAR" + cam_obj.rotation_euler = [math.radians(90), 0, math.radians(-90)] + + try: + yield + finally: + bpy.data.objects.remove(cam_obj) + bpy.data.cameras.remove(cam) + bpy.data.scenes.remove(scene) + + +def render_envmap(target_dir: str, world: bpy.types.World) -> str: + """Render an environment texture for the given world into the + target_dir and return the filename of the rendered image. Use in + combination with setup_envmap_render(). + """ + scene = bpy.data.scenes['_arm_envmap_render'] + scene.world = world + + image_name = f'env_{arm.utils.safesrc(world.name)}.{ENVMAP_EXT}' + render_path = os.path.join(target_dir, image_name) + scene.render.filepath = render_path + + bpy.ops.render.render(write_still=True, scene=scene.name) + + return image_name + + +def write_probes(image_filepath: str, disable_hdr: bool, from_srgb: bool, cached_num_mips: int, arm_radiance=True) -> int: + """Generate probes from environment map and return the mipmap count.""" + envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' + + if not os.path.exists(envpath): + os.makedirs(envpath) + + base_name = arm.utils.extract_filename(image_filepath).rsplit('.', 1)[0] + + # Assets to be generated + output_file_irr = envpath + '/' + base_name + '_irradiance' + output_file_rad = envpath + '/' + base_name + '_radiance' + rad_format = 'jpg' if disable_hdr else 'hdr' + + # Radiance & irradiance exists, keep cache + basep = envpath + '/' + base_name + if os.path.exists(basep + '_irradiance.arm'): + if not arm_radiance or os.path.exists(basep + '_radiance_0.' + rad_format): + add_irr_assets(output_file_irr) + if arm_radiance: + add_rad_assets(output_file_rad, rad_format, cached_num_mips) + return cached_num_mips + + # Get paths + sdk_path = arm.utils.get_sdk_path() + kha_path = arm.utils.get_kha_path() + + if arm.utils.get_os() == 'win': + cmft_path = sdk_path + '/lib/armory_tools/cmft/cmft.exe' + kraffiti_path = kha_path + '/Kinc/Tools/windows_x64/kraffiti.exe' + elif arm.utils.get_os() == 'mac': + cmft_path = '"' + sdk_path + '/lib/armory_tools/cmft/cmft-osx"' + kraffiti_path = '"' + kha_path + '/Kinc/Tools/macos/kraffiti"' + else: + cmft_path = '"' + sdk_path + '/lib/armory_tools/cmft/cmft-linux64"' + kraffiti_path = '"' + kha_path + '/Kinc/Tools/linux_x64/kraffiti"' + + input_file = arm.utils.asset_path(image_filepath) + + # Scale map, ensure 2:1 ratio (required by cmft) + rpdat = arm.utils.get_rp() + target_w = int(rpdat.arm_radiance_size) + target_h = int(target_w / 2) + scaled_file = output_file_rad + '.' + rad_format + + if arm.utils.get_os() == 'win': + subprocess.check_output([ + kraffiti_path, + 'from=' + input_file, + 'to=' + scaled_file, + 'format=' + rad_format, + 'width=' + str(target_w), + 'height=' + str(target_h)]) + else: + subprocess.check_output([ + kraffiti_path + + ' from="' + input_file + '"' + + ' to="' + scaled_file + '"' + + ' format=' + rad_format + + ' width=' + str(target_w) + + ' height=' + str(target_h)], shell=True) + + # Convert sRGB colors into linear color space first (approximately) + input_gamma_numerator = '2.2' if from_srgb else '1.0' + + # Irradiance spherical harmonics + if arm.utils.get_os() == 'win': + subprocess.call([ + cmft_path, + '--input', scaled_file, + '--filter', 'shcoeffs', + '--outputNum', '1', + '--output0', output_file_irr, + '--inputGammaNumerator', input_gamma_numerator, + '--inputGammaDenominator', '1.0', + '--outputGammaNumerator', '1.0', + '--outputGammaDenominator', '1.0' + ]) + else: + subprocess.call([ + cmft_path + + ' --input ' + '"' + scaled_file + '"' + + ' --filter shcoeffs' + + ' --outputNum 1' + + ' --output0 ' + '"' + output_file_irr + '"' + + ' --inputGammaNumerator' + input_gamma_numerator + + ' --inputGammaDenominator' + '1.0' + + ' --outputGammaNumerator' + '1.0' + + ' --outputGammaDenominator' + '1.0' + ], shell=True) + + sh_to_json(output_file_irr) + add_irr_assets(output_file_irr) + + # Mip-mapped radiance + if not arm_radiance: + return cached_num_mips + + # 4096 = 256 face + # 2048 = 128 face + # 1024 = 64 face + face_size = target_w / 8 + if target_w == 2048: + mip_count = 9 + elif target_w == 1024: + mip_count = 8 + else: + mip_count = 7 + + wrd = bpy.data.worlds['Arm'] + use_opencl = 'true' if arm.utils.get_pref_or_default("cmft_use_opencl", True) else 'false' + + # cmft doesn't work correctly when passing the number of logical + # CPUs if there are more logical than physical CPUs on a machine + cpu_count = arm.utils.cpu_count(physical_only=True) + + # CMFT might hang with OpenCl enabled, output warning in that case. + # See https://github.com/armory3d/armory/issues/2760 for details. + global __cmft_start_time_seconds, __cmft_end_time_seconds + __cmft_start_time_seconds = time.time() + + try: + if arm.utils.get_os() == 'win': + cmd = [ + cmft_path, + '--input', scaled_file, + '--filter', 'radiance', + '--dstFaceSize', str(face_size), + '--srcFaceSize', str(face_size), + '--excludeBase', 'false', + # '--mipCount', str(mip_count), + '--glossScale', '8', + '--glossBias', '3', + '--lightingModel', 'blinnbrdf', + '--edgeFixup', 'none', + '--numCpuProcessingThreads', str(cpu_count), + '--useOpenCL', use_opencl, + '--clVendor', 'anyGpuVendor', + '--deviceType', 'gpu', + '--deviceIndex', '0', + '--generateMipChain', 'true', + '--inputGammaNumerator', input_gamma_numerator, + '--inputGammaDenominator', '1.0', + '--outputGammaNumerator', '1.0', + '--outputGammaDenominator', '1.0', + '--outputNum', '1', + '--output0', output_file_rad, + '--output0params', 'hdr,rgbe,latlong' + ] + if not wrd.arm_verbose_output: + cmd.append('--silent') + print(cmd) + subprocess.call(cmd) + else: + cmd = cmft_path + \ + ' --input "' + scaled_file + '"' + \ + ' --filter radiance' + \ + ' --dstFaceSize ' + str(face_size) + \ + ' --srcFaceSize ' + str(face_size) + \ + ' --excludeBase false' + \ + ' --glossScale 8' + \ + ' --glossBias 3' + \ + ' --lightingModel blinnbrdf' + \ + ' --edgeFixup none' + \ + ' --numCpuProcessingThreads ' + str(cpu_count) + \ + ' --useOpenCL ' + use_opencl + \ + ' --clVendor anyGpuVendor' + \ + ' --deviceType gpu' + \ + ' --deviceIndex 0' + \ + ' --generateMipChain true' + \ + ' --inputGammaNumerator ' + input_gamma_numerator + \ + ' --inputGammaDenominator 1.0' + \ + ' --outputGammaNumerator 1.0' + \ + ' --outputGammaDenominator 1.0' + \ + ' --outputNum 1' + \ + ' --output0 "' + output_file_rad + '"' + \ + ' --output0params hdr,rgbe,latlong' + if not wrd.arm_verbose_output: + cmd += ' --silent' + print(cmd) + subprocess.call([cmd], shell=True) + + except KeyboardInterrupt as e: + __cmft_end_time_seconds = time.time() + check_last_cmft_time() + raise e + + __cmft_end_time_seconds = time.time() + + # Remove size extensions in file name + mip_w = int(face_size * 4) + mip_base = output_file_rad + '_' + mip_num = 0 + while mip_w >= 4: + mip_name = mip_base + str(mip_num) + os.rename( + mip_name + '_' + str(mip_w) + 'x' + str(int(mip_w / 2)) + '.hdr', + mip_name + '.hdr') + mip_w = int(mip_w / 2) + mip_num += 1 + + # Append mips + generated_files = [] + for i in range(0, mip_count): + generated_files.append(output_file_rad + '_' + str(i)) + + # Convert to jpgs + if disable_hdr is True: + for f in generated_files: + if arm.utils.get_os() == 'win': + subprocess.call([ + kraffiti_path, + 'from=' + f + '.hdr', + 'to=' + f + '.jpg', + 'format=jpg']) + else: + subprocess.call([ + kraffiti_path + + ' from="' + f + '.hdr"' + + ' to="' + f + '.jpg"' + + ' format=jpg'], shell=True) + os.remove(f + '.hdr') + + # Scale from (4x2 to 1x1> + for i in range(0, 2): + last = generated_files[-1] + out = output_file_rad + '_' + str(mip_count + i) + if arm.utils.get_os() == 'win': + subprocess.call([ + kraffiti_path, + 'from=' + last + '.' + rad_format, + 'to=' + out + '.' + rad_format, + 'scale=0.5', + 'format=' + rad_format], shell=True) + else: + subprocess.call([ + kraffiti_path + + ' from=' + '"' + last + '.' + rad_format + '"' + + ' to=' + '"' + out + '.' + rad_format + '"' + + ' scale=0.5' + + ' format=' + rad_format], shell=True) + generated_files.append(out) + + mip_count += 2 + + add_rad_assets(output_file_rad, rad_format, mip_count) + + return mip_count + + +def sh_to_json(sh_file): + """Parse sh coefs produced by cmft into json array""" + with open(sh_file + '.c') as f: + sh_lines = f.read().splitlines() + band0_line = sh_lines[5] + band1_line = sh_lines[6] + band2_line = sh_lines[7] + + irradiance_floats = [] + parse_band_floats(irradiance_floats, band0_line) + parse_band_floats(irradiance_floats, band1_line) + parse_band_floats(irradiance_floats, band2_line) + + # Lower exposure to adjust to Eevee and Cycles + for i in range(0, len(irradiance_floats)): + irradiance_floats[i] /= 2 + + sh_json = {'irradiance': irradiance_floats} + ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else '' + arm.utils.write_arm(sh_file + ext, sh_json) + + # Clean up .c + os.remove(sh_file + '.c') + + +def parse_band_floats(irradiance_floats, band_line): + string_floats = re.findall(r'[-+]?\d*\.\d+|\d+', band_line) + string_floats = string_floats[1:] # Remove 'Band 0/1/2' number + for s in string_floats: + irradiance_floats.append(float(s)) + + +def write_sky_irradiance(base_name): + # Hosek spherical harmonics + irradiance_floats = [ + 1.5519331988822218, 2.3352207154503266, 2.997277451988076, + 0.2673894962434794, 0.4305630474135794, 0.11331825259716752, + -0.04453633521758638, -0.038753175134160295, -0.021302768541875794, + 0.00055858020486499, 0.000371654770334503, 0.000126606145406403, + -0.000135708721978705, -0.000787399554583089, -0.001550090690860059, + 0.021947399048903773, 0.05453650591711572, 0.08783641266630278, + 0.17053593578630663, 0.14734127083304463, 0.07775404698816404, + -2.6924363189795e-05, -7.9350169701934e-05, -7.559914435231e-05, + 0.27035455385870993, 0.23122918445556914, 0.12158817295211832] + for i in range(0, len(irradiance_floats)): + irradiance_floats[i] /= 2 + + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') + if not os.path.exists(envpath): + os.makedirs(envpath) + + output_file = os.path.join(envpath, base_name + '_irradiance') + + sh_json = {'irradiance': irradiance_floats} + arm.utils.write_arm(output_file + '.arm', sh_json) + + assets.add(output_file + '.arm') + + +def write_color_irradiance(base_name, col): + """Constant color irradiance""" + # Adjust to Cycles + irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13] + for i in range(0, 24): + irradiance_floats.append(0.0) + + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') + if not os.path.exists(envpath): + os.makedirs(envpath) + + output_file = os.path.join(envpath, base_name + '_irradiance') + + sh_json = {'irradiance': irradiance_floats} + arm.utils.write_arm(output_file + '.arm', sh_json) + + assets.add(output_file + '.arm') + + +def check_last_cmft_time(): + global __cmft_start_time_seconds, __cmft_end_time_seconds + + if __cmft_start_time_seconds <= 0.0: + # CMFT was not called + return + + if __cmft_end_time_seconds <= 0.0: + # Build was aborted and CMFT didn't finish + __cmft_end_time_seconds = time.time() + + cmft_duration_seconds = __cmft_end_time_seconds - __cmft_start_time_seconds + + # We could also check here if the user already disabled OpenCL and + # then don't show the warning, but this might trick users into + # thinking they have fixed their issue even in the case that the + # slow runtime isn't caused by using OpenCL + if cmft_duration_seconds > 20: + log.warn( + "Generating the radiance map with CMFT took an unusual amount" + f" of time ({cmft_duration_seconds:.2f} s). If the issue persists," + " try disabling \"CMFT: Use OpenCL\" in the Armory add-on preferences." + " For more information see https://github.com/armory3d/armory/issues/2760." + ) + + __cmft_start_time_seconds = 0.0 + __cmft_end_time_seconds = 0.0 diff --git a/blender/data/arm_data.blend b/blender/data/arm_data.blend new file mode 100644 index 0000000000000000000000000000000000000000..70634fe10793ecd8a5fcddcb976b2b4eba93be12 GIT binary patch literal 96980 zcmeFY2UOEp)Gs>fD5I#1g`$*BNSp zl+XhN5&%iteR6zCIviL&C)$jqdIJZQuH(f%VT4VO2La|Mj8@w)>BqFPknn z<%Ev-{Z_C8NG&b8{X_7?rP+kvcl`C+Xqs_N!FZ_mjz_2NSFm3d;e3ExpUVadeF6!? zmSh^?GIL09=*Ah!p4u#NjuoRTOK!!0&h$uUA6QlPgyx%Z^4xTgeYlyrLZkD$OX8

g))kcl|kK5x2vH+^W{Zo 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 j0Q=BE#%)&_ugbcazui-&Xkh~V67gLPov= literal 0 HcmV?d00001 diff --git a/blender/data/haxelogic.py b/blender/data/haxelogic.py new file mode 100644 index 0000000000..c2bcc745cb --- /dev/null +++ b/blender/data/haxelogic.py @@ -0,0 +1,104 @@ +# Convert Python logic node definition to Haxe + +import json +import glob +import sys + +def socket_type(s): + if s == 'ArmNodeSocketAction': + return 'ACTION' + elif s == 'ArmNodeSocketObject': + return 'OBJECT' + elif s == 'ArmNodeSocketAnimAction': + return 'ANIMACTION' + elif s == 'ArmNodeSocketArray': + return 'ARRAY' + elif s == 'NodeSocketShader': + return 'SHADER' + elif s == 'NodeSocketInt': + return 'INTEGER' + elif s == 'NodeSocketFloat': + return 'VALUE' + elif s == 'NodeSocketString': + return 'STRING' + elif s == 'NodeSocketBool': + return 'BOOL' + elif s == 'NodeSocketVector': + return 'VECTOR' + elif s == 'NodeSocketColor': + return 'RGBA' + else: + return s + +# path = '/Users/lubos/Downloads/Armory/armsdk/armory/blender/arm/logicnode' +path = sys.argv[1] +modules = glob.glob(path + "/*.py") +out = {} +out['categories'] = [] + +for m in modules: + if m == '__init__.py': + continue + if m == 'arm_nodes.py': + continue + with open(m) as f: + n = {} + n['inputs'] = [] + n['outputs'] = [] + n['buttons'] = [] + but = None + lines = f.read().splitlines() + for l in lines: + l = l.strip() + if l == '' or l == '],': + continue + + # if l.startswith('property'): + if 'EnumProperty' in l: # TODO: enum only for now + but = {} + but['name'] = 'property' + l.split(' = ', 1)[0][-1] + but['type'] = 'ENUM' + but['default_value'] = 0 + but['data'] = [] + n['buttons'].append(but) + continue + elif but != None: + if l.endswith(')'): + but = None + continue + ar = l.split("'") + but['data'].append(ar[1]) + + if l.startswith('bl_idname'): + ar = l.split("'") + n['type'] = ar[1][2:] + if l.startswith('bl_label'): + ar = l.split("'") + n['name'] = ar[1] + if l.startswith('self.inputs.new('): + ar = l.split("'") + soc = {} + soc['type'] = socket_type(ar[1]) + soc['name'] = ar[3] + n['inputs'].append(soc) + if l.startswith('self.outputs.new('): + ar = l.split("'") + soc = {} + soc['type'] = socket_type(ar[1]) + soc['name'] = ar[3] + n['outputs'].append(soc) + if l.startswith('add_node('): + ar = l.split("'") + cat = None + for c in out['categories']: + if c['name'] == ar[1]: + cat = c + break + if cat == None: + cat = {} + cat['name'] = ar[1] + cat['nodes'] = [] + out['categories'].append(cat) + cat['nodes'].append(n) + +print(json.dumps(out)) diff --git a/blender/data/skydome.blend b/blender/data/skydome.blend new file mode 100644 index 0000000000000000000000000000000000000000..597462bee325be0e185ba71816aa078731d97210 GIT binary patch literal 107524 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[KD3@Z5jZy3;OW2a;g~;S{6=mMu^%m=Hauykl+^HE z?_%wK_^XigH@R1q#WkgaI@8spAI6K-l1s-mrUt#GPWhRx_L3(3?YH8Zq37iG7*<)* z-Tcu`yrX%*DGGI&pDQvHFEd#VMC~gVH|mU_Mi`clsFhEr;TaYt**|Be(oogr-4EbzeS3&^W7BO;up`4wJ12 zd&^B#*HO?xpFHu;e4P9%+#2Nc=!(s=qwXE%&_cOGC_$bK9s9TG^jzX$=y6bc8<8T< zaJPr&M^v>uOMeg5kJoYXaogEDWyKzr<_)D6&r9bJ9EUj!H79<2&r>M@U49!wT0G^y zzw(Yf*$S0$n@+V@a7<4z%HApT&J;z?i7XFkPF}yKUI|fwj72t2&I9#2vf&b*VIuZ_ z0>$=tUz)CRSPiu2S3dk0k0*x>2>{*Bs51ecwc4Ai7e2IbsM#DVh#J;g& z&1LuSxXvtv-SUI>8;wJV?s`LkKf{+bAU+K((;qLiF52W%+TR6t83hL8$`2aPkDP4D z{}?qFFdoeK#G*A4e=I_B(95Vn2OzZU`Iv0@D;eKQe1M|c&+WzsIw|Mv5~-Jh@zeKm zuq7!i^m2#Eo!WJ6Ypd1We}ZELcI6;D=ca=GO(iP?>#g&y@W~UH=IXXP`)S$R*5}vH zHjYzJ0xh@h=KJDUcesJzC#Fha^5Xf32C^oYpi^iS9-)f7%nm%>KAFbF$o-M0gGoh=5|` z1)_SQ1tUNAYQDDq{rE7P$uPWk!5N>}L$5|(g1oU`$$dm{8B-h^m(yZ!S<3ZI*>2S* z`u*H37}GQz{|s@$8NHwn2FqRAyA029%{R!9Z^sgZZ_)BLwvPrqS|7a`-{3L*i8lp} Py8uL9!)^qf(vtmOhqxDQ literal 0 HcmV?d00001 diff --git a/blender/start.py b/blender/start.py new file mode 100644 index 0000000000..7780addab3 --- /dev/null +++ b/blender/start.py @@ -0,0 +1,98 @@ +import time + +import arm +import arm.log +import arm.nodes_logic +import arm.nodes_material +import arm.props_traits_props +import arm.props_traits +import arm.props_lod +import arm.props_tilesheet +import arm.props_exporter +import arm.props_bake +import arm.props_renderpath +import arm.props_properties +import arm.props_collision_filter_mask +import arm.props +import arm.props_ui +import arm.handlers +import arm.utils +import arm.keymap + +reload_started = 0 + +if arm.is_reload(__name__): + arm.log.debug('Reloading Armory SDK...') + reload_started = time.time() + + # Clear the module cache + import importlib + arm = importlib.reload(arm) # type: ignore + + arm.nodes_logic = arm.reload_module(arm.nodes_logic) + arm.nodes_material = arm.reload_module(arm.nodes_material) + arm.props_traits_props = arm.reload_module(arm.props_traits_props) + arm.props_traits = arm.reload_module(arm.props_traits) + arm.props_lod = arm.reload_module(arm.props_lod) + arm.props_tilesheet = arm.reload_module(arm.props_tilesheet) + arm.props_exporter = arm.reload_module(arm.props_exporter) + arm.props_bake = arm.reload_module(arm.props_bake) + arm.props_renderpath = arm.reload_module(arm.props_renderpath) + arm.props_properties = arm.reload_module(arm.props_properties) + arm.props_collision_filter_mask = arm.reload_module(arm.props_collision_filter_mask) + arm.props = arm.reload_module(arm.props) + arm.props_ui = arm.reload_module(arm.props_ui) + arm.handlers = arm.reload_module(arm.handlers) + arm.utils = arm.reload_module(arm.utils) + arm.keymap = arm.reload_module(arm.keymap) +else: + arm.enable_reload(__name__) + +registered = False + + +def register(local_sdk=False): + global registered + registered = True + arm.utils.register(local_sdk=local_sdk) + arm.props_traits_props.register() + arm.props_traits.register() + arm.props_lod.register() + arm.props_tilesheet.register() + arm.props_exporter.register() + arm.props_bake.register() + arm.props_renderpath.register() + arm.props_properties.register() + arm.props.register() + arm.props_ui.register() + arm.nodes_logic.register() + arm.nodes_material.register() + arm.keymap.register() + arm.handlers.register() + arm.props_collision_filter_mask.register() + + arm.handlers.post_register() + + if reload_started != 0: + arm.log.debug(f'Armory SDK: Reloading finished in {time.time() - reload_started:.3f}s') + + +def unregister(): + global registered + registered = False + arm.keymap.unregister() + arm.utils.unregister() + arm.nodes_material.unregister() + arm.nodes_logic.unregister() + arm.handlers.unregister() + arm.props_ui.unregister() + arm.props.unregister() + arm.props_traits_props.unregister() + arm.props_traits.unregister() + arm.props_lod.unregister() + arm.props_tilesheet.unregister() + arm.props_exporter.unregister() + arm.props_bake.unregister() + arm.props_renderpath.unregister() + arm.props_properties.unregister() + arm.props_collision_filter_mask.unregister() diff --git a/changes.md b/changes.md new file mode 100644 index 0000000000..45a9923f4b --- /dev/null +++ b/changes.md @@ -0,0 +1,6 @@ +* 2019-06-30: Return value of `PhysicsWorld.rayCast()` changed, see https://github.com/armory3d/armory/commit/dfb7609a28cebf3a520e6a25a8563d01c32f2b01. +* 2019-04-06: Use voxelao instead of voxelgi, gi will be reworked into raytracing. +* 2019-01-13: If you are using Armory Updater, get Armory 0.6beta from https://armory.itch.io/armory3d first (or clone the sdk from https://github.com/armory3d/armsdk). +* 2018-08-28: `LampObject` and `LampData` is now `LightObject` and `LightData` +* 2018-06-01: 'Not Equal' has been removed from the Gate logic node. Use 'Equal' and 'False' output socket instead. +* 2017-11-20: Use `armory.trait.physics.*` instead of `armory.trait.internal.*` to access physics traits. diff --git a/checkstyle.json b/checkstyle.json new file mode 100644 index 0000000000..d3e91df2fb --- /dev/null +++ b/checkstyle.json @@ -0,0 +1,253 @@ +{ + "defaultSeverity": "INFO", + "checks": [ + { + "type": "CodeSimilarity" + }, + { + "type": "DefaultComesLast" + }, + { + "type": "DocCommentStyle" + }, + { + "type": "ERegLiteral" + }, + { + "props": { + "tokens": [ + "CLASS_DEF", + "ENUM_DEF", + "ABSTRACT_DEF", + "TYPEDEF_DEF", + "INTERFACE_DEF", + "OBJECT_DECL", + "FUNCTION", + "FOR", + "IF", + "WHILE", + "SWITCH", + "TRY", + "CATCH" + ], + "option": "empty" + }, + "type": "EmptyBlock" + }, + { + "props": { + "max": 1 + }, + "type": "EmptyLines" + }, + { + "props": { + "option": "lowerCase" + }, + "type": "HexadecimalLiteral" + }, + { + "type": "InnerAssignment" + }, + { + "props": { + "modifiers": [ + "MACRO", + "OVERRIDE", + "PUBLIC_PRIVATE", + "STATIC", + "INLINE", + "DYNAMIC" + ] + }, + "type": "ModifierOrder" + }, + { + "type": "MultipleVariableDeclarations" + }, + { + "props": { + "allowSingleLineStatement": true, + "tokens": [ + "FOR", + "IF", + "ELSE_IF", + "WHILE", + "DO_WHILE" + ] + }, + "type": "NeedBraces" + }, + { + "props": { + "assignOpPolicy": "around", + "unaryOpPolicy": "none", + "ternaryOpPolicy": "around", + "arithmeticOpPolicy": "around", + "compareOpPolicy": "around", + "bitwiseOpPolicy": "around", + "boolOpPolicy": "around", + "intervalOpPolicy": "none", + "arrowPolicy": "none", + "oldFunctionTypePolicy": "none", + "newFunctionTypePolicy": "none", + "arrowFunctionPolicy": "around" + }, + "type": "OperatorWhitespace" + }, + { + "props": { + "tokens": [ + "=", + "*", + "/", + "%", + ">", + "<", + ">=", + "<=", + "==", + "!=", + "&", + "|", + "^", + "<<", + ">>", + ">>>", + "+=", + "-=", + "*=", + "/=", + "%=", + "<<=", + ">>=", + ">>>=", + "|=", + "&=", + "^=", + "...", + "=>", + "++", + "--", + "+", + "-", + "&&", + "||" + ], + "option": "eol" + }, + "type": "OperatorWrap" + }, + { + "type": "RedundantModifier" + }, + { + "type": "RedundantAllowMeta" + }, + { + "type": "RedundantAccessMeta" + }, + { + "props": { + "allowEmptyReturn": true, + "enforceReturnType": false + }, + "type": "Return" + }, + { + "props": { + "dotPolicy": "none", + "commaPolicy": "after", + "semicolonPolicy": "after" + }, + "type": "SeparatorWhitespace" + }, + { + "props": { + "tokens": [ + "," + ], + "option": "eol" + }, + "type": "SeparatorWrap" + }, + { + "props": { + "spaceIfCondition": "should", + "spaceAroundBinop": true, + "spaceForLoop": "should", + "ignoreRangeOperator": true, + "spaceWhileLoop": "should", + "spaceCatch": "should", + "spaceSwitchCase": "should", + "noSpaceAroundUnop": true + }, + "type": "Spacing" + }, + { + "props": { + "allowException": true, + "policy": "doubleAndInterpolation" + }, + "type": "StringLiteral" + }, + { + "type": "TrailingWhitespace" + }, + { + "type": "UnusedImport" + }, + { + "type": "UnusedLocalVar" + }, + { + "props": { + "tokens": [ + ",", + ";", + ":" + ] + }, + "type": "WhitespaceAfter" + }, + { + "props": { + "tokens": [ + "=", + "+", + "-", + "*", + "/", + "%", + ">", + "<", + ">=", + "<=", + "==", + "!=", + "&", + "|", + "^", + "&&", + "||", + "<<", + ">>", + ">>>", + "+=", + "-=", + "*=", + "/=", + "%=", + "<<=", + ">>=", + ">>>=", + "|=", + "&=", + "^=", + "=>" + ] + }, + "type": "WhitespaceAround" + } + ] +} From 4a4145ba96fcae5fdede199104e713ed74bb7377 Mon Sep 17 00:00:00 2001 From: e2002e Date: Tue, 16 May 2023 20:51:11 +0200 Subject: [PATCH 002/175] fix comparing in to string --- blender/arm/material/make_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 70e3fe0ce7..006b81a729 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -548,7 +548,7 @@ def make_forward(con_mesh, rpasses): frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') - if mrt > 2 in wrd.world_defs: + if mrt > 2: if 'refraction' in rpasses: frag.write('fragColor[{0}] = vec4(rior, opacity, 0.0, 0.0);'.format(mrt-1)) else: From f27d007950cfb512f713bc43f5cca4d054df92ab Mon Sep 17 00:00:00 2001 From: e2002e Date: Tue, 16 May 2023 21:38:30 +0200 Subject: [PATCH 003/175] corrected raySteps --- Shaders/ssr_pass/ssr_pass.frag.glsl | 4 ++-- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index f8a75a7909..75a4a91b18 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -25,7 +25,7 @@ vec3 hitCoord; float depth; const int numBinarySearchSteps = 7; -#define maxSteps (1.0 / ssrRayStep) +const int maxSteps = 18; vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); @@ -38,7 +38,7 @@ vec2 getProjectedCoord(const vec3 hit) { } float getDeltaDepth(const vec3 hit) { - float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 7420061308..d587c01c9c 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -30,7 +30,7 @@ vec3 viewPos; const int numBinarySearchSteps = 7; -#define maxSteps (1.0 / ss_refractionRayStep) +const int maxSteps = 18; vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); From 7f7b34f8b6221b26009d458c03ab2a50ca919797 Mon Sep 17 00:00:00 2001 From: e2002e Date: Sat, 20 May 2023 19:05:00 +0200 Subject: [PATCH 004/175] rm useless lines --- Sources/armory/renderpath/RenderPathDeferred.hx | 1 - Sources/armory/renderpath/RenderPathForward.hx | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 5386bf4305..b9a21aa287 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -70,7 +70,6 @@ class RenderPathDeferred { } #end - path.createDepthBuffer("main", "DEPTH24"); var t = new RenderTargetRaw(); diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index d0f7f12861..dbe02c0273 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -266,16 +266,6 @@ class RenderPathForward { path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.loadShader("shader_datas/copy_pass/copy_pass"); - //holds rior and opacity - var t = new RenderTargetRaw(); - t.name = "gbuffer_refraction"; - t.width = 0; - t.height = 0; - t.displayp = Inc.getDisplayp(); - t.format = "RGBA64"; - t.scale = Inc.getSuperSampling(); - path.createRenderTarget(t); - //holds colors before refractive meshes are drawn var t = new RenderTargetRaw(); t.name = "refr"; @@ -452,7 +442,6 @@ class RenderPathForward { path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("lbuffer1", "gbuffer0"); - path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); } } From 9b5e2a9fd7d91911f0fc67d4c1eb378ebe54ce05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 20 May 2023 22:39:57 +0200 Subject: [PATCH 005/175] Fix potential division by zero in GGX normal distribution function --- Shaders/std/brdf.glsl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Shaders/std/brdf.glsl b/Shaders/std/brdf.glsl index d6f2def2da..cce227e893 100644 --- a/Shaders/std/brdf.glsl +++ b/Shaders/std/brdf.glsl @@ -29,7 +29,8 @@ float g2_approx(const float NdotL, const float NdotV, const float alpha) float d_ggx(const float nh, const float a) { float a2 = a * a; - float denom = pow(nh * nh * (a2 - 1.0) + 1.0, 2.0); + float denom = nh * nh * (a2 - 1.0) + 1.0; + denom = max(denom * denom, 0.00006103515625 /* 2^-14 = smallest possible half float value, prevent div by zero */); return a2 * (1.0 / 3.1415926535) / denom; } From 70183c89ea625d210c20be94d5208f2a4698ede1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sun, 21 May 2023 23:22:56 +0200 Subject: [PATCH 006/175] Implement Clamp option for Map Range node --- .../material/cycles_nodes/nodes_converter.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/blender/arm/material/cycles_nodes/nodes_converter.py b/blender/arm/material/cycles_nodes/nodes_converter.py index 044834ef5f..8c350c5caa 100644 --- a/blender/arm/material/cycles_nodes/nodes_converter.py +++ b/blender/arm/material/cycles_nodes/nodes_converter.py @@ -32,25 +32,30 @@ def parse_maprange(node: bpy.types.ShaderNodeMapRange, out_socket: bpy.types.Nod toMax = c.parse_value_input(node.inputs[4]) if interp == "LINEAR": - state.curshader.add_function(c_functions.str_map_range_linear) - return f'map_range_linear({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_linear({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' elif interp == "STEPPED": - steps = float(c.parse_value_input(node.inputs[5])) state.curshader.add_function(c_functions.str_map_range_stepped) - return f'map_range_stepped({value}, {fromMin}, {fromMax}, {toMin}, {toMax}, {steps})' + out = f'map_range_stepped({value}, {fromMin}, {fromMax}, {toMin}, {toMax}, {steps})' elif interp == "SMOOTHSTEP": - state.curshader.add_function(c_functions.str_map_range_smoothstep) - return f'map_range_smoothstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_smoothstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' elif interp == "SMOOTHERSTEP": - state.curshader.add_function(c_functions.str_map_range_smootherstep) - return f'map_range_smootherstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_smootherstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + + else: + log.warn(f'Interpolation mode {interp} not supported for Map Range node') + return '0.0' + + if node.clamp: + out = f'clamp({out}, {toMin}, {toMax})' + + return out def parse_blackbody(node: bpy.types.ShaderNodeBlackbody, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: From 6cf40b159f5437d414ea6fe0241b05d12acc469d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sun, 21 May 2023 23:27:42 +0200 Subject: [PATCH 007/175] Fix compilation of Mix RGB node with Soft Light blending --- blender/arm/material/cycles_nodes/nodes_color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_nodes/nodes_color.py b/blender/arm/material/cycles_nodes/nodes_color.py index bb29506cb1..b7f3ba9234 100644 --- a/blender/arm/material/cycles_nodes/nodes_color.py +++ b/blender/arm/material/cycles_nodes/nodes_color.py @@ -101,7 +101,7 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc elif blend == 'COLOR': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'SOFT_LIGHT': - out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))));'.format(col1, col2, fac) + out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))))'.format(col1, col2, fac) elif blend == 'LINEAR_LIGHT': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix # out_col = '({0} + {2} * (2.0 * ({1} - vec3(0.5))))'.format(col1, col2, fac_var) From d46b2a5130664a3416c5336f7c01390fbea8e226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 1 Jun 2023 00:18:59 +0200 Subject: [PATCH 008/175] Fix compilation of draw nodes if "Zui" setting is set to "Auto" --- Sources/armory/logicnode/DrawArcNode.hx | 4 ++++ Sources/armory/logicnode/DrawCircleNode.hx | 4 ++++ Sources/armory/logicnode/DrawCurveNode.hx | 4 ++++ Sources/armory/logicnode/DrawEllipseNode.hx | 4 ++++ Sources/armory/logicnode/DrawPolygonNode.hx | 4 ++++ Sources/armory/logicnode/DrawTextAreaStringNode.hx | 6 ++++-- blender/arm/make.py | 7 +++++-- 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Sources/armory/logicnode/DrawArcNode.hx b/Sources/armory/logicnode/DrawArcNode.hx index 9ac22b79bd..29620aa346 100644 --- a/Sources/armory/logicnode/DrawArcNode.hx +++ b/Sources/armory/logicnode/DrawArcNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawArcNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawArcNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawArcNode"); final colorVec = inputs[1].get(); @@ -30,6 +33,7 @@ class DrawArcNode extends LogicNode { } else { RenderToTexture.g.drawArc(cx, cy, radius, sAngle, eAngle, inputs[3].get(), ccw, segments); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawCircleNode.hx b/Sources/armory/logicnode/DrawCircleNode.hx index 822efd4caa..0501e5e7e2 100644 --- a/Sources/armory/logicnode/DrawCircleNode.hx +++ b/Sources/armory/logicnode/DrawCircleNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawCircleNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawCircleNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawCircleNode"); final colorVec = inputs[1].get(); @@ -28,6 +31,7 @@ class DrawCircleNode extends LogicNode { else { RenderToTexture.g.drawCircle(cx, cy, radius, inputs[3].get(), segments); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawCurveNode.hx b/Sources/armory/logicnode/DrawCurveNode.hx index 213bfd1914..df3176de59 100644 --- a/Sources/armory/logicnode/DrawCurveNode.hx +++ b/Sources/armory/logicnode/DrawCurveNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawCurveNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawCurveNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawCurveNode"); final colorVec = inputs[1].get(); @@ -22,6 +25,7 @@ class DrawCurveNode extends LogicNode { [inputs[5].get(), inputs[7].get(), inputs[9].get(), inputs[11].get()], inputs[3].get(), inputs[2].get() ); + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawEllipseNode.hx b/Sources/armory/logicnode/DrawEllipseNode.hx index 85adf67e23..7b37b79ab8 100644 --- a/Sources/armory/logicnode/DrawEllipseNode.hx +++ b/Sources/armory/logicnode/DrawEllipseNode.hx @@ -4,7 +4,9 @@ import iron.math.Vec4; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawEllipseNode extends LogicNode { @@ -13,6 +15,7 @@ class DrawEllipseNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawEllipseNode"); final colorVec: Vec4 = inputs[1].get(); @@ -41,6 +44,7 @@ class DrawEllipseNode extends LogicNode { RenderToTexture.g.rotate(-angle, cx, cy); RenderToTexture.g.scale(1.0, scaleInv); + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawPolygonNode.hx b/Sources/armory/logicnode/DrawPolygonNode.hx index da2a3cf387..a84a904ca0 100644 --- a/Sources/armory/logicnode/DrawPolygonNode.hx +++ b/Sources/armory/logicnode/DrawPolygonNode.hx @@ -4,7 +4,9 @@ import kha.Color; import kha.math.Vector2; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawPolygonNode extends LogicNode { static inline var numStaticInputs = 6; @@ -16,6 +18,7 @@ class DrawPolygonNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawPolygonNode"); if (vertices == null) { @@ -43,6 +46,7 @@ class DrawPolygonNode extends LogicNode { } else { RenderToTexture.g.drawPolygon(inputs[4].get(), inputs[5].get(), vertices, inputs[3].get()); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawTextAreaStringNode.hx b/Sources/armory/logicnode/DrawTextAreaStringNode.hx index 4a524df5e3..dd9a812623 100644 --- a/Sources/armory/logicnode/DrawTextAreaStringNode.hx +++ b/Sources/armory/logicnode/DrawTextAreaStringNode.hx @@ -7,10 +7,10 @@ import armory.renderpath.RenderToTexture; import kha.graphics2.VerTextAlignment; import kha.graphics2.HorTextAlignment; -using zui.GraphicsExtension; - #if arm_ui import armory.ui.Canvas; + +using zui.GraphicsExtension; #end class DrawTextAreaStringNode extends LogicNode { @@ -29,6 +29,7 @@ class DrawTextAreaStringNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawTextAreaStringNode"); var string:String = Std.string(inputs[1].get()); @@ -124,6 +125,7 @@ class DrawTextAreaStringNode extends LogicNode { ++index; } + #end runOutput(0); } diff --git a/blender/arm/make.py b/blender/arm/make.py index 626b6774d9..ad7efe1dba 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -208,8 +208,11 @@ def export_data(fp, sdk_path): if network_found == False: export_network = False - if wrd.arm_ui == 'Enabled': - export_ui = True + # Ugly workaround: some logic nodes require Zui code even if no UI is used, + # for now enable UI export unless explicitly disabled. + export_ui = True + if wrd.arm_ui == 'Disabled': + export_ui = False if wrd.arm_network == 'Enabled': export_network = True From 415fd5214c89f217eb56509ec768afc78b34a7a4 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Fri, 2 Jun 2023 10:16:09 +0200 Subject: [PATCH 009/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 43f52a0206..9a6786a33e 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.5' +arm_version = '2023.6' arm_commit = '$Id$' def get_project_html5_copy(self): From 14f427cc7b6531c4583fc85a9336db9df2c3a160 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 16 Jun 2023 17:49:48 +0200 Subject: [PATCH 010/175] Do not force objects to be visible at spawn --- Sources/armory/logicnode/SpawnObjectByNameNode.hx | 1 - Sources/armory/logicnode/SpawnObjectNode.hx | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/armory/logicnode/SpawnObjectByNameNode.hx b/Sources/armory/logicnode/SpawnObjectByNameNode.hx index 5ecb1bc71a..8a20ece8d2 100644 --- a/Sources/armory/logicnode/SpawnObjectByNameNode.hx +++ b/Sources/armory/logicnode/SpawnObjectByNameNode.hx @@ -59,7 +59,6 @@ class SpawnObjectByNameNode extends LogicNode { } #end } - object.visible = true; runOutput(0); }, spawnChildren, rawScene); diff --git a/Sources/armory/logicnode/SpawnObjectNode.hx b/Sources/armory/logicnode/SpawnObjectNode.hx index e6f16aac7f..5529bc8cea 100644 --- a/Sources/armory/logicnode/SpawnObjectNode.hx +++ b/Sources/armory/logicnode/SpawnObjectNode.hx @@ -37,7 +37,6 @@ class SpawnObjectNode extends LogicNode { } #end } - object.visible = true; runOutput(0); }, spawnChildren); } From 4f6669c4f450ca7f54571c904dec84b5fdb5d215 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 16 Jun 2023 17:58:11 +0200 Subject: [PATCH 011/175] Add option to recursivley set children visibility --- Sources/armory/logicnode/SetVisibleNode.hx | 51 ++++++++++++------- .../logicnode/object/LN_set_object_visible.py | 17 ++++++- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/Sources/armory/logicnode/SetVisibleNode.hx b/Sources/armory/logicnode/SetVisibleNode.hx index 2fe99d2493..2abd6b462b 100644 --- a/Sources/armory/logicnode/SetVisibleNode.hx +++ b/Sources/armory/logicnode/SetVisibleNode.hx @@ -14,31 +14,44 @@ class SetVisibleNode extends LogicNode { var object: Object = inputs[1].get(); var visible: Bool = inputs[2].get(); var children: Bool = inputs[3].get(); + var recursive: Bool = inputs[4].get(); if (object == null) return; - var objectChildren: Array = object.children; - switch (property0) { - case "object": - object.visible = visible; - if (children == true) for (child in objectChildren) { - child.visible = visible; - } - - case "mesh": - object.visibleMesh = visible; - if (children == true) for (child in objectChildren) { - child.visibleMesh = visible; - } - - case "shadow": - object.visibleShadow = visible; - if (children == true) for (child in objectChildren) { - child.visibleShadow = visible; + case "object": + object.visible = visible; + case "mesh": + object.visibleMesh = visible; + case "shadow": + object.visibleShadow = visible; } - } + + if (children) setVisisbleRecursive(property0, object, visible, recursive); runOutput(0); } + + function setVisisbleRecursive(property: String, object: Object, visible: Bool, recursive: Bool) { + var objectChildren: Array = object.children; + switch (property) { + case "object": + for (child in objectChildren) { + child.visible = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + + case "mesh": + for (child in objectChildren) { + child.visibleMesh = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + + case "shadow": + for (child in objectChildren) { + child.visibleShadow = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + } + } } diff --git a/blender/arm/logicnode/object/LN_set_object_visible.py b/blender/arm/logicnode/object/LN_set_object_visible.py index 09107e9ec9..b77b86b125 100644 --- a/blender/arm/logicnode/object/LN_set_object_visible.py +++ b/blender/arm/logicnode/object/LN_set_object_visible.py @@ -3,11 +3,19 @@ class SetVisibleNode(ArmLogicTreeNode): """Sets whether the given object is visible. + @input Object: Object whose property to be set. + + @input Visible: Visibility. + + @input Children: Set the visibility of the children too. Visibility is set only to the immidiate children. + + @input Recursive: If enabled, visibility of all the children in the tree is set. Ignored if `Children` is disabled. + @seeNode Get Object Visible""" bl_idname = 'LNSetVisibleNode' bl_label = 'Set Object Visible' arm_section = 'props' - arm_version = 1 + arm_version = 2 property0: HaxeEnumProperty( 'property0', @@ -22,8 +30,15 @@ def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmBoolSocket', 'Visible') self.add_input('ArmBoolSocket', 'Children', default_value=True) + self.add_input('ArmBoolSocket', 'Recursive', default_value=False) self.add_output('ArmNodeSocketAction', 'Out') def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + 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 From ce18be82d0624f8ef97c42a524647da2e5ac1a76 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 16 Jun 2023 18:06:25 +0200 Subject: [PATCH 012/175] fix typo --- blender/arm/logicnode/object/LN_set_object_visible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/object/LN_set_object_visible.py b/blender/arm/logicnode/object/LN_set_object_visible.py index b77b86b125..0bb93e04d0 100644 --- a/blender/arm/logicnode/object/LN_set_object_visible.py +++ b/blender/arm/logicnode/object/LN_set_object_visible.py @@ -7,7 +7,7 @@ class SetVisibleNode(ArmLogicTreeNode): @input Visible: Visibility. - @input Children: Set the visibility of the children too. Visibility is set only to the immidiate children. + @input Children: Set the visibility of the children too. Visibility is set only to the immediate children. @input Recursive: If enabled, visibility of all the children in the tree is set. Ignored if `Children` is disabled. From dcb5f4ed317c81095a09206d24de6758e124dc0d Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 18 Jun 2023 15:40:52 +0200 Subject: [PATCH 013/175] Add new variant for shape keys --- blender/arm/exporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 2035de0df3..b69a648f20 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -588,6 +588,8 @@ def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Mat # Tilesheets elif bobject.arm_tilesheet != '': variant_suffix = '_armtile' + elif arm.utils.export_morph_targets(bobject): + variant_suffix = '_armskey' if variant_suffix == '': continue From 929b53cd8db5ad1a1ba75bf680b79e89f067f1d8 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 18 Jun 2023 15:42:13 +0200 Subject: [PATCH 014/175] Use skin and shapekeys for material batch signature --- blender/arm/material/mat_batch.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/blender/arm/material/mat_batch.py b/blender/arm/material/mat_batch.py index 3d2746c31b..862a189628 100644 --- a/blender/arm/material/mat_batch.py +++ b/blender/arm/material/mat_batch.py @@ -1,12 +1,15 @@ +import bpy import arm import arm.material.cycles as cycles import arm.material.make_shader as make_shader import arm.material.mat_state as mat_state +import arm.utils as arm_utils if arm.is_reload(__name__): cycles = arm.reload_module(cycles) make_shader = arm.reload_module(make_shader) mat_state = arm.reload_module(mat_state) + arm_utils = arm.reload_module(arm_utils) else: arm.enable_reload(__name__) @@ -25,7 +28,7 @@ def traverse_tree(node, sign): sign += 'o' # Unconnected socket return sign -def get_signature(mat): +def get_signature(mat, object: bpy.types.Object): nodes = mat.node_tree.nodes output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') @@ -54,6 +57,8 @@ def get_signature(mat): sign += mat.arm_skip_context if mat.arm_skip_context != '' else '0' sign += '1' if mat.arm_particle_fade else '0' sign += mat.arm_billboard + sign += '_skin' if arm_utils.export_bone_data(object) else '0' + sign += '_morph' if arm_utils.export_morph_targets(object) else '0' return sign def traverse_tree2(node, ar): @@ -105,7 +110,7 @@ def build(materialArray, mat_users, mat_armusers): # Update signatures for mat in materialArray: if mat.signature == '' or not mat.arm_cached: - mat.signature = get_signature(mat) + mat.signature = get_signature(mat, mat_users[mat][0]) # Group signatures if mat.signature in signatureDict: signatureDict[mat.signature].append(mat) From 68b2c039d118be3a33730d6c4a5fce4aa2d14559 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 25 Nov 2022 00:22:42 +0530 Subject: [PATCH 015/175] try fixing casting, add trace statements --- .../armory/trait/physics/bullet/SoftBody.hx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index e45344e16b..7e5eea098a 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -67,6 +67,7 @@ class SoftBody extends Trait { for (ar in ars) { for (j in 0...ar.length) { vals[i] = ar[j]; + trace(ar[j]); i++; } } @@ -101,6 +102,8 @@ class SoftBody extends Trait { v.y *= object.transform.scale.y; v.z *= object.transform.scale.z; v.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); + trace("Position index" + i); + trace(v); positions[i * 3 ] = v.x; positions[i * 3 + 1] = v.y; positions[i * 3 + 2] = v.z; @@ -124,8 +127,36 @@ class SoftBody extends Trait { helpersCreated = true; } + var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(positions.length); + for(i in 0...positions.length){ + positionsVector.set(i, positions.get(i)); + } + + var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vecind.length); + for(i in 0...vecind.length){ + vecindVector.set(i, vecind.get(i)); + } + + /* trace("_______________________________________________"); + trace(worldInfo); + + //var posVector: haxe.ds.Vector = cast positions; + trace(positions); + for(vvv in 0...positions.length) trace(positions.get(vvv)); + trace("posVector"); + for(vvv in 0...positionsVector.length) trace(positionsVector.get(vvv)); + + //var vecindVector: haxe.ds.Vector = cast vecind; + trace(vecind); + for(vvv in 0...vecind.length) trace(vecind.get(vvv)); + trace("vecindVector"); + for(vvv in 0...vecindVector.length) trace(vecindVector.get(vvv)); + + trace(numtri); + trace("_______________________________________________"); */ + #if js - body = helpers.CreateFromTriMesh(worldInfo, cast positions, cast vecind, numtri); + body = helpers.CreateFromTriMesh(worldInfo, positionsVector, vecindVector, numtri); #elseif cpp untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); #end @@ -195,6 +226,8 @@ class SoftBody extends Trait { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); + trace("nodepos " + i); + trace("( " + nodePos.x() + ", " + nodePos.y() + ", " + nodePos.z() + " )"); #elseif cpp var nodePos = node.m_x; #end From 4422a8edcdf934c554e1a58699ec03c80ad1632b Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 7 Dec 2022 12:56:52 +0530 Subject: [PATCH 016/175] use ByteArray for vertices --- .../armory/trait/physics/bullet/SoftBody.hx | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 7e5eea098a..12bc9bda50 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -1,5 +1,6 @@ package armory.trait.physics.bullet; +import kha.arrays.ByteArray; #if arm_bullet import iron.math.Vec4; @@ -49,8 +50,8 @@ class SoftBody extends Trait { }); } - function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): kha.arrays.Float32Array { - var vals = new kha.arrays.Float32Array(Std.int(ar.length / 4) * 3); + function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): haxe.ds.Vector { + var vals = new haxe.ds.Vector(Std.int(ar.length / 4) * 3); for (i in 0...Std.int(vals.length / 3)) { vals[i * 3 ] = (ar[i * 4 ] / 32767) * scalePos; vals[i * 3 + 1] = (ar[i * 4 + 1] / 32767) * scalePos; @@ -59,10 +60,10 @@ class SoftBody extends Trait { return vals; } - function fromU32(ars: Array): kha.arrays.Uint32Array { + function fromU32(ars: Array): haxe.ds.Vector { var len = 0; for (ar in ars) len += ar.length; - var vals = new kha.arrays.Uint32Array(len); + var vals = new haxe.ds.Vector(len); var i = 0; for (ar in ars) { for (j in 0...ar.length) { @@ -137,26 +138,8 @@ class SoftBody extends Trait { vecindVector.set(i, vecind.get(i)); } - /* trace("_______________________________________________"); - trace(worldInfo); - - //var posVector: haxe.ds.Vector = cast positions; - trace(positions); - for(vvv in 0...positions.length) trace(positions.get(vvv)); - trace("posVector"); - for(vvv in 0...positionsVector.length) trace(positionsVector.get(vvv)); - - //var vecindVector: haxe.ds.Vector = cast vecind; - trace(vecind); - for(vvv in 0...vecind.length) trace(vecind.get(vvv)); - trace("vecindVector"); - for(vvv in 0...vecindVector.length) trace(vecindVector.get(vvv)); - - trace(numtri); - trace("_______________________________________________"); */ - #if js - body = helpers.CreateFromTriMesh(worldInfo, positionsVector, vecindVector, numtri); + body = helpers.CreateFromTriMesh(worldInfo, positions, vecind, numtri); #elseif cpp untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); #end @@ -203,17 +186,29 @@ class SoftBody extends Trait { function update() { var mo = cast(object, MeshObject); var geom = mo.data.geom; - + trace(";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; UPDATE"); #if arm_deinterleaved + trace("deinterleieved"); var v = geom.vertexBuffers[0].lock(); var n = geom.vertexBuffers[1].lock(); #else - var v = geom.vertexBuffer.lock(); + var v:ByteArray = geom.vertexBuffer.lock(); var vbPos = geom.vertexBufferMap.get("pos"); var v2 = vbPos != null ? vbPos.lock() : null; // For shadows var l = geom.structLength; #end - var numVerts = Std.int(v.length / l); + var numVerts = geom.getVerticesCount(); + trace("num verts = " + numVerts); + for(i in 0...numVerts * l) { + trace("point " + i + " = " + v.getInt16( i * 2 )); + } + trace("num tris = " + geom.numTris); + trace("structLength = " + l); + trace("total len =" + v.byteLength); + for(k in geom.vertexBufferMap.keys()){ + trace("key = " + k); + trace(geom.vertexBufferMap.get(k)); + } #if js var nodes = body.get_m_nodes(); @@ -241,6 +236,7 @@ class SoftBody extends Trait { for (i in 0...numVerts) { var node = nodes.at(i); + var vertIndex = i * l * 2; #if js var nodePos = node.get_m_x(); var nodeNor = node.get_m_n(); @@ -256,17 +252,18 @@ class SoftBody extends Trait { n.set(i * 2 + 1, Std.int(nodeNor.y() * 32767)); v.set(i * 4 + 3, Std.int(nodeNor.z() * 32767)); #else - v.set(i * l , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.set(i * l + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.set(i * l + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + //trace(i); + v.setInt16(vertIndex , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); if (vbPos != null) { - v2.set(i * 4 , v.get(i * l )); - v2.set(i * 4 + 1, v.get(i * l + 1)); - v2.set(i * 4 + 2, v.get(i * l + 2)); + v2.setInt16(i * 8 , v.getInt16(vertIndex )); + v2.setInt16(i * 8 + 2, v.getInt16(vertIndex + 2)); + v2.setInt16(i * 8 + 4, v.getInt16(vertIndex + 4)); } - v.set(i * l + 3, Std.int(nodeNor.z() * 32767)); - v.set(i * l + 4, Std.int(nodeNor.x() * 32767)); - v.set(i * l + 5, Std.int(nodeNor.y() * 32767)); + v.setInt16(vertIndex + 6, Std.int(nodeNor.z() * 32767)); + v.setInt16(vertIndex + 8, Std.int(nodeNor.x() * 32767)); + v.setInt16(vertIndex + 10, Std.int(nodeNor.y() * 32767)); #end } // for (i in 0...Std.int(geom.indices[0].length / 3)) { From ebcb2d8f8eec40357a7704c425c661b96c1cc768 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 7 Dec 2022 12:57:04 +0530 Subject: [PATCH 017/175] add debug print in export --- blender/arm/exporter_opt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index 3e762062fe..af2e98ceea 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -118,7 +118,9 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec # exportMesh.calc_loop_triangles() vcol0 = self.get_nth_vertex_colors(export_mesh, 0) vert_list = {Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}.keys() + print("EXPORT") num_verts = len(vert_list) + print(num_verts) num_uv_layers = len(export_mesh.uv_layers) # Check if shape keys were exported has_morph_target = self.get_shape_keys(bobject.data) From c121637c498e6283540cd1f5d9c81af0d8830f0a Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 16 Dec 2022 19:43:56 +0530 Subject: [PATCH 018/175] Add debug print in exporter_opt --- blender/arm/exporter_opt.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index af2e98ceea..41ce7215a0 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -30,11 +30,22 @@ def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Option self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] self.loop_indices = [loop_idx] self.index = 0 + print("******************************************************************") + print(self.vertex_index) + print(loop_idx) + print(self.co) + print(self.normal) + print(self.uvs) + print(self.col) + print(self.loop_indices) def __hash__(self): + print("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + print(hash((self.co, self.normal, self.uvs))) return hash((self.co, self.normal, self.uvs)) def __eq__(self, other): + print("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") eq = ( (self.co == other.co) and (self.normal == other.normal) and @@ -42,6 +53,7 @@ def __eq__(self, other): (self.col == other.col) ) if eq: + print("EQUAL") indices = self.loop_indices + other.loop_indices self.loop_indices = indices other.loop_indices = indices @@ -119,6 +131,8 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec vcol0 = self.get_nth_vertex_colors(export_mesh, 0) vert_list = {Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}.keys() print("EXPORT") + print({Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}) + print(vert_list) num_verts = len(vert_list) print(num_verts) num_uv_layers = len(export_mesh.uv_layers) From 20689b5de75f54fcad317a10b66eced0840c6228 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 15:38:36 +0200 Subject: [PATCH 019/175] export vertex_map index buffer attribute --- blender/arm/exporter.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b69a648f20..0e90698d3e 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -1579,6 +1579,7 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec if tris == 0: # No face assigned continue prim = np.empty(tris * 3, dtype=' 1: for i in range(len(mats)): # Multi-mat mesh if mats[i] == mats[index]: # Default material for empty slots From aa36430dce557659988f39e1648c4a8096743524 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 15:39:08 +0200 Subject: [PATCH 020/175] remove debug print statements --- blender/arm/exporter_opt.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index 41ce7215a0..3e762062fe 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -30,22 +30,11 @@ def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Option self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] self.loop_indices = [loop_idx] self.index = 0 - print("******************************************************************") - print(self.vertex_index) - print(loop_idx) - print(self.co) - print(self.normal) - print(self.uvs) - print(self.col) - print(self.loop_indices) def __hash__(self): - print("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - print(hash((self.co, self.normal, self.uvs))) return hash((self.co, self.normal, self.uvs)) def __eq__(self, other): - print("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") eq = ( (self.co == other.co) and (self.normal == other.normal) and @@ -53,7 +42,6 @@ def __eq__(self, other): (self.col == other.col) ) if eq: - print("EQUAL") indices = self.loop_indices + other.loop_indices self.loop_indices = indices other.loop_indices = indices @@ -130,11 +118,7 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec # exportMesh.calc_loop_triangles() vcol0 = self.get_nth_vertex_colors(export_mesh, 0) vert_list = {Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}.keys() - print("EXPORT") - print({Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}) - print(vert_list) num_verts = len(vert_list) - print(num_verts) num_uv_layers = len(export_mesh.uv_layers) # Check if shape keys were exported has_morph_target = self.get_shape_keys(bobject.data) From d3f2e379cb167edef2f34678bc3ffb3f2e5739c8 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 15:40:24 +0200 Subject: [PATCH 021/175] Export vertex_map in optimized export. Add comments. --- blender/arm/exporter_opt.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index 3e762062fe..88f4ebd7a0 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -248,35 +248,57 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec cdata[i3 + 2] = v.col[2] # Indices + # Create dict for every material slot prims = {ma.name if ma else '': [] for ma in export_mesh.materials} + v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} if not prims: + # No materials prims = {'': []} + v_maps = {'': []} + # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list vert_dict = {i : v for v in vert_list for i in v.loop_indices} + # For each polygon in a mesh for poly in export_mesh.polygons: + # Index of the first loop of this polygon first = poly.loop_start + # No materials assigned if len(export_mesh.materials) == 0: + # Get prim prim = prims[''] + v_map = v_maps[''] else: + # First material mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] + # Get prim for this material prim = prims[mat.name if mat else ''] + v_map = v_maps[mat.name if mat else ''] + # List of indices for each loop_index belonging to this polygon indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] + # If 3 loops per polygon (Triangle?) if poly.loop_total == 3: prim += indices + v_map += v_indices + # If > 3 loops per polygon (Non-Triangular?) elif poly.loop_total > 3: for i in range(poly.loop_total-2): prim += (indices[-1], indices[i], indices[i + 1]) + v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) # Write indices o['index_arrays'] = [] for mat, prim in prims.items(): idata = [0] * len(prim) + v_map_data = [0] * len(prim) + v_map_sub = v_maps[mat] for i, v in enumerate(prim): idata[i] = v + v_map_data[i] = v_map_sub[i] if len(idata) == 0: # No face assigned continue - ia = {'values': idata, 'material': 0} + ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} # Find material index for multi-mat mesh if len(export_mesh.materials) > 1: for i in range(0, len(export_mesh.materials)): From ebf7adaa17546ff462345908624b5c15695e4057 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 15:42:37 +0200 Subject: [PATCH 022/175] remove debug traces --- .../armory/trait/physics/bullet/SoftBody.hx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 12bc9bda50..663e5be094 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -68,7 +68,6 @@ class SoftBody extends Trait { for (ar in ars) { for (j in 0...ar.length) { vals[i] = ar[j]; - trace(ar[j]); i++; } } @@ -103,8 +102,6 @@ class SoftBody extends Trait { v.y *= object.transform.scale.y; v.z *= object.transform.scale.z; v.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); - trace("Position index" + i); - trace(v); positions[i * 3 ] = v.x; positions[i * 3 + 1] = v.y; positions[i * 3 + 2] = v.z; @@ -186,9 +183,7 @@ class SoftBody extends Trait { function update() { var mo = cast(object, MeshObject); var geom = mo.data.geom; - trace(";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; UPDATE"); #if arm_deinterleaved - trace("deinterleieved"); var v = geom.vertexBuffers[0].lock(); var n = geom.vertexBuffers[1].lock(); #else @@ -198,17 +193,6 @@ class SoftBody extends Trait { var l = geom.structLength; #end var numVerts = geom.getVerticesCount(); - trace("num verts = " + numVerts); - for(i in 0...numVerts * l) { - trace("point " + i + " = " + v.getInt16( i * 2 )); - } - trace("num tris = " + geom.numTris); - trace("structLength = " + l); - trace("total len =" + v.byteLength); - for(k in geom.vertexBufferMap.keys()){ - trace("key = " + k); - trace(geom.vertexBufferMap.get(k)); - } #if js var nodes = body.get_m_nodes(); @@ -221,8 +205,6 @@ class SoftBody extends Trait { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); - trace("nodepos " + i); - trace("( " + nodePos.x() + ", " + nodePos.y() + ", " + nodePos.z() + " )"); #elseif cpp var nodePos = node.m_x; #end From 85b2c3377fe4376b28f3554b9eca4437fc1b7d62 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 15:43:17 +0200 Subject: [PATCH 023/175] use vertex_map index buffer attribute to map vertices and indices --- .../armory/trait/physics/bullet/SoftBody.hx | 100 ++++++++++++------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 663e5be094..0a021e57a4 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -10,6 +10,7 @@ import iron.object.MeshObject; import iron.data.Geometry; import iron.data.MeshData; import iron.data.SceneFormat; +import kha.arrays.Uint32Array; #if arm_physics_soft import armory.trait.physics.RigidBody; import armory.trait.physics.PhysicsWorld; @@ -38,6 +39,8 @@ class SoftBody extends Trait { static var helpersCreated = false; static var worldInfo: bullet.Bt.SoftBodyWorldInfo; + var vertexIndexMap: Map>; + public function new(shape = SoftShape.Cloth, bend = 0.5, mass = 1.0, margin = 0.04) { super(); this.shape = shape; @@ -74,6 +77,22 @@ class SoftBody extends Trait { return vals; } + function generateVertexIndexMap(ind: haxe.ds.Vector, vert: haxe.ds.Vector) { + if (vertexIndexMap == null) vertexIndexMap = new Map(); + for (i in 0...ind.length) { + var currentVertex = vert[i]; + var currentIndex = ind[i]; + + var mapping = vertexIndexMap.get(currentVertex); + if (mapping == null) { + vertexIndexMap.set(currentVertex, [currentIndex]); + } + else { + if(! mapping.contains(currentIndex)) mapping.push(currentIndex); + } + } + } + var v = new Vec4(); function init() { if (ready) return; @@ -84,6 +103,17 @@ class SoftBody extends Trait { var mo = cast(object, MeshObject); mo.frustumCulling = false; var geom = mo.data.geom; + var rawData = mo.data.raw; + var vertexMap: Array = []; + for (ind in rawData.index_arrays) { + if (ind.vertex_map == null) return; + vertexMap.push(ind.vertex_map); + } + + var vecind = fromU32(geom.indices); + var vertexMapArray = fromU32(vertexMap); + + generateVertexIndexMap(vecind, vertexMapArray); // Parented soft body - clear parent location if (object.parent != null && object.parent.name != "") { @@ -115,7 +145,6 @@ class SoftBody extends Trait { object.transform.rot.set(0, 0, 0, 1); object.transform.buildMatrix(); - var vecind = fromU32(geom.indices); var numtri = 0; for (ar in geom.indices) numtri += Std.int(ar.length / 3); @@ -125,18 +154,26 @@ class SoftBody extends Trait { helpersCreated = true; } - var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(positions.length); + var verts: Array = []; + for (key in vertexIndexMap.keys()) { + var i = vertexIndexMap.get(key)[0]; + verts.push(positions[i * 3 ]); + verts.push(positions[i * 3 + 1]); + verts.push(positions[i * 3 + 2]); + } + + var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(verts.length); for(i in 0...positions.length){ - positionsVector.set(i, positions.get(i)); + positionsVector.set(i, verts[i]); } - var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vecind.length); - for(i in 0...vecind.length){ - vecindVector.set(i, vecind.get(i)); + var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vertexMapArray.length); + for(i in 0...vertexMapArray.length){ + vecindVector.set(i, vertexMapArray.get(i)); } #if js - body = helpers.CreateFromTriMesh(worldInfo, positions, vecind, numtri); + body = helpers.CreateFromTriMesh(worldInfo, positionsVector, vecindVector, numtri); #elseif cpp untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); #end @@ -201,7 +238,7 @@ class SoftBody extends Trait { #end var scalePos = 1.0; - for (i in 0...numVerts) { + for (i in 0...nodes.size()) { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); @@ -215,10 +252,9 @@ class SoftBody extends Trait { mo.data.scalePos = scalePos; mo.transform.scaleWorld = scalePos; mo.transform.buildMatrix(); - - for (i in 0...numVerts) { + for (i in 0...nodes.size()) { var node = nodes.at(i); - var vertIndex = i * l * 2; + var indices = vertexIndexMap.get(i); #if js var nodePos = node.get_m_x(); var nodeNor = node.get_m_n(); @@ -226,27 +262,29 @@ class SoftBody extends Trait { var nodePos = node.m_x; var nodeNor = node.m_n; #end - #if arm_deinterleaved - v.set(i * 4 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.set(i * 4 + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.set(i * 4 + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - n.set(i * 2 , Std.int(nodeNor.x() * 32767)); - n.set(i * 2 + 1, Std.int(nodeNor.y() * 32767)); - v.set(i * 4 + 3, Std.int(nodeNor.z() * 32767)); - #else - //trace(i); - v.setInt16(vertIndex , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.setInt16(vertIndex + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.setInt16(vertIndex + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - if (vbPos != null) { - v2.setInt16(i * 8 , v.getInt16(vertIndex )); - v2.setInt16(i * 8 + 2, v.getInt16(vertIndex + 2)); - v2.setInt16(i * 8 + 4, v.getInt16(vertIndex + 4)); + for (idx in indices){ + var vertIndex = idx * l * 2; + #if arm_deinterleaved + v.set(idx * 4 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.set(idx * 4 + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.set(idx * 4 + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + n.set(idx * 2 , Std.int(nodeNor.x() * 32767)); + n.set(idx * 2 + 1, Std.int(nodeNor.y() * 32767)); + v.set(idx * 4 + 3, Std.int(nodeNor.z() * 32767)); + #else + v.setInt16(vertIndex , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + if (vbPos != null) { + v2.setInt16(idx * 8 , v.getInt16(vertIndex )); + v2.setInt16(idx * 8 + 2, v.getInt16(vertIndex + 2)); + v2.setInt16(idx * 8 + 4, v.getInt16(vertIndex + 4)); + } + v.setInt16(vertIndex + 6, Std.int(nodeNor.z() * 32767)); + v.setInt16(vertIndex + 8, Std.int(nodeNor.x() * 32767)); + v.setInt16(vertIndex + 10, Std.int(nodeNor.y() * 32767)); + #end } - v.setInt16(vertIndex + 6, Std.int(nodeNor.z() * 32767)); - v.setInt16(vertIndex + 8, Std.int(nodeNor.x() * 32767)); - v.setInt16(vertIndex + 10, Std.int(nodeNor.y() * 32767)); - #end } // for (i in 0...Std.int(geom.indices[0].length / 3)) { // var a = geom.indices[0][i * 3]; From 5627db4f1d4f41d4506db1c16cf9314e4e43f189 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 16:30:12 +0200 Subject: [PATCH 024/175] fix looping over vertices and indices --- Sources/armory/trait/physics/bullet/SoftBody.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 0a021e57a4..165aafc7c0 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -163,12 +163,12 @@ class SoftBody extends Trait { } var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(verts.length); - for(i in 0...positions.length){ + for(i in 0...positionsVector.length){ positionsVector.set(i, verts[i]); } var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vertexMapArray.length); - for(i in 0...vertexMapArray.length){ + for(i in 0...vecindVector.length){ vecindVector.set(i, vertexMapArray.get(i)); } From ba7c2bb2d72bf2b168e7f8fc25bcc4e8385d2cf7 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 10 Jun 2023 16:31:17 +0200 Subject: [PATCH 025/175] fix for deinterleaved vertex buffers --- .../armory/trait/physics/bullet/SoftBody.hx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 165aafc7c0..251815933f 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -221,8 +221,8 @@ class SoftBody extends Trait { var mo = cast(object, MeshObject); var geom = mo.data.geom; #if arm_deinterleaved - var v = geom.vertexBuffers[0].lock(); - var n = geom.vertexBuffers[1].lock(); + var v: ByteArray = geom.vertexBuffers[0].buffer.lock(); + var n: ByteArray = geom.vertexBuffers[1].buffer.lock(); #else var v:ByteArray = geom.vertexBuffer.lock(); var vbPos = geom.vertexBufferMap.get("pos"); @@ -263,15 +263,15 @@ class SoftBody extends Trait { var nodeNor = node.m_n; #end for (idx in indices){ - var vertIndex = idx * l * 2; #if arm_deinterleaved - v.set(idx * 4 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.set(idx * 4 + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.set(idx * 4 + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - n.set(idx * 2 , Std.int(nodeNor.x() * 32767)); - n.set(idx * 2 + 1, Std.int(nodeNor.y() * 32767)); - v.set(idx * 4 + 3, Std.int(nodeNor.z() * 32767)); + v.setInt16(idx * 8 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + n.setInt16(idx * 4 , Std.int(nodeNor.x() * 32767)); + n.setInt16(idx * 4 + 2, Std.int(nodeNor.y() * 32767)); + v.setInt16(idx * 8 + 6, Std.int(nodeNor.z() * 32767)); #else + var vertIndex = idx * l * 2; v.setInt16(vertIndex , Std.int(nodePos.x() * 32767 * (1 / scalePos))); v.setInt16(vertIndex + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); v.setInt16(vertIndex + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); @@ -308,8 +308,8 @@ class SoftBody extends Trait { // v.set(c * l + 5, cb.z); // } #if arm_deinterleaved - geom.vertexBuffers[0].unlock(); - geom.vertexBuffers[1].unlock(); + geom.vertexBuffers[0].buffer.unlock(); + geom.vertexBuffers[1].buffer.unlock(); #else geom.vertexBuffer.unlock(); if (vbPos != null) vbPos.unlock(); From 1ffb3206ae249c82a08311198904b646e9c163a2 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 14 Jun 2023 14:32:26 +0200 Subject: [PATCH 026/175] Fix SoftBody on HL using custom float and int arrays --- .../armory/trait/physics/bullet/SoftBody.hx | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 251815933f..7726aa1b2d 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -1,8 +1,11 @@ package armory.trait.physics.bullet; -import kha.arrays.ByteArray; #if arm_bullet - +import iron.Scene; +import haxe.ds.Vector; +import kha.arrays.ByteArray; +import bullet.Bt.Vector3; +import bullet.Bt.CollisionObjectActivationState; import iron.math.Vec4; import iron.math.Mat4; import iron.Trait; @@ -154,30 +157,42 @@ class SoftBody extends Trait { helpersCreated = true; } - var verts: Array = []; - for (key in vertexIndexMap.keys()) { + var vertsLength = 0; + for (key in vertexIndexMap.keys()) vertsLength++; + var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(vertsLength * 3); + for (key in 0...vertsLength){ var i = vertexIndexMap.get(key)[0]; - verts.push(positions[i * 3 ]); - verts.push(positions[i * 3 + 1]); - verts.push(positions[i * 3 + 2]); - } - - var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(verts.length); - for(i in 0...positionsVector.length){ - positionsVector.set(i, verts[i]); + positionsVector.set(key * 3 , positions[i * 3 ]); + positionsVector.set(key * 3 + 1, positions[i * 3 + 1]); + positionsVector.set(key * 3 + 2, positions[i * 3 + 2]); } + var indexMax: Int = 0; var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vertexMapArray.length); - for(i in 0...vecindVector.length){ - vecindVector.set(i, vertexMapArray.get(i)); + for (i in 0...vecindVector.length){ + var idx = vertexMapArray.get(i); + vecindVector.set(i, idx); + indexMax = indexMax > idx ? indexMax : idx; } #if js body = helpers.CreateFromTriMesh(worldInfo, positionsVector, vecindVector, numtri); - #elseif cpp - untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); + #else + //Create helper float array + var floatArray = new bullet.Bt.FloatArray(positionsVector.length); + for (i in 0...positionsVector.length){ + floatArray.set(i, positionsVector[i]); + } + //Create helper int array + var intArray = new bullet.Bt.IntArray(vecindVector.length); + for (i in 0...vecindVector.length){ + intArray.set(i, vecindVector[i]); + } + //world info is passed as value and not as a reference, need to set gravity again in HL. + worldInfo.m_gravity = physics.world.getGravity(); + //Create soft body + body = helpers.CreateFromTriMesh(worldInfo, floatArray.raw, intArray.raw, numtri, false); #end - // body.generateClusters(4); #if js @@ -185,29 +200,33 @@ class SoftBody extends Trait { cfg.set_viterations(physics.solverIterations); cfg.set_piterations(physics.solverIterations); // cfg.set_collisions(0x0001 + 0x0020 + 0x0040); // self collision - // cfg.set_collisions(0x11); // Soft-rigid, soft-soft + cfg.set_collisions(0x11); // Soft-rigid, soft-soft if (shape == SoftShape.Volume) { cfg.set_kDF(0.1); cfg.set_kDP(0.01); cfg.set_kPR(bend); } - - #elseif cpp - body.m_cfg.viterations = physics.solverIterations; - body.m_cfg.piterations = physics.solverIterations; + #else + //Not passed as refernece + var cfg = body.m_cfg; + cfg.viterations = physics.solverIterations; + cfg.piterations = physics.solverIterations; // body.m_cfg.collisions = 0x0001 + 0x0020 + 0x0040; + cfg.collisions = 0x11; // Soft-rigid, soft-soft if (shape == SoftShape.Volume) { - body.m_cfg.kDF = 0.1; - body.m_cfg.kDP = 0.01; - body.m_cfg.kPR = bend; + cfg.kDF = 0.1; + cfg.kDP = 0.01; + cfg.kPR = bend; } + //Set config again in HL + body.m_cfg = cfg; #end body.setTotalMass(mass, false); body.getCollisionShape().setMargin(margin); physics.world.addSoftBody(body, 1, -1); - body.setActivationState(bullet.Bt.CollisionObject.DISABLE_DEACTIVATION); + body.setActivationState(CollisionObjectActivationState.DISABLE_DEACTIVATION); notifyOnUpdate(update); } @@ -233,7 +252,7 @@ class SoftBody extends Trait { #if js var nodes = body.get_m_nodes(); - #elseif cpp + #else var nodes = body.m_nodes; #end @@ -242,7 +261,7 @@ class SoftBody extends Trait { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); - #elseif cpp + #else var nodePos = node.m_x; #end if (Math.abs(nodePos.x()) > scalePos) scalePos = Math.abs(nodePos.x()); @@ -258,7 +277,7 @@ class SoftBody extends Trait { #if js var nodePos = node.get_m_x(); var nodeNor = node.get_m_n(); - #elseif cpp + #else var nodePos = node.m_x; var nodeNor = node.m_n; #end From ac24dd19f17a209d467b1d866c129410c95a65ba Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 14 Jun 2023 14:33:53 +0200 Subject: [PATCH 027/175] fix PhysicsHook for HL --- Sources/armory/trait/physics/bullet/PhysicsHook.hx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/PhysicsHook.hx b/Sources/armory/trait/physics/bullet/PhysicsHook.hx index e0f8e45008..5501735fee 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsHook.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsHook.hx @@ -92,17 +92,15 @@ class PhysicsHook extends Trait { #if js var nodes = sb.body.get_m_nodes(); - #elseif cpp + #else var nodes = sb.body.m_nodes; #end - var geom = cast(object, MeshObject).data.geom; - var numNodes = Std.int(geom.positions.values.length / 4); - for (i in 0...numNodes) { + for (i in 0...nodes.size()) { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); - #elseif cpp + #else var nodePos = node.m_x; #end From 0fc237fda347724e3395667d562e6fc88343d4f5 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 17 Jun 2023 21:52:32 +0200 Subject: [PATCH 028/175] Attempt fixing spawning at runtime --- .../armory/trait/physics/bullet/SoftBody.hx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index 7726aa1b2d..fdfa427d40 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -51,9 +51,10 @@ class SoftBody extends Trait { this.mass = mass; this.margin = margin; - iron.Scene.active.notifyOnInit(function() { - notifyOnInit(init); - }); + //notifyOnAdd(init); + //The above line works as well, but the object transforms are not set + //properly, so the positions are not accurate + notifyOnInit(init); } function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): haxe.ds.Vector { @@ -96,8 +97,20 @@ class SoftBody extends Trait { } } - var v = new Vec4(); function init() { + var mo = cast(object, MeshObject); + //Set new mesh data for this object + new MeshData(mo.data.raw, function (data) { + mo.setData(data); + //Init soft body after setting new data + initSoftBody(); + //If the above line is commented out, the program becomes unresponsive with white screen + //and no errors. + }); + } + + var v = new Vec4(); + function initSoftBody() { if (ready) return; ready = true; From fb32db89bf88e65afd09656008a6e85e8fcff3ad Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 18 Jun 2023 01:48:03 +0200 Subject: [PATCH 029/175] Adaptively set object position --- .../armory/trait/physics/bullet/SoftBody.hx | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index fdfa427d40..bcfc40acd0 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -152,14 +152,6 @@ class SoftBody extends Trait { positions[i * 3 + 1] = v.y; positions[i * 3 + 2] = v.z; } - vertOffsetX = object.transform.worldx(); - vertOffsetY = object.transform.worldy(); - vertOffsetZ = object.transform.worldz(); - - object.transform.scale.set(1, 1, 1); - object.transform.loc.set(0, 0, 0); - object.transform.rot.set(0, 0, 0, 1); - object.transform.buildMatrix(); var numtri = 0; for (ar in geom.indices) numtri += Std.int(ar.length / 3); @@ -261,29 +253,63 @@ class SoftBody extends Trait { var v2 = vbPos != null ? vbPos.lock() : null; // For shadows var l = geom.structLength; #end - var numVerts = geom.getVerticesCount(); #if js var nodes = body.get_m_nodes(); #else var nodes = body.m_nodes; #end + var numNodes = nodes.size(); + //Finding the mean position of vertices in world space + vertOffsetX = 0.0; + vertOffsetY = 0.0; + vertOffsetZ = 0.0; + for (i in 0...numNodes) { + var node = nodes.at(i); + #if js + var nodePos = node.get_m_x(); + #else + var nodePos = node.m_x; + #end + var mx = nodePos.x(); + var my = nodePos.y(); + var mz = nodePos.z(); + vertOffsetX += mx; + vertOffsetY += my; + vertOffsetZ += mz; + } + vertOffsetX /= numNodes; + vertOffsetY /= numNodes; + vertOffsetZ /= numNodes; + + //Setting the mean position as object local location + mo.transform.scale.set(1, 1, 1); + mo.transform.loc.set(vertOffsetX, vertOffsetY, vertOffsetZ); + mo.transform.rot.set(0, 0, 0, 1); + + //Checking maximum dimension for scalePos var scalePos = 1.0; - for (i in 0...nodes.size()) { + for (i in 0...numNodes) { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); #else var nodePos = node.m_x; #end - if (Math.abs(nodePos.x()) > scalePos) scalePos = Math.abs(nodePos.x()); - if (Math.abs(nodePos.y()) > scalePos) scalePos = Math.abs(nodePos.y()); - if (Math.abs(nodePos.z()) > scalePos) scalePos = Math.abs(nodePos.z()); + var mx = nodePos.x() - vertOffsetX; + var my = nodePos.y() - vertOffsetY; + var mz = nodePos.z() - vertOffsetZ; + if (Math.abs(mx * 2) > scalePos) scalePos = Math.abs(mx * 2); + if (Math.abs(my * 2) > scalePos) scalePos = Math.abs(my * 2); + if (Math.abs(mz * 2) > scalePos) scalePos = Math.abs(mz * 2); } + //Set scalePos and buildMatrix mo.data.scalePos = scalePos; mo.transform.scaleWorld = scalePos; mo.transform.buildMatrix(); + + //Set vertices with location offset for (i in 0...nodes.size()) { var node = nodes.at(i); var indices = vertexIndexMap.get(i); @@ -294,27 +320,35 @@ class SoftBody extends Trait { var nodePos = node.m_x; var nodeNor = node.m_n; #end + var mx = nodePos.x() - vertOffsetX; + var my = nodePos.y() - vertOffsetY; + var mz = nodePos.z() - vertOffsetZ; + + var nx = nodeNor.x(); + var ny = nodeNor.y(); + var nz = nodeNor.z(); + for (idx in indices){ #if arm_deinterleaved - v.setInt16(idx * 8 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.setInt16(idx * 8 + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.setInt16(idx * 8 + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - n.setInt16(idx * 4 , Std.int(nodeNor.x() * 32767)); - n.setInt16(idx * 4 + 2, Std.int(nodeNor.y() * 32767)); - v.setInt16(idx * 8 + 6, Std.int(nodeNor.z() * 32767)); + v.setInt16(idx * 8 , Std.int(mx * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 2, Std.int(my * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 4, Std.int(mz * 32767 * (1 / scalePos))); + n.setInt16(idx * 4 , Std.int(nx * 32767)); + n.setInt16(idx * 4 + 2, Std.int(ny * 32767)); + v.setInt16(idx * 8 + 6, Std.int(nz * 32767)); #else var vertIndex = idx * l * 2; - v.setInt16(vertIndex , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.setInt16(vertIndex + 2, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.setInt16(vertIndex + 4, Std.int(nodePos.z() * 32767 * (1 / scalePos))); + v.setInt16(vertIndex , Std.int(mx * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 2, Std.int(my * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 4, Std.int(mz * 32767 * (1 / scalePos))); if (vbPos != null) { v2.setInt16(idx * 8 , v.getInt16(vertIndex )); v2.setInt16(idx * 8 + 2, v.getInt16(vertIndex + 2)); v2.setInt16(idx * 8 + 4, v.getInt16(vertIndex + 4)); } - v.setInt16(vertIndex + 6, Std.int(nodeNor.z() * 32767)); - v.setInt16(vertIndex + 8, Std.int(nodeNor.x() * 32767)); - v.setInt16(vertIndex + 10, Std.int(nodeNor.y() * 32767)); + v.setInt16(vertIndex + 6, Std.int(nx * 32767)); + v.setInt16(vertIndex + 8, Std.int(ny * 32767)); + v.setInt16(vertIndex + 10, Std.int(nz * 32767)); #end } } From 8a93eb68fd6f87b08e31f6fd0322fabffce5049c Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 18 Jun 2023 12:57:20 +0200 Subject: [PATCH 030/175] Add function to remove soft body, delete statements for memory safety. --- .../armory/trait/physics/bullet/SoftBody.hx | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index bcfc40acd0..9e44f12267 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -160,6 +160,10 @@ class SoftBody extends Trait { helpers = new bullet.Bt.SoftBodyHelpers(); worldInfo = physics.world.getWorldInfo(); helpersCreated = true; + #if hl + //world info is passed as value and not as a reference, need to set gravity again in HL. + worldInfo.m_gravity = physics.world.getGravity(); + #end } var vertsLength = 0; @@ -193,10 +197,11 @@ class SoftBody extends Trait { for (i in 0...vecindVector.length){ intArray.set(i, vecindVector[i]); } - //world info is passed as value and not as a reference, need to set gravity again in HL. - worldInfo.m_gravity = physics.world.getGravity(); //Create soft body body = helpers.CreateFromTriMesh(worldInfo, floatArray.raw, intArray.raw, numtri, false); + + floatArray.delete(); + intArray.delete(); #end // body.generateClusters(4); @@ -233,6 +238,11 @@ class SoftBody extends Trait { physics.world.addSoftBody(body, 1, -1); body.setActivationState(CollisionObjectActivationState.DISABLE_DEACTIVATION); + #if hl + cfg.delete(); + #end + + notifyOnRemove(removeFromWorld); notifyOnUpdate(update); } @@ -278,6 +288,11 @@ class SoftBody extends Trait { vertOffsetX += mx; vertOffsetY += my; vertOffsetZ += mz; + + #if hl + node.delete(); + nodePos.delete(); + #end } vertOffsetX /= numNodes; vertOffsetY /= numNodes; @@ -303,6 +318,11 @@ class SoftBody extends Trait { if (Math.abs(mx * 2) > scalePos) scalePos = Math.abs(mx * 2); if (Math.abs(my * 2) > scalePos) scalePos = Math.abs(my * 2); if (Math.abs(mz * 2) > scalePos) scalePos = Math.abs(mz * 2); + + #if hl + node.delete(); + nodePos.delete(); + #end } //Set scalePos and buildMatrix mo.data.scalePos = scalePos; @@ -351,6 +371,12 @@ class SoftBody extends Trait { v.setInt16(vertIndex + 10, Std.int(nz * 32767)); #end } + + #if hl + node.delete(); + nodePos.delete(); + nodeNor.delete(); + #end } // for (i in 0...Std.int(geom.indices[0].length / 3)) { // var a = geom.indices[0][i * 3]; @@ -380,6 +406,18 @@ class SoftBody extends Trait { geom.vertexBuffer.unlock(); if (vbPos != null) vbPos.unlock(); #end + #if hl + nodes.delete(); + #end + } + + function removeFromWorld() { + physics.world.removeSoftBody(body); + #if js + bullet.Bt.Ammo.destroy(body); + #else + body.delete(); + #end } #end From a9cb538e8a6c167e733c0895f6d20cdfce32da06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Tue, 20 Jun 2023 00:30:46 +0200 Subject: [PATCH 031/175] Add support for Bullet physics debug drawing --- .../trait/physics/bullet/DebugDrawHelper.hx | 224 ++++++++++++++++++ .../trait/physics/bullet/PhysicsWorld.hx | 189 ++++++++++++++- blender/arm/exporter.py | 10 + blender/arm/props.py | 34 +++ blender/arm/props_ui.py | 35 +++ 5 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 Sources/armory/trait/physics/bullet/DebugDrawHelper.hx diff --git a/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx b/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx new file mode 100644 index 0000000000..a528839df8 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx @@ -0,0 +1,224 @@ +package armory.trait.physics.bullet; + +import bullet.Bt.Vector3; + +import kha.FastFloat; +import kha.System; + +import iron.math.Vec4; + +#if arm_ui +import armory.ui.Canvas; +#end + +using StringTools; + +class DebugDrawHelper { + static inline var contactPointSizePx = 4; + static inline var contactPointNormalColor = 0xffffffff; + static inline var contactPointDrawLifetime = true; + + final physicsWorld: PhysicsWorld; + final lines: Array = []; + final texts: Array = []; + var font: kha.Font = null; + + var debugMode: PhysicsWorld.DebugDrawMode = NoDebug; + + public function new(physicsWorld: PhysicsWorld) { + this.physicsWorld = physicsWorld; + + #if arm_ui + iron.data.Data.getFont(Canvas.defaultFontName, function(defaultFont: kha.Font) { + font = defaultFont; + }); + #end + + iron.App.notifyOnRender2D(onRender); + } + + public function drawLine(from: bullet.Bt.Vector3, to: bullet.Bt.Vector3, color: bullet.Bt.Vector3) { + #if js + // https://github.com/InfiniteLee/ammo-debug-drawer/pull/1/files + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html#pointers-and-comparisons + from = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", from); + to = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", to); + color = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", color); + #end + + final fromScreenSpace = worldToScreenFast(new Vec4(from.x(), from.y(), from.z(), 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(to.x(), to.y(), to.z(), 1.0)); + + // For now don't draw lines if any point is outside of clip space z, + // investigate how to clamp lines to clip space borders + if (fromScreenSpace.w == 1 && toScreenSpace.w == 1) { + lines.push({ + fromX: fromScreenSpace.x, + fromY: fromScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: kha.Color.fromFloats(color.x(), color.y(), color.z(), 1.0) + }); + } + } + + public function drawContactPoint(pointOnB: Vector3, normalOnB: Vector3, distance: kha.FastFloat, lifeTime: Int, color: Vector3) { + #if js + pointOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", pointOnB); + normalOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", normalOnB); + color = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", color); + #end + + final contactPointScreenSpace = worldToScreenFast(new Vec4(pointOnB.x(), pointOnB.y(), pointOnB.z(), 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(pointOnB.x() + normalOnB.x() * distance, pointOnB.y() + normalOnB.y() * distance, pointOnB.z() + normalOnB.z() * distance, 1.0)); + + if (contactPointScreenSpace.w == 1) { + final color = kha.Color.fromFloats(color.x(), color.y(), color.z(), 1.0); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y - contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y + contactPointSizePx, + color: color + }); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y + contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y - contactPointSizePx, + color: color + }); + + if (toScreenSpace.w == 1) { + lines.push({ + fromX: contactPointScreenSpace.x, + fromY: contactPointScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: contactPointNormalColor + }); + } + + if (contactPointDrawLifetime && font != null) { + texts.push({ + x: contactPointScreenSpace.x, + y: contactPointScreenSpace.y, + color: color, + text: Std.string(lifeTime), // lifeTime: number of frames the contact point existed + }); + } + } + } + + public function reportErrorWarning(warningString: bullet.Bt.BulletString) { + trace(warningString.toHaxeString().trim()); + } + + public function draw3dText(location: Vector3, textString: bullet.Bt.BulletString) { + if (font == null) { + return; + } + + #if js + location = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", location); + #end + + final locationScreenSpace = worldToScreenFast(new Vec4(location.x(), location.y(), location.z(), 1.0)); + + texts.push({ + x: locationScreenSpace.x, + y: locationScreenSpace.y, + color: kha.Color.fromFloats(0.0, 0.0, 0.0, 1.0), + text: textString.toHaxeString() + }); + } + + public function setDebugMode(debugMode: PhysicsWorld.DebugDrawMode) { + this.debugMode = debugMode; + } + + public function getDebugMode(): PhysicsWorld.DebugDrawMode { + #if js + return debugMode; + #elseif hl + return physicsWorld.getDebugDrawMode(); + #else + return NoDebug; + #end + } + + function onRender(g: kha.graphics2.Graphics) { + if (getDebugMode() == NoDebug) { + return; + } + + // It might be a bit unusual to call this method in a render callback + // instead of the update loop (after all it doesn't draw anything but + // will cause Bullet to call the btIDebugDraw callbacks), but this way + // we can ensure that--within a frame--the function will not be called + // before some user-specific physics update, which would result in a + // one-frame drawing delay... Ideally we would ensure that debugDrawWorld() + // is called when all other (late) update callbacks are already executed... + physicsWorld.world.debugDrawWorld(); + + g.opacity = 1.0; + + for (line in lines) { + g.color = line.color; + g.drawLine(line.fromX, line.fromY, line.toX, line.toY, 1.0); + } + lines.resize(0); + + if (font != null) { + g.font = font; + g.fontSize = 12; + for (text in texts) { + g.color = text.color; + g.drawString(text.text, text.x, text.y); + } + texts.resize(0); + } + } + + /** + Transform a world coordinate vector into screen space and store the + result in the input vector's x and y coordinates. The w coordinate is + set to 0 if the input vector is outside the active camera's far and near + planes, and 1 otherwise. + **/ + inline function worldToScreenFast(loc: Vec4): Vec4 { + final cam = iron.Scene.active.camera; + loc.w = 1.0; + loc.applyproj(cam.VP); + + if (loc.z < -1 || loc.z > 1) { + loc.w = 0.0; + } + else { + loc.x = (loc.x + 1) * 0.5 * System.windowWidth(); + loc.y = (1 - loc.y) * 0.5 * System.windowHeight(); + loc.w = 1.0; + } + + return loc; + } +} + +@:structInit +class LineData { + public var fromX: FastFloat; + public var fromY: FastFloat; + public var toX: FastFloat; + public var toY: FastFloat; + public var color: kha.Color; +} + +@:structInit +class TextData { + public var x: FastFloat; + public var y: FastFloat; + public var color: kha.Color; + public var text: String; +} diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx index 6af4cd2761..97f40499a3 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -79,11 +79,13 @@ class PhysicsWorld extends Trait { static var transform1: bullet.Bt.Transform = null; static var transform2: bullet.Bt.Transform = null; + var debugDrawHelper: DebugDrawHelper = null; + #if arm_debug public static var physTime = 0.0; #end - public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10) { + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, debugDrawMode: DebugDrawMode = NoDebug) { super(); if (nullvec) { @@ -122,6 +124,8 @@ class PhysicsWorld extends Trait { _lateUpdate = [lateUpdate]; @:privateAccess iron.App.traitLateUpdates.insert(0, lateUpdate); + setDebugDrawMode(debugDrawMode); + iron.Scene.active.notifyOnRemove(function() { sceneRemoved = true; }); @@ -445,7 +449,7 @@ class PhysicsWorld extends Trait { var worldCol: bullet.Bt.CollisionWorld = worldDyn; var bodyColl: bullet.Bt.ConvexShape = cast rb.body.getCollisionShape(); worldCol.convexSweepTest(bodyColl, transformFrom, transformTo, convexCallback, 0.0); - + var hitInfo: ConvexHit = null; var cc: bullet.Bt.ClosestConvexResultCallback = convexCallback; @@ -483,6 +487,187 @@ class PhysicsWorld extends Trait { public function removePreUpdate(f: Void->Void) { preUpdates.remove(f); } + + public function setDebugDrawMode(debugDrawMode: DebugDrawMode) { + if (debugDrawHelper == null) { + if (debugDrawMode == NoDebug) { + return; + } + initDebugDrawing(); + } + + #if js + world.getDebugDrawer().setDebugMode(debugDrawMode); + #elseif hl + hlDebugDrawer_setDebugMode(debugDrawMode); + #end + } + + public inline function getDebugDrawMode(): DebugDrawMode { + if (debugDrawHelper == null) { + return NoDebug; + } + + #if js + return world.getDebugDrawer().getDebugMode(); + #elseif hl + return hlDebugDrawer_getDebugMode(); + #else + return NoDebug; + #end + } + + function initDebugDrawing() { + debugDrawHelper = new DebugDrawHelper(this); + + #if js + final drawer = new bullet.Bt.DebugDrawer(); + + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html?highlight=jsimplementation#sub-classing-c-base-classes-in-javascript-jsimplementation + drawer.drawLine = debugDrawHelper.drawLine; + drawer.drawContactPoint = debugDrawHelper.drawContactPoint; + drawer.reportErrorWarning = debugDrawHelper.reportErrorWarning; + drawer.draw3dText = debugDrawHelper.draw3dText; + + // From the Armory API perspective this is not required, + // but Ammo requires it and will result in a black screen if not set + drawer.setDebugMode = debugDrawHelper.setDebugMode; + drawer.getDebugMode = debugDrawHelper.getDebugMode; + + world.setDebugDrawer(drawer); + #elseif hl + hlDebugDrawer_setDrawLine(debugDrawHelper.drawLine); + hlDebugDrawer_setDrawContactPoint(debugDrawHelper.drawContactPoint); + hlDebugDrawer_setReportErrorWarning(debugDrawHelper.reportErrorWarning); + hlDebugDrawer_setDraw3dText(debugDrawHelper.draw3dText); + + hlDebugDrawer_worldSetGlobalDebugDrawer(world); + #end + } + + #if hl + @:hlNative("bullet", "debugDrawer_worldSetGlobalDebugDrawer") + public static function hlDebugDrawer_worldSetGlobalDebugDrawer(world: #if arm_physics_soft bullet.Bt.SoftRigidDynamicsWorld #else bullet.Bt.DiscreteDynamicsWorld #end) {} + + @:hlNative("bullet", "debugDrawer_setDebugMode") + public static function hlDebugDrawer_setDebugMode(debugMode: Int) {} + + @:hlNative("bullet", "debugDrawer_getDebugMode") + public static function hlDebugDrawer_getDebugMode(): Int { return 0; } + + @:hlNative("bullet", "debugDrawer_setDrawLine") + public static function hlDebugDrawer_setDrawLine(func: bullet.Bt.Vector3->bullet.Bt.Vector3->bullet.Bt.Vector3->Void) {} + + @:hlNative("bullet", "debugDrawer_setDrawContactPoint") + public static function hlDebugDrawer_setDrawContactPoint(func: bullet.Bt.Vector3->bullet.Bt.Vector3->kha.FastFloat->Int->bullet.Bt.Vector3->Void) {} + + @:hlNative("bullet", "debugDrawer_setReportErrorWarning") + public static function hlDebugDrawer_setReportErrorWarning(func: hl.Bytes->Void) {} + + @:hlNative("bullet", "debugDrawer_setDraw3dText") + public static function hlDebugDrawer_setDraw3dText(func: bullet.Bt.Vector3->hl.Bytes->Void) {} + #end +} + +/** + Debug flags for Bullet physics, despite the name not solely related to debug drawing. + You can combine multiple flags with bitwise operations. + + Taken from Bullet's `btIDebugDraw::DebugDrawModes` enum. + Please note that the descriptions of the individual flags are a result of inspecting the Bullet sources and thus might contain inaccuracies. + + @see `armory.trait.physics.PhysicsWorld.getDebugDrawMode()` + @see `armory.trait.physics.PhysicsWorld.setDebugDrawMode()` +**/ +// Not all of the flags below are actually used in the library core, some of them are only used +// in individual Bullet example projects. The intention of the original authors is unknown, +// so whether those flags are actually meant to get their purpose from the implementing application +// and not from the library remains a mystery... +enum abstract DebugDrawMode(Int) from Int to Int { + /** All debug flags off. **/ + var NoDebug = 0; + + /** Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations. **/ + var DrawWireframe = 1; + + /** Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes. **/ + var DrawAABB = 1 << 1; + + /** Not used in Armory. **/ + // Only used in a Bullet physics example at the moment: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ExampleBrowser/GL_ShapeDrawer.cpp#L616-L644 + var DrawFeaturesText = 1 << 2; + + /** Visualize contact points of multiple colliders. **/ + var DrawContactPoints = 1 << 3; + + /** + Globally disable sleeping/deactivation of dynamic colliders. + **/ + var NoDeactivation = 1 << 4; + + /** Not used in Armory. **/ + // Not used in the library core, in some Bullet examples this flag is used to print application-specific help text (e.g. keyboard shortcuts) to the screen, e.g.: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ForkLift/ForkLiftDemo.cpp#L586 + var NoHelpText = 1 << 5; + + /** Not used in Armory. **/ + // Not used in the library core, appears to be the opposite of NoHelpText (not sure why there are two flags required for this...) + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/FractureDemo/FractureDemo.cpp#L189 + var DrawText = 1 << 6; + + /** Not used in Armory. **/ + // Not even used in official Bullet examples, probably obsolete. + // Related to btQuickprof.h: https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=1285#p4743 + // Probably replaced by define: https://github.com/bulletphysics/bullet3/commit/d051e2eacb01a948c7b53e24fd3d9942ce64bdcc + var ProfileTimings = 1 << 7; + + /** Not used in Armory. **/ + // Not even used in official Bullet examples, might be obsolete. + var EnableSatComparison = 1 << 8; + + /** Not used in Armory. **/ + var DisableBulletLCP = 1 << 9; + + /** Not used in Armory. **/ + var EnableCCD = 1 << 10; + + /** Draw axis gizmos for important constraint points. **/ + var DrawConstraints = 1 << 11; + + /** Draw additional constraint information such as distance or angle limits. **/ + var DrawConstraintLimits = 1 << 12; + + /** Not used in Armory. **/ + // Only used in a Bullet physics example at the moment: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ExampleBrowser/GL_ShapeDrawer.cpp#L258 + // We could use it in the future to toggle depth testing for lines, i.e. draw actual 3D lines if not set and Kha's g2 lines if set. + var FastWireframe = 1 << 13; + + /** + Draw the normal vectors of the triangles of the physics collider meshes. + This only works for `Mesh` collision shapes. + **/ + // Outside of Armory this works for a few more collision shapes + var DrawNormals = 1 << 14; + + /** + Draw a small axis gizmo at the origin of the collision shape. + Only works if `DrawWireframe` is enabled as well. + **/ + var DrawFrames = 1 << 15; + + @:op(~A) public inline function bitwiseNegate(): DebugDrawMode { + return ~this; + } + + @:op(A & B) public inline function bitwiseAND(other: DebugDrawMode): DebugDrawMode { + return this & other; + } + + @:op(A | B) public inline function bitwiseOR(other: DebugDrawMode): DebugDrawMode { + return this | other; + } } #end diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b69a648f20..df03133f01 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2951,6 +2951,16 @@ def export_scene_traits(self) -> None: if rbw is not None and rbw.enabled: out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] + if phys_pkg == 'bullet': + debug_draw_mode = 1 if wrd.arm_bullet_dbg_draw_wireframe else 0 + debug_draw_mode |= 2 if wrd.arm_bullet_dbg_draw_aabb else 0 + debug_draw_mode |= 8 if wrd.arm_bullet_dbg_draw_contact_points else 0 + debug_draw_mode |= 2048 if wrd.arm_bullet_dbg_draw_constraints else 0 + debug_draw_mode |= 4096 if wrd.arm_bullet_dbg_draw_constraint_limits else 0 + debug_draw_mode |= 16384 if wrd.arm_bullet_dbg_draw_normals else 0 + debug_draw_mode |= 32768 if wrd.arm_bullet_dbg_draw_axis_gizmo else 0 + out_trait['parameters'].append(str(debug_draw_mode)) + self.output['traits'].append(out_trait) if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: diff --git a/blender/arm/props.py b/blender/arm/props.py index 9a6786a33e..ffb68d01b7 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -197,6 +197,40 @@ def init_properties(): items=[('Bullet', 'Bullet', 'Bullet'), ('Oimo', 'Oimo', 'Oimo')], name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_bullet_dbg_draw_wireframe = BoolProperty( + name="Collider Wireframes", default=False, + description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations" + ) + bpy.types.World.arm_bullet_dbg_draw_aabb = BoolProperty( + name="Axis-aligned Minimum Bounding Boxes", default=False, + description="Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes" + ) + bpy.types.World.arm_bullet_dbg_draw_contact_points = BoolProperty( + name="Contact Points", default=False, + description="Visualize contact points of multiple colliders" + ) + bpy.types.World.arm_bullet_dbg_draw_constraints = BoolProperty( + name="Constraints", default=False, + description="Draw axis gizmos for important constraint points" + ) + bpy.types.World.arm_bullet_dbg_draw_constraint_limits = BoolProperty( + name="Constraint Limits", default=False, + description="Draw additional constraint information such as distance or angle limits" + ) + bpy.types.World.arm_bullet_dbg_draw_normals = BoolProperty( + name="Face Normals", default=False, + description=( + "Draw the normal vectors of the triangles of the physics collider meshes." + " This only works for mesh collision shapes" + ) + ) + bpy.types.World.arm_bullet_dbg_draw_axis_gizmo = BoolProperty( + name="Axis Gizmos", default=False, + description=( + "Draw a small axis gizmo at the origin of the collision shape." + " Only works if \"Collider Wireframes\" is enabled as well" + ) + ) bpy.types.World.arm_navigation = EnumProperty( items=[('Disabled', 'Disabled', 'Disabled'), ('Auto', 'Auto', 'Auto'), diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 06afd1beeb..0320e9b008 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -2641,6 +2641,39 @@ def execute(self, context): return {'FINISHED'} + +class ARM_PT_BulletDebugDrawingPanel(bpy.types.Panel): + bl_label = "Armory Debug Drawing" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "SCENE_PT_rigid_body_world" + + @classmethod + def poll(cls, context): + return context.scene.rigidbody_world is not None + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + if wrd.arm_physics_engine != 'Bullet': + row = layout.row() + row.alert = True + row.label(text="Physics debug drawing is only supported for the Bullet physics engine") + + col = layout.column(align=False) + col.prop(wrd, "arm_bullet_dbg_draw_wireframe") + col.prop(wrd, "arm_bullet_dbg_draw_aabb") + col.prop(wrd, "arm_bullet_dbg_draw_contact_points") + col.prop(wrd, "arm_bullet_dbg_draw_constraints") + col.prop(wrd, "arm_bullet_dbg_draw_constraint_limits") + col.prop(wrd, "arm_bullet_dbg_draw_normals") + col.prop(wrd, "arm_bullet_dbg_draw_axis_gizmo") + def draw_custom_node_menu(self, context): """Extension of the node context menu. @@ -2774,6 +2807,7 @@ def register(): bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorButton) bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) + bpy.utils.register_class(ARM_PT_BulletDebugDrawingPanel) bpy.utils.register_class(scene.TLM_PT_Settings) bpy.utils.register_class(scene.TLM_PT_Denoise) @@ -2796,6 +2830,7 @@ def unregister(): bpy.types.VIEW3D_HT_header.remove(draw_view3d_header) bpy.types.TOPBAR_HT_upper_bar.remove(draw_space_topbar) + bpy.utils.unregister_class(ARM_PT_BulletDebugDrawingPanel) bpy.utils.unregister_class(ArmoryUpdateListInstalledVSButton) bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorRunButton) bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorButton) From 4d26ee320bce01d884d3e324bcdf3780caf88100 Mon Sep 17 00:00:00 2001 From: 1k8 Date: Tue, 27 Jun 2023 00:42:15 +0200 Subject: [PATCH 032/175] UI Node editor icon update --- blender/arm/nodes_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 3e24b70444..9b16a18d1f 100644 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -33,7 +33,7 @@ class ArmLogicTree(bpy.types.NodeTree): """Logic nodes""" bl_idname = 'ArmLogicTreeType' bl_label = 'Logic Node Editor' - bl_icon = 'DECORATE' + bl_icon = 'NODETREE' def update(self): pass From 7266c937cd75a8b68622549c4af42419f5a3b92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 29 Jun 2023 01:06:40 +0200 Subject: [PATCH 033/175] Set required Blender version to 3.6 --- blender/arm/utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 521718c18d..ce5c9b04f9 100644 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -800,11 +800,11 @@ def get_cascade_size(rpdat): def check_blender_version(op: bpy.types.Operator): - """Check whether the user uses the correct Blender version, if not - report in UI. + """Check whether the Blender version is supported by Armory, + if not, report in UI. """ - if not compare_version_blender_arm(): - op.report({'INFO'}, 'For Armory to work correctly, you need Blender 3.3 LTS.') + if bpy.app.version[0] != 3 or bpy.app.version[1] != 6: + op.report({'INFO'}, 'For Armory to work correctly, you need Blender 3.6 LTS.') def check_saved(self): @@ -1136,8 +1136,6 @@ def get_link_web_server(): addon_prefs = get_arm_preferences() return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server -def compare_version_blender_arm(): - return not (bpy.app.version[0] != 3 or bpy.app.version[1] != 3) def get_file_arm_version_tuple() -> tuple[int]: wrd = bpy.data.worlds['Arm'] From d7f2739cadc0adc167f259b6c84ba55ea68ed30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 29 Jun 2023 01:13:12 +0200 Subject: [PATCH 034/175] Implement Blender 3.4+ Mix shader node --- blender/arm/material/cycles.py | 3 +- .../arm/material/cycles_nodes/nodes_color.py | 49 +++++++++++++++++-- blender/arm/material/node_meta.py | 2 +- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 72dcbffbce..8b92b56dc0 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -337,7 +337,7 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: 'GAMMA', 'HUE_SAT', 'INVERT', - 'MIX_RGB', + 'MIX', 'BLACKBODY', 'VALTORGB', 'CURVE_VEC', @@ -464,6 +464,7 @@ def parse_value(node, socket): 'CLAMP', 'VALTORGB', 'MATH', + 'MIX', 'RGBTOBW', 'SEPARATE_COLOR', 'SEPHSV', diff --git a/blender/arm/material/cycles_nodes/nodes_color.py b/blender/arm/material/cycles_nodes/nodes_color.py index b7f3ba9234..35c5ecd95b 100644 --- a/blender/arm/material/cycles_nodes/nodes_color.py +++ b/blender/arm/material/cycles_nodes/nodes_color.py @@ -54,9 +54,47 @@ def parse_invert(node: bpy.types.ShaderNodeInvert, out_socket: bpy.types.NodeSoc return f'mix({out_col}, vec3(1.0) - ({out_col}), {fac})' -def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: - col1 = c.parse_vector_input(node.inputs[1]) - col2 = c.parse_vector_input(node.inputs[2]) +def parse_mix(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> str: + if node.data_type == 'FLOAT': + return _parse_mixfloat(node, out_socket, state) + elif node.data_type == 'VECTOR': + return _parse_mixvec(node, out_socket, state) + elif node.data_type == 'RGBA': + return _parse_mixrgb(node, out_socket, state) + else: + log.warn(f'Mix node: unsupported data type {node.data_type}.') + return '0.0' + + +def _parse_mixfloat(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + + return f'mix({c.parse_value_input(node.inputs[2])}, {c.parse_value_input(node.inputs[3])}, {fac})' + + +def _parse_mixvec(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if node.factor_mode == 'UNIFORM': + fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + + elif node.factor_mode == 'NON_UNIFORM': + fac = c.parse_vector_input(node.inputs[1]) + if node.clamp_factor: + fac = f'clamp({fac}, vec3(0.0), vec3(1.0))' + + else: + log.warn(f'Mix node: unsupported factor mode {node.factor_mode}.') + return 'vec3(0.0, 0.0, 0.0)' + + return f'mix({c.parse_vector_input(node.inputs[4])}, {c.parse_vector_input(node.inputs[5])}, {fac})' + + +def _parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + col1 = c.parse_vector_input(node.inputs[6]) + col2 = c.parse_vector_input(node.inputs[7]) # Store factor in variable for linked factor input if node.inputs[0].is_linked: @@ -65,6 +103,9 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc else: fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + # TODO: Do not mix if factor is constant 0.0 or 1.0? blend = node.blend_type @@ -109,7 +150,7 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') return col1 - if node.use_clamp: + if node.clamp_result: return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col) return out_col diff --git a/blender/arm/material/node_meta.py b/blender/arm/material/node_meta.py index 4860df4f62..623b2abe4e 100644 --- a/blender/arm/material/node_meta.py +++ b/blender/arm/material/node_meta.py @@ -70,7 +70,7 @@ class MaterialNodeMeta: 'HUE_SAT': MaterialNodeMeta(parse_func=nodes_color.parse_huesat), 'INVERT': MaterialNodeMeta(parse_func=nodes_color.parse_invert), 'LIGHT_FALLOFF': MaterialNodeMeta(parse_func=nodes_color.parse_lightfalloff), - 'MIX_RGB': MaterialNodeMeta(parse_func=nodes_color.parse_mixrgb), + 'MIX': MaterialNodeMeta(parse_func=nodes_color.parse_mix), # --- nodes_converter 'BLACKBODY': MaterialNodeMeta(parse_func=nodes_converter.parse_blackbody), From 2e66392ab0435df80bb3f4cad31d9fb499427f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Fri, 30 Jun 2023 22:58:15 +0200 Subject: [PATCH 035/175] [3.6 LTS] Avoid warnings for default logic tree names --- blender/arm/handlers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py index 056a526705..ca9720180c 100644 --- a/blender/arm/handlers.py +++ b/blender/arm/handlers.py @@ -89,6 +89,18 @@ def on_operator_post(operator_id: str) -> None: target_obj.arm_rb_ccd = source_obj.arm_rb_ccd target_obj.arm_rb_collision_filter_mask = source_obj.arm_rb_collision_filter_mask + elif operator_id == "NODE_OT_new_node_tree": + if bpy.context.space_data.tree_type == arm.nodes_logic.ArmLogicTree.bl_idname: + # In Blender 3.5+, new node trees are no longer called "NodeTree" + # but follow the bl_label attribute by default. New logic trees + # are thus called "Armory Logic Editor" which conflicts with Haxe's + # class naming convention. To avoid this, we listen for the + # creation of a node tree and then rename it. + # Unfortunately, manually naming the tree has the unfortunate + # side effect of not basing the new name on the name of the + # previously opened node tree, as it is the case for Blender trees... + bpy.context.space_data.edit_tree.name = "LogicTree" + def send_operator(op): if hasattr(bpy.context, 'object') and bpy.context.object is not None: From 50943bca33690a75772f9e233a0e6f3ac403c33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Fri, 30 Jun 2023 23:57:08 +0200 Subject: [PATCH 036/175] Allow to configure canvas image scaling quality from Blender UI --- Sources/armory/ui/Canvas.hx | 2 ++ blender/arm/props.py | 9 +++++++++ blender/arm/props_ui.py | 2 ++ blender/arm/write_data.py | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/Sources/armory/ui/Canvas.hx b/Sources/armory/ui/Canvas.hx index c9950e96c2..6d9d0be022 100644 --- a/Sources/armory/ui/Canvas.hx +++ b/Sources/armory/ui/Canvas.hx @@ -16,6 +16,7 @@ class Canvas { public static var screenW = -1; public static var screenH = -1; public static var locale = "en"; + public static var imageScaleQuality = kha.graphics2.ImageScaleQuality.Low; static var _ui: Zui; static var h = new zui.Zui.Handle(); // TODO: needs one handle per canvas @@ -29,6 +30,7 @@ class Canvas { _ui = ui; g.end(); + g.imageScaleQuality = Canvas.imageScaleQuality; ui.begin(g); // Bake elements g.begin(false); diff --git a/blender/arm/props.py b/blender/arm/props.py index ffb68d01b7..9387e11665 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -254,6 +254,15 @@ def init_properties(): ('Enabled', 'Enabled', 'Enabled')], name="Audio", default='Enabled', update=assets.invalidate_compiler_cache) bpy.types.World.arm_khafile = PointerProperty(name="Append Khafile", description="Source appended to the project's khafile.js after it is generated", update=assets.invalidate_compiler_cache, type=bpy.types.Text) + bpy.types.World.arm_canvas_img_scaling_quality = EnumProperty( + name='Canvas Image Quality', + description='The quality with which to scale images drawn to Kha canvases', + items=[ + ('low', 'Low', 'Low quality. Scaling usually takes place using a point filter.'), + ('high', 'High', 'High quality. Scaling usually takes place using a bilinear filter.'), + ], + default='low' + ) bpy.types.World.arm_texture_quality = FloatProperty(name="Texture Quality", default=1.0, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_sound_quality = FloatProperty(name="Sound Quality", default=0.9, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_copy_override = BoolProperty(name="Copy Override", description="Overrides any existing files when copying", default=False, update=assets.invalidate_compiled_data) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 0320e9b008..24fc2bf554 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1072,6 +1072,8 @@ def draw(self, context): col.prop(wrd, 'arm_export_tangents') col = layout.column(heading='Quality') + row = col.row() # To expand below property UI horizontally + row.prop(wrd, 'arm_canvas_img_scaling_quality', expand=True) col.prop(wrd, 'arm_texture_quality') col.prop(wrd, 'arm_sound_quality') diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index 9e0ac6cb8c..7a2a64ec58 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -498,6 +498,10 @@ class Main { f.write(""" armory.system.Starter.numAssets = """ + str(len(asset_references)) + """; armory.system.Starter.drawLoading = """ + loadscreen_class + """.render;""") + if wrd.arm_canvas_img_scaling_quality == 'low': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") + elif wrd.arm_canvas_img_scaling_quality == 'high': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") f.write(""" armory.system.Starter.main( '""" + arm.utils.safestr(scene_name) + scene_ext + """', From 16b62730a97a0c93a827983b2893fc0cbd6141e8 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sun, 2 Jul 2023 15:18:27 +0200 Subject: [PATCH 037/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 9387e11665..9b13e46769 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.6' +arm_version = '2023.7' arm_commit = '$Id$' def get_project_html5_copy(self): From fb6c2806102deac539bfc97072949e45c7d0c6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:43:13 +0200 Subject: [PATCH 038/175] Ensure referenced collections are always exported --- blender/arm/exporter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index c83682da04..b81b1afc1e 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -138,6 +138,9 @@ def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.S self.world_array = [] self.particle_system_array = {} + self.referenced_collections: list[bpy.types.Collection] = [] + """Collections referenced by collection instances""" + self.has_spawning_camera = False """Whether there is at least one camera in the scene that spawns by default""" @@ -771,6 +774,7 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: out_object['group_ref'] = bobject.instance_collection.name + self.referenced_collections.append(bobject.instance_collection) if bobject.arm_tilesheet != '': out_object['tilesheet_ref'] = bobject.arm_tilesheet @@ -2433,7 +2437,7 @@ def execute(self): if collection.name.startswith(('RigidBodyWorld', 'Trait|')): continue - if self.scene.user_of_id(collection) or collection.library: + if self.scene.user_of_id(collection) or collection.library or collection in self.referenced_collections: self.export_collection(collection) if not ArmoryExporter.option_mesh_only: From ec369f2b33ec9c30b057e7e72fac3dbe87aa11b0 Mon Sep 17 00:00:00 2001 From: tong Date: Sat, 8 Jul 2023 17:48:53 +0200 Subject: [PATCH 039/175] Add editorconfig --- .editorconfig | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..9c9dbaeff2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +tab_width = 4 +indent_style = tab + +[*.py] +indent_size = 4 +tab_width = 4 +indent_style = space From acfdc59ad1b9ab077ef060570543563f5efffde5 Mon Sep 17 00:00:00 2001 From: tong Date: Sat, 8 Jul 2023 21:20:25 +0200 Subject: [PATCH 040/175] Fix vscode settings.json --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7153bbb6d1..6778ea66a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "editor.detectIndentation": false, "editor.tabSize": 4, - "editor.insertSpaces": false + "editor.insertSpaces": false, "[python]": { "editor.insertSpaces": true - }, + } } From 1dcf6040a72af8a9d78d8c96f51d36fda4feb5c2 Mon Sep 17 00:00:00 2001 From: 1k8 Date: Thu, 13 Jul 2023 11:37:07 +0200 Subject: [PATCH 041/175] Hashlink Null Float Fix Cannot check for Null on hashlink base types so I changed it to Null whereas the Lerp/Slerp wanted an f32 so those were set to Null. Other solutions are to use a Dynamic type to then check if null, or use another inputs.get(i) in the null check. --- Sources/armory/logicnode/RotationMathNode.hx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx index 2af24da5cd..ae9473acab 100644 --- a/Sources/armory/logicnode/RotationMathNode.hx +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -54,7 +54,7 @@ class RotationMathNode extends LogicNode { } case "Amplify": { var v1: Quat = inputs[0].get(); - var v2: Float = inputs[1].get(); + var v2: Null = inputs[1].get(); if ((v1 == null) || (v2 == null)) return null; res_q.setFrom(v1); var fac2 = Math.sqrt(1- res_q.w*res_q.w); @@ -78,7 +78,7 @@ class RotationMathNode extends LogicNode { //var from = q; var from: Quat = inputs[0].get(); var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); + var f: Null = inputs[2].get(); if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.lerp(from, to, f); } @@ -86,11 +86,11 @@ class RotationMathNode extends LogicNode { //var from = q; var from:Quat = inputs[0].get(); var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); + var f: Null = inputs[2].get(); if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.slerp(from, to, f); } } return res_q; } -} \ No newline at end of file +} From c89d52eb3ea2b7a57ce8d5a93354e52a7f255407 Mon Sep 17 00:00:00 2001 From: 1k8 Date: Thu, 13 Jul 2023 12:17:51 +0200 Subject: [PATCH 042/175] Support JS Float Type Added the missing condition --- Sources/armory/logicnode/RotationMathNode.hx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx index ae9473acab..4f9b3f763f 100644 --- a/Sources/armory/logicnode/RotationMathNode.hx +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -78,7 +78,11 @@ class RotationMathNode extends LogicNode { //var from = q; var from: Quat = inputs[0].get(); var to: Quat = inputs[1].get(); + #if js + var f: Null = inputs[2].get(); + #else var f: Null = inputs[2].get(); + #end if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.lerp(from, to, f); } @@ -86,7 +90,11 @@ class RotationMathNode extends LogicNode { //var from = q; var from:Quat = inputs[0].get(); var to: Quat = inputs[1].get(); + #if js + var f: Null = inputs[2].get(); + #else var f: Null = inputs[2].get(); + #end if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.slerp(from, to, f); } From 8468b5a19937cb144bac3530eb0c27a9f26974d5 Mon Sep 17 00:00:00 2001 From: 1k8 Date: Mon, 17 Jul 2023 13:22:26 +0200 Subject: [PATCH 043/175] Update NetworkHttpRequestNode.hx Async option available on JS --- Sources/armory/logicnode/NetworkHttpRequestNode.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx index 695285acab..9ebf7a7df6 100644 --- a/Sources/armory/logicnode/NetworkHttpRequestNode.hx +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -27,9 +27,9 @@ class NetworkHttpRequestNode extends LogicNode { } var request = new haxe.Http(url); - + #if js request.async = true; - + #end if(headers != null){ for (k in headers.keys()) { request.addHeader( k, headers[k]); From e141c2fa76ad5806c99eb58cf2115c13bc38301a Mon Sep 17 00:00:00 2001 From: 1k8 Date: Mon, 17 Jul 2023 14:15:38 +0200 Subject: [PATCH 044/175] Update CreateMapNode.hx Fix type cast --- Sources/armory/logicnode/CreateMapNode.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/CreateMapNode.hx b/Sources/armory/logicnode/CreateMapNode.hx index f06fc7b35b..6a230b986c 100644 --- a/Sources/armory/logicnode/CreateMapNode.hx +++ b/Sources/armory/logicnode/CreateMapNode.hx @@ -7,7 +7,7 @@ import armory.system.Event; class CreateMapNode extends LogicNode { public var property0: String; public var property1: String; - public var map: Map; + public var map: Dynamic; public function new(tree:LogicTree) { From 7ce6ce5ff907ae0a621aeffad021b9f1d0220311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sun, 23 Jul 2023 23:28:19 +0200 Subject: [PATCH 045/175] Add "clean" button to 3D view topbar --- blender/arm/props_ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 24fc2bf554..45313c16be 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1398,6 +1398,7 @@ def draw_header(self, context): row.operator("arm.play", icon="PLAY", text="") else: row.operator("arm.stop", icon="SEQUENCE_COLOR_01", text="") + row.operator("arm.clean_menu", icon="BRUSH_DATA", text="") row.operator("arm.open_editor", icon="DESKTOP", text="") row.operator("arm.open_project_folder", icon="FILE_FOLDER", text="") From 5a8c7e58ef188245b7cd77a1912ce7a3cfb25247 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Thu, 3 Aug 2023 10:28:45 +0200 Subject: [PATCH 046/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 9b13e46769..e9f05298f5 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.7' +arm_version = '2023.8' arm_commit = '$Id$' def get_project_html5_copy(self): From 511657981bd2716eddcee8dff26820d27f2bc610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Fri, 4 Aug 2023 20:55:45 +0200 Subject: [PATCH 047/175] Cleanup class registration --- blender/arm/logicnode/arm_node_group.py | 4 +- blender/arm/logicnode/arm_nodes.py | 4 +- blender/arm/logicnode/arm_sockets.py | 4 +- .../logicnode/miscellaneous/LN_group_input.py | 4 +- .../miscellaneous/LN_group_output.py | 4 +- blender/arm/logicnode/tree_variables.py | 8 +- blender/arm/nodes_logic.py | 46 ++-- blender/arm/props_bake.py | 79 +++--- blender/arm/props_exporter.py | 8 +- blender/arm/props_lod.py | 24 +- blender/arm/props_properties.py | 24 +- blender/arm/props_renderpath.py | 24 +- blender/arm/props_tilesheet.py | 42 ++-- blender/arm/props_traits.py | 81 +++---- blender/arm/props_traits_props.py | 16 +- blender/arm/props_ui.py | 226 +++++++----------- 16 files changed, 251 insertions(+), 347 deletions(-) diff --git a/blender/arm/logicnode/arm_node_group.py b/blender/arm/logicnode/arm_node_group.py index 0e8e27ddc2..51728ad7c1 100644 --- a/blender/arm/logicnode/arm_node_group.py +++ b/blender/arm/logicnode/arm_node_group.py @@ -549,7 +549,7 @@ def draw(self, context): row.operator('arm.edit_group_tree', icon='FULLSCREEN_ENTER', text='Edit Tree') -REG_CLASSES = ( +__REG_CLASSES = ( ArmGroupTree, ArmEditGroupTree, ArmCopyGroupTree, @@ -562,4 +562,4 @@ def draw(self, context): ArmAddCallGroupNode, ARM_PT_LogicGroupPanel ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 827694fc5f..0dbebaa918 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -1014,7 +1014,7 @@ def reset_globals(): category_items = OrderedDict() -REG_CLASSES = ( +__REG_CLASSES = ( ArmNodeSearch, ArmNodeAddInputButton, ArmNodeAddInputValueButton, @@ -1026,4 +1026,4 @@ def reset_globals(): ArmNodeRemoveInputOutputButton, ArmNodeCallFuncButton ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index a07cad871d..4604d11fb7 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -633,7 +633,7 @@ def draw_color(self, context): ArmAnySocketInterface = _make_socket_interface('ArmAnySocketInterface', 'ArmAnySocket') -REG_CLASSES = ( +__REG_CLASSES = ( ArmActionSocketInterface, ArmAnimSocketInterface, ArmRotationSocketInterface, @@ -662,4 +662,4 @@ def draw_color(self, context): ArmVectorSocket, ArmAnySocket, ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/miscellaneous/LN_group_input.py b/blender/arm/logicnode/miscellaneous/LN_group_input.py index 018c3b35e6..89fec6b476 100644 --- a/blender/arm/logicnode/miscellaneous/LN_group_input.py +++ b/blender/arm/logicnode/miscellaneous/LN_group_input.py @@ -189,7 +189,7 @@ def draw_buttons_ext(self, context, layout): layout.enabled = False node = context.active_node split = layout.row() - split.template_list('ARM_UL_interface_sockets', 'OUT', node, 'outputs', node, 'active_output') + split.template_list('ARM_UL_InterfaceSockets', 'OUT', node, 'outputs', node, 'active_output') ops_col = split.column() add_remove_col = ops_col.column(align=True) props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") @@ -214,4 +214,4 @@ def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.arm_version not in (0, 1, 2): raise LookupError() - return node_tree.nodes.new('LNGroupInputsNode') \ No newline at end of file + return node_tree.nodes.new('LNGroupInputsNode') diff --git a/blender/arm/logicnode/miscellaneous/LN_group_output.py b/blender/arm/logicnode/miscellaneous/LN_group_output.py index a4fa9636d1..79b0a1720b 100644 --- a/blender/arm/logicnode/miscellaneous/LN_group_output.py +++ b/blender/arm/logicnode/miscellaneous/LN_group_output.py @@ -189,7 +189,7 @@ def draw_buttons_ext(self, context, layout): layout.enabled = False node = context.active_node split = layout.row() - split.template_list('ARM_UL_interface_sockets', 'IN', node, 'inputs', node, 'active_input') + split.template_list('ARM_UL_InterfaceSockets', 'IN', node, 'inputs', node, 'active_input') ops_col = split.column() add_remove_col = ops_col.column(align=True) props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") @@ -214,4 +214,4 @@ def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.arm_version not in (0, 1, 2): raise LookupError() - return node_tree.nodes.new('LNGroupOutputsNode') \ No newline at end of file + return node_tree.nodes.new('LNGroupOutputsNode') diff --git a/blender/arm/logicnode/tree_variables.py b/blender/arm/logicnode/tree_variables.py index 42cab1ab84..8faaaca611 100644 --- a/blender/arm/logicnode/tree_variables.py +++ b/blender/arm/logicnode/tree_variables.py @@ -525,7 +525,7 @@ def node_compat_sdk2209(): arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.synchronize(tree, item.name) -REG_CLASSES = ( +__REG_CLASSES = ( ARM_PT_Variables, ARM_OT_TreeVariableListMoveItem, ARM_OT_TreeVariableMakeLocalNode, @@ -536,11 +536,11 @@ def node_compat_sdk2209(): ARM_UL_TreeVarList, ARM_PG_TreeVarListItem, ) -register_classes, unregister_classes = bpy.utils.register_classes_factory(REG_CLASSES) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): - register_classes() + __reg_classes() bpy.types.Node.arm_logic_id = StringProperty( name='ID', @@ -554,4 +554,4 @@ def register(): def unregister(): - unregister_classes() + __unreg_classes() diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 9b16a18d1f..998469e957 100644 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -311,7 +311,7 @@ def execute(self, context): def poll(cls, context): return context.space_data is not None and context.space_data.type == 'NODE_EDITOR' -class ARM_UL_interface_sockets(bpy.types.UIList): +class ARM_UL_InterfaceSockets(bpy.types.UIList): """UI List of input and output sockets""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): socket = item @@ -356,26 +356,32 @@ def unregister_draw(cls): bpy.types.SpaceNodeEditor.draw_handler_remove(cls.draw_handler, 'WINDOW') cls.draw_handler = None + +__REG_CLASSES = ( + ArmLogicTree, + ArmOpenNodeHaxeSource, + ArmOpenNodePythonSource, + ArmOpenNodeWikiEntry, + ARM_OT_ReplaceNodesOperator, + ARM_MT_NodeAddOverride, + ARM_OT_AddNodeOverride, + ARM_UL_InterfaceSockets, + ARM_PT_LogicNodePanel, + ARM_PT_NodeDevelopment +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): arm.logicnode.arm_nodes.register() arm.logicnode.arm_sockets.register() arm.logicnode.arm_node_group.register() + arm.logicnode.tree_variables.register() - bpy.utils.register_class(ArmLogicTree) - bpy.utils.register_class(ArmOpenNodeHaxeSource) - bpy.utils.register_class(ArmOpenNodePythonSource) - bpy.utils.register_class(ArmOpenNodeWikiEntry) - bpy.utils.register_class(ARM_OT_ReplaceNodesOperator) ARM_MT_NodeAddOverride.overridden_menu = bpy.types.NODE_MT_add ARM_MT_NodeAddOverride.overridden_draw = bpy.types.NODE_MT_add.draw - bpy.utils.register_class(ARM_MT_NodeAddOverride) - bpy.utils.register_class(ARM_OT_AddNodeOverride) - bpy.utils.register_class(ARM_UL_interface_sockets) - # Register panels in correct order - bpy.utils.register_class(ARM_PT_LogicNodePanel) - arm.logicnode.tree_variables.register() - bpy.utils.register_class(ARM_PT_NodeDevelopment) + __reg_classes() arm.logicnode.init_categories() DrawNodeBreadCrumbs.register_draw() @@ -388,20 +394,10 @@ def unregister(): # Ensure that globals are reset if the addon is enabled again in the same Blender session arm_nodes.reset_globals() - bpy.utils.unregister_class(ARM_PT_NodeDevelopment) - arm.logicnode.tree_variables.unregister() - bpy.utils.unregister_class(ARM_PT_LogicNodePanel) - - bpy.utils.unregister_class(ARM_OT_ReplaceNodesOperator) - bpy.utils.unregister_class(ArmLogicTree) - bpy.utils.unregister_class(ArmOpenNodeHaxeSource) - bpy.utils.unregister_class(ArmOpenNodePythonSource) - bpy.utils.unregister_class(ArmOpenNodeWikiEntry) - bpy.utils.unregister_class(ARM_OT_AddNodeOverride) - bpy.utils.unregister_class(ARM_MT_NodeAddOverride) + __unreg_classes() bpy.utils.register_class(ARM_MT_NodeAddOverride.overridden_menu) - bpy.utils.unregister_class(ARM_UL_interface_sockets) + arm.logicnode.tree_variables.unregister() arm.logicnode.arm_node_group.unregister() arm.logicnode.arm_sockets.unregister() arm.logicnode.arm_nodes.unregister() diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py index ca26aa3aee..9c71ed2fe5 100644 --- a/blender/arm/props_bake.py +++ b/blender/arm/props_bake.py @@ -350,52 +350,57 @@ def execute(self, context): bpy.data.materials.remove(mat, do_unlink=True) return{'FINISHED'} + +__REG_CLASSES = ( + ArmBakeListItem, + ARM_UL_BakeList, + ArmBakeListNewItem, + ArmBakeListDeleteItem, + ArmBakeListMoveItem, + ArmBakeButton, + ArmBakeApplyButton, + ArmBakeSpecialsMenu, + ArmBakeAddAllButton, + ArmBakeAddSelectedButton, + ArmBakeClearAllButton, + ArmBakeRemoveBakedMaterialsButton +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmBakeListItem) - bpy.utils.register_class(ARM_UL_BakeList) - bpy.utils.register_class(ArmBakeListNewItem) - bpy.utils.register_class(ArmBakeListDeleteItem) - bpy.utils.register_class(ArmBakeListMoveItem) - bpy.utils.register_class(ArmBakeButton) - bpy.utils.register_class(ArmBakeApplyButton) - bpy.utils.register_class(ArmBakeSpecialsMenu) - bpy.utils.register_class(ArmBakeAddAllButton) - bpy.utils.register_class(ArmBakeAddSelectedButton) - bpy.utils.register_class(ArmBakeClearAllButton) - bpy.utils.register_class(ArmBakeRemoveBakedMaterialsButton) - bpy.types.Scene.arm_bakelist_scale = FloatProperty(name="Resolution", description="Resolution scale", default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0, subtype='PERCENTAGE') + __reg_classes() + + bpy.types.Scene.arm_bakelist_scale = FloatProperty( + name="Resolution", description="Resolution scale", subtype='PERCENTAGE', + default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0 + ) bpy.types.Scene.arm_bakelist = CollectionProperty(type=ArmBakeListItem) bpy.types.Scene.arm_bakelist_index = IntProperty(name="Index for my_list", default=0) bpy.types.Scene.arm_bakelist_unwrap = EnumProperty( - items = [('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), - ('Smart UV Project', 'Smart UV Project', 'Smart UV Project')], - name = "UV Unwrap", default='Smart UV Project') - - - #Register lightmapper + name="UV Unwrap", default='Smart UV Project', + items=[ + ('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), + ('Smart UV Project', 'Smart UV Project', 'Smart UV Project') + ] + ) + + # Register lightmapper bpy.types.Scene.arm_bakemode = EnumProperty( - items = [('Static Map', 'Static Map', 'Static Map'), - ('Lightmap', 'Lightmap', 'Lightmap')], - name = "Bake mode", default='Static Map') + name="Bake mode", default='Static Map', + items=[ + ('Static Map', 'Static Map', 'Static Map'), + ('Lightmap', 'Lightmap', 'Lightmap') + ] + ) operators.register() properties.register() + def unregister(): - bpy.utils.unregister_class(ArmBakeListItem) - bpy.utils.unregister_class(ARM_UL_BakeList) - bpy.utils.unregister_class(ArmBakeListNewItem) - bpy.utils.unregister_class(ArmBakeListDeleteItem) - bpy.utils.unregister_class(ArmBakeListMoveItem) - bpy.utils.unregister_class(ArmBakeButton) - bpy.utils.unregister_class(ArmBakeApplyButton) - bpy.utils.unregister_class(ArmBakeSpecialsMenu) - bpy.utils.unregister_class(ArmBakeAddAllButton) - bpy.utils.unregister_class(ArmBakeAddSelectedButton) - bpy.utils.unregister_class(ArmBakeClearAllButton) - bpy.utils.unregister_class(ArmBakeRemoveBakedMaterialsButton) - - #Unregister lightmapper + __unreg_classes() + # Unregister lightmapper operators.unregister() - properties.unregister() \ No newline at end of file + properties.unregister() diff --git a/blender/arm/props_exporter.py b/blender/arm/props_exporter.py index f8620a8952..9384176090 100644 --- a/blender/arm/props_exporter.py +++ b/blender/arm/props_exporter.py @@ -451,7 +451,7 @@ def execute(self, context): return{'FINISHED'} -REG_CLASSES = ( +__REG_CLASSES = ( ArmExporterListItem, ArmExporterAndroidPermissionListItem, ArmExporterAndroidAbiListItem, @@ -470,11 +470,11 @@ def execute(self, context): ArmoryExporterOpenFolderButton, ARM_OT_ExporterOpenVS ) -_reg_classes, _unreg_classes = bpy.utils.register_classes_factory(REG_CLASSES) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): - _reg_classes() + __reg_classes() bpy.types.World.arm_exporterlist = CollectionProperty(type=ArmExporterListItem) bpy.types.World.arm_exporterlist_index = IntProperty(name="Index for my_list", default=0) @@ -485,4 +485,4 @@ def register(): def unregister(): - _unreg_classes() + __unreg_classes() diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index 2ad67bf2f0..66da8cc4d9 100644 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -138,20 +138,20 @@ def execute(self, context): return{'CANCELLED'} return{'FINISHED'} + +__REG_CLASSES = ( + ArmLodListItem, + ARM_UL_LodList, + ArmLodListNewItem, + ArmLodListDeleteItem, + ArmLodListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmLodListItem) - bpy.utils.register_class(ARM_UL_LodList) - bpy.utils.register_class(ArmLodListNewItem) - bpy.utils.register_class(ArmLodListDeleteItem) - bpy.utils.register_class(ArmLodListMoveItem) + __reg_classes() bpy.types.Mesh.arm_lodlist = CollectionProperty(type=ArmLodListItem) bpy.types.Mesh.arm_lodlist_index = IntProperty(name="Index for my_list", default=0) bpy.types.Mesh.arm_lod_material = BoolProperty(name="Material Lod", description="Use materials of lod objects", default=False) - -def unregister(): - bpy.utils.unregister_class(ArmLodListItem) - bpy.utils.unregister_class(ARM_UL_LodList) - bpy.utils.unregister_class(ArmLodListNewItem) - bpy.utils.unregister_class(ArmLodListDeleteItem) - bpy.utils.unregister_class(ArmLodListMoveItem) diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py index 510e10eb5f..b7fb24ed20 100644 --- a/blender/arm/props_properties.py +++ b/blender/arm/props_properties.py @@ -141,18 +141,18 @@ def draw_properties(layout, obj): op = col.operator("arm_propertylist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' + +__REG_CLASSES = ( + ArmPropertyListItem, + ARM_UL_PropertyList, + ArmPropertyListNewItem, + ArmPropertyListDeleteItem, + ArmPropertyListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmPropertyListItem) - bpy.utils.register_class(ARM_UL_PropertyList) - bpy.utils.register_class(ArmPropertyListNewItem) - bpy.utils.register_class(ArmPropertyListDeleteItem) - bpy.utils.register_class(ArmPropertyListMoveItem) + __reg_classes() bpy.types.Object.arm_propertylist = CollectionProperty(type=ArmPropertyListItem) bpy.types.Object.arm_propertylist_index = IntProperty(name="Index for arm_propertylist", default=0) - -def unregister(): - bpy.utils.unregister_class(ArmPropertyListItem) - bpy.utils.unregister_class(ARM_UL_PropertyList) - bpy.utils.unregister_class(ArmPropertyListNewItem) - bpy.utils.unregister_class(ArmPropertyListDeleteItem) - bpy.utils.unregister_class(ArmPropertyListMoveItem) diff --git a/blender/arm/props_renderpath.py b/blender/arm/props_renderpath.py index 7917bda535..8309a26807 100644 --- a/blender/arm/props_renderpath.py +++ b/blender/arm/props_renderpath.py @@ -738,21 +738,19 @@ def execute(self, context): return{'FINISHED'} +__REG_CLASSES = ( + ArmRPListItem, + ARM_UL_RPList, + ArmRPListNewItem, + ArmRPListDeleteItem, + ArmRPListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmRPListItem) - bpy.utils.register_class(ARM_UL_RPList) - bpy.utils.register_class(ArmRPListNewItem) - bpy.utils.register_class(ArmRPListDeleteItem) - bpy.utils.register_class(ArmRPListMoveItem) + __reg_classes() bpy.types.World.arm_rplist = CollectionProperty(type=ArmRPListItem) bpy.types.World.rp_driver_list = CollectionProperty(type=bpy.types.PropertyGroup) bpy.types.World.arm_rplist_index = IntProperty(name="Index for my_list", default=0, update=update_renderpath) - - -def unregister(): - bpy.utils.unregister_class(ArmRPListItem) - bpy.utils.unregister_class(ARM_UL_RPList) - bpy.utils.unregister_class(ArmRPListNewItem) - bpy.utils.unregister_class(ArmRPListDeleteItem) - bpy.utils.unregister_class(ArmRPListMoveItem) diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py index 1f7d53b6db..d625087d3b 100644 --- a/blender/arm/props_tilesheet.py +++ b/blender/arm/props_tilesheet.py @@ -252,31 +252,25 @@ def execute(self, context): return{'CANCELLED'} return{'FINISHED'} + +__REG_CLASSES = ( + ArmTilesheetActionListItem, + ARM_UL_TilesheetActionList, + ArmTilesheetActionListNewItem, + ArmTilesheetActionListDeleteItem, + ArmTilesheetActionListMoveItem, + + ArmTilesheetListItem, + ARM_UL_TilesheetList, + ArmTilesheetListNewItem, + ArmTilesheetListDeleteItem, + ArmTilesheetListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmTilesheetActionListItem) - bpy.utils.register_class(ARM_UL_TilesheetActionList) - bpy.utils.register_class(ArmTilesheetActionListNewItem) - bpy.utils.register_class(ArmTilesheetActionListDeleteItem) - bpy.utils.register_class(ArmTilesheetActionListMoveItem) - - bpy.utils.register_class(ArmTilesheetListItem) - bpy.utils.register_class(ARM_UL_TilesheetList) - bpy.utils.register_class(ArmTilesheetListNewItem) - bpy.utils.register_class(ArmTilesheetListDeleteItem) - bpy.utils.register_class(ArmTilesheetListMoveItem) + __reg_classes() bpy.types.World.arm_tilesheetlist = CollectionProperty(type=ArmTilesheetListItem) bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for arm_tilesheetlist", default=0) - -def unregister(): - bpy.utils.unregister_class(ArmTilesheetListItem) - bpy.utils.unregister_class(ARM_UL_TilesheetList) - bpy.utils.unregister_class(ArmTilesheetListNewItem) - bpy.utils.unregister_class(ArmTilesheetListDeleteItem) - bpy.utils.unregister_class(ArmTilesheetListMoveItem) - - bpy.utils.unregister_class(ArmTilesheetActionListItem) - bpy.utils.unregister_class(ARM_UL_TilesheetActionList) - bpy.utils.unregister_class(ArmTilesheetActionListNewItem) - bpy.utils.unregister_class(ArmTilesheetActionListDeleteItem) - bpy.utils.unregister_class(ArmTilesheetActionListMoveItem) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 566ca5a455..de91282963 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -954,59 +954,40 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b row = layout.row() row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + +__REG_CLASSES = ( + ArmTraitListItem, + ARM_UL_TraitList, + ArmTraitListNewItem, + ArmTraitListDeleteItem, + ArmTraitListDeleteItemScene, + ArmTraitListMoveItem, + ArmEditScriptButton, + ArmEditBundledScriptButton, + ArmEditWasmScriptButton, + ArmoryGenerateNavmeshButton, + ArmEditCanvasButton, + ArmNewScriptDialog, + ArmNewTreeNodeDialog, + ArmEditTreeNodeDialog, + ArmGetTreeNodeDialog, + ArmNewCanvasDialog, + ArmNewWasmButton, + ArmRefreshScriptsButton, + ArmRefreshObjectScriptsButton, + ArmRefreshCanvasListButton, + ARM_PT_TraitPanel, + ARM_PT_SceneTraitPanel, + ARM_OT_CopyTraitsFromActive, + ARM_MT_context_menu, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmTraitListItem) - bpy.utils.register_class(ARM_UL_TraitList) - bpy.utils.register_class(ArmTraitListNewItem) - bpy.utils.register_class(ArmTraitListDeleteItem) - bpy.utils.register_class(ArmTraitListDeleteItemScene) - bpy.utils.register_class(ArmTraitListMoveItem) - bpy.utils.register_class(ArmEditScriptButton) - bpy.utils.register_class(ArmEditBundledScriptButton) - bpy.utils.register_class(ArmEditWasmScriptButton) - bpy.utils.register_class(ArmoryGenerateNavmeshButton) - bpy.utils.register_class(ArmEditCanvasButton) - bpy.utils.register_class(ArmNewScriptDialog) - bpy.utils.register_class(ArmNewTreeNodeDialog) - bpy.utils.register_class(ArmEditTreeNodeDialog) - bpy.utils.register_class(ArmGetTreeNodeDialog) - bpy.utils.register_class(ArmNewCanvasDialog) - bpy.utils.register_class(ArmNewWasmButton) - bpy.utils.register_class(ArmRefreshScriptsButton) - bpy.utils.register_class(ArmRefreshObjectScriptsButton) - bpy.utils.register_class(ArmRefreshCanvasListButton) - bpy.utils.register_class(ARM_PT_TraitPanel) - bpy.utils.register_class(ARM_PT_SceneTraitPanel) - bpy.utils.register_class(ARM_OT_CopyTraitsFromActive) - bpy.utils.register_class(ARM_MT_context_menu) + __reg_classes() bpy.types.Object.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) bpy.types.Object.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) bpy.types.Scene.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) bpy.types.Scene.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - -def unregister(): - bpy.utils.unregister_class(ARM_OT_CopyTraitsFromActive) - bpy.utils.unregister_class(ARM_MT_context_menu) - bpy.utils.unregister_class(ArmTraitListItem) - bpy.utils.unregister_class(ARM_UL_TraitList) - bpy.utils.unregister_class(ArmTraitListNewItem) - bpy.utils.unregister_class(ArmTraitListDeleteItem) - bpy.utils.unregister_class(ArmTraitListDeleteItemScene) - bpy.utils.unregister_class(ArmTraitListMoveItem) - bpy.utils.unregister_class(ArmEditScriptButton) - bpy.utils.unregister_class(ArmEditBundledScriptButton) - bpy.utils.unregister_class(ArmEditWasmScriptButton) - bpy.utils.unregister_class(ArmoryGenerateNavmeshButton) - bpy.utils.unregister_class(ArmEditCanvasButton) - bpy.utils.unregister_class(ArmNewScriptDialog) - bpy.utils.unregister_class(ArmGetTreeNodeDialog) - bpy.utils.unregister_class(ArmEditTreeNodeDialog) - bpy.utils.unregister_class(ArmNewTreeNodeDialog) - bpy.utils.unregister_class(ArmNewCanvasDialog) - bpy.utils.unregister_class(ArmNewWasmButton) - bpy.utils.unregister_class(ArmRefreshObjectScriptsButton) - bpy.utils.unregister_class(ArmRefreshScriptsButton) - bpy.utils.unregister_class(ArmRefreshCanvasListButton) - bpy.utils.unregister_class(ARM_PT_TraitPanel) - bpy.utils.unregister_class(ARM_PT_SceneTraitPanel) diff --git a/blender/arm/props_traits_props.py b/blender/arm/props_traits_props.py index 126d01582e..8e353f2d25 100644 --- a/blender/arm/props_traits_props.py +++ b/blender/arm/props_traits_props.py @@ -152,13 +152,9 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn layout.alignment = 'CENTER' -def register(): - bpy.utils.register_class(ArmTraitPropWarning) - bpy.utils.register_class(ArmTraitPropListItem) - bpy.utils.register_class(ARM_UL_PropList) - - -def unregister(): - bpy.utils.unregister_class(ARM_UL_PropList) - bpy.utils.unregister_class(ArmTraitPropListItem) - bpy.utils.unregister_class(ArmTraitPropWarning) +__REG_CLASSES = ( + ArmTraitPropWarning, + ArmTraitPropListItem, + ARM_UL_PropList, +) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 45313c16be..b6a21424a1 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -2744,80 +2744,85 @@ def draw_multiline_with_icon(layout: bpy.types.UILayout, layout_width_px: int, i return col +__REG_CLASSES = ( + ARM_PT_ObjectPropsPanel, + ARM_PT_ModifiersPropsPanel, + ARM_PT_ParticlesPropsPanel, + ARM_PT_PhysicsPropsPanel, + ARM_PT_DataPropsPanel, + ARM_PT_ScenePropsPanel, + ARM_PT_WorldPropsPanel, + InvalidateCacheButton, + InvalidateMaterialCacheButton, + ARM_OT_NewCustomMaterial, + ARM_PG_BindTexturesListItem, + ARM_UL_BindTexturesList, + ARM_OT_BindTexturesListNewItem, + ARM_OT_BindTexturesListDeleteItem, + ARM_PT_MaterialPropsPanel, + ARM_PT_BindTexturesPropsPanel, + ARM_PT_MaterialBlendingPropsPanel, + ARM_PT_MaterialDriverPropsPanel, + ARM_PT_ArmoryPlayerPanel, + ARM_PT_TopbarPanel, + ARM_PT_ArmoryExporterPanel, + ARM_PT_ArmoryExporterAndroidSettingsPanel, + ARM_PT_ArmoryExporterAndroidPermissionsPanel, + ARM_PT_ArmoryExporterAndroidAbiPanel, + ARM_PT_ArmoryExporterAndroidBuildAPKPanel, + ARM_PT_ArmoryExporterHTML5SettingsPanel, + ARM_PT_ArmoryExporterWindowsSettingsPanel, + ARM_PT_ArmoryProjectPanel, + ARM_PT_ProjectFlagsPanel, + ARM_PT_ProjectFlagsDebugConsolePanel, + ARM_PT_ProjectWindowPanel, + ARM_PT_ProjectModulesPanel, + ARM_PT_RenderPathPanel, + ARM_PT_RenderPathRendererPanel, + ARM_PT_RenderPathShadowsPanel, + ARM_PT_RenderPathVoxelsPanel, + ARM_PT_RenderPathWorldPanel, + ARM_PT_RenderPathPostProcessPanel, + ARM_PT_RenderPathCompositorPanel, + ARM_PT_BakePanel, + # ArmVirtualInputPanel, + ArmoryPlayButton, + ArmoryStopButton, + ArmoryBuildProjectButton, + ArmoryOpenProjectFolderButton, + ArmoryOpenEditorButton, + CleanMenu, + CleanButtonMenu, + ArmoryCleanProjectButton, + ArmoryPublishProjectButton, + ArmGenLodButton, + ARM_PT_LodPanel, + ArmGenTerrainButton, + ARM_PT_TerrainPanel, + ARM_PT_TilesheetPanel, + ArmPrintTraitsButton, + ARM_PT_MaterialNodePanel, + ARM_OT_UpdateFileSDK, + ARM_OT_CopyToBundled, + ARM_OT_ShowFileVersionInfo, + ARM_OT_ShowNodeUpdateErrors, + ARM_OT_DiscardPopup, + ArmoryUpdateListAndroidEmulatorButton, + ArmoryUpdateListAndroidEmulatorRunButton, + ArmoryUpdateListInstalledVSButton, + ARM_PT_BulletDebugDrawingPanel, + scene.TLM_PT_Settings, + scene.TLM_PT_Denoise, + scene.TLM_PT_Filtering, + scene.TLM_PT_Encoding, + scene.TLM_PT_Utility, + scene.TLM_PT_Additional, +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ARM_PT_ObjectPropsPanel) - bpy.utils.register_class(ARM_PT_ModifiersPropsPanel) - bpy.utils.register_class(ARM_PT_ParticlesPropsPanel) - bpy.utils.register_class(ARM_PT_PhysicsPropsPanel) - bpy.utils.register_class(ARM_PT_DataPropsPanel) - bpy.utils.register_class(ARM_PT_ScenePropsPanel) - bpy.utils.register_class(ARM_PT_WorldPropsPanel) - bpy.utils.register_class(InvalidateCacheButton) - bpy.utils.register_class(InvalidateMaterialCacheButton) - bpy.utils.register_class(ARM_OT_NewCustomMaterial) - bpy.utils.register_class(ARM_PG_BindTexturesListItem) - bpy.utils.register_class(ARM_UL_BindTexturesList) - bpy.utils.register_class(ARM_OT_BindTexturesListNewItem) - bpy.utils.register_class(ARM_OT_BindTexturesListDeleteItem) - bpy.utils.register_class(ARM_PT_MaterialPropsPanel) - bpy.utils.register_class(ARM_PT_BindTexturesPropsPanel) - bpy.utils.register_class(ARM_PT_MaterialBlendingPropsPanel) - bpy.utils.register_class(ARM_PT_MaterialDriverPropsPanel) - bpy.utils.register_class(ARM_PT_ArmoryPlayerPanel) - bpy.utils.register_class(ARM_PT_TopbarPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidAbiPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryProjectPanel) - bpy.utils.register_class(ARM_PT_ProjectFlagsPanel) - bpy.utils.register_class(ARM_PT_ProjectFlagsDebugConsolePanel) - bpy.utils.register_class(ARM_PT_ProjectWindowPanel) - bpy.utils.register_class(ARM_PT_ProjectModulesPanel) - bpy.utils.register_class(ARM_PT_RenderPathPanel) - bpy.utils.register_class(ARM_PT_RenderPathRendererPanel) - bpy.utils.register_class(ARM_PT_RenderPathShadowsPanel) - bpy.utils.register_class(ARM_PT_RenderPathVoxelsPanel) - bpy.utils.register_class(ARM_PT_RenderPathWorldPanel) - bpy.utils.register_class(ARM_PT_RenderPathPostProcessPanel) - bpy.utils.register_class(ARM_PT_RenderPathCompositorPanel) - bpy.utils.register_class(ARM_PT_BakePanel) - # bpy.utils.register_class(ArmVirtualInputPanel) - bpy.utils.register_class(ArmoryPlayButton) - bpy.utils.register_class(ArmoryStopButton) - bpy.utils.register_class(ArmoryBuildProjectButton) - bpy.utils.register_class(ArmoryOpenProjectFolderButton) - bpy.utils.register_class(ArmoryOpenEditorButton) - bpy.utils.register_class(CleanMenu) - bpy.utils.register_class(CleanButtonMenu) - bpy.utils.register_class(ArmoryCleanProjectButton) - bpy.utils.register_class(ArmoryPublishProjectButton) - bpy.utils.register_class(ArmGenLodButton) - bpy.utils.register_class(ARM_PT_LodPanel) - bpy.utils.register_class(ArmGenTerrainButton) - bpy.utils.register_class(ARM_PT_TerrainPanel) - bpy.utils.register_class(ARM_PT_TilesheetPanel) - bpy.utils.register_class(ArmPrintTraitsButton) - bpy.utils.register_class(ARM_PT_MaterialNodePanel) - bpy.utils.register_class(ARM_OT_UpdateFileSDK) - bpy.utils.register_class(ARM_OT_CopyToBundled) - bpy.utils.register_class(ARM_OT_ShowFileVersionInfo) - bpy.utils.register_class(ARM_OT_ShowNodeUpdateErrors) - bpy.utils.register_class(ARM_OT_DiscardPopup) - bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorButton) - bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) - bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) - bpy.utils.register_class(ARM_PT_BulletDebugDrawingPanel) - - bpy.utils.register_class(scene.TLM_PT_Settings) - bpy.utils.register_class(scene.TLM_PT_Denoise) - bpy.utils.register_class(scene.TLM_PT_Filtering) - bpy.utils.register_class(scene.TLM_PT_Encoding) - bpy.utils.register_class(scene.TLM_PT_Utility) - bpy.utils.register_class(scene.TLM_PT_Additional) + __reg_classes() bpy.types.VIEW3D_HT_header.append(draw_view3d_header) bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) @@ -2827,82 +2832,11 @@ def register(): bpy.types.Material.arm_bind_textures_list = CollectionProperty(type=ARM_PG_BindTexturesListItem) bpy.types.Material.arm_bind_textures_list_index = IntProperty(name='Index for arm_bind_textures_list', default=0) + def unregister(): bpy.types.NODE_MT_context_menu.remove(draw_custom_node_menu) bpy.types.VIEW3D_MT_object.remove(draw_view3d_object_menu) bpy.types.VIEW3D_HT_header.remove(draw_view3d_header) bpy.types.TOPBAR_HT_upper_bar.remove(draw_space_topbar) - bpy.utils.unregister_class(ARM_PT_BulletDebugDrawingPanel) - bpy.utils.unregister_class(ArmoryUpdateListInstalledVSButton) - bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorRunButton) - bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorButton) - bpy.utils.unregister_class(ARM_OT_DiscardPopup) - bpy.utils.unregister_class(ARM_OT_ShowNodeUpdateErrors) - bpy.utils.unregister_class(ARM_OT_CopyToBundled) - bpy.utils.unregister_class(ARM_OT_ShowFileVersionInfo) - bpy.utils.unregister_class(ARM_OT_UpdateFileSDK) - bpy.utils.unregister_class(ARM_PT_ObjectPropsPanel) - bpy.utils.unregister_class(ARM_PT_ModifiersPropsPanel) - bpy.utils.unregister_class(ARM_PT_ParticlesPropsPanel) - bpy.utils.unregister_class(ARM_PT_PhysicsPropsPanel) - bpy.utils.unregister_class(ARM_PT_DataPropsPanel) - bpy.utils.unregister_class(ARM_PT_WorldPropsPanel) - bpy.utils.unregister_class(ARM_PT_ScenePropsPanel) - bpy.utils.unregister_class(InvalidateCacheButton) - bpy.utils.unregister_class(InvalidateMaterialCacheButton) - bpy.utils.unregister_class(ARM_OT_NewCustomMaterial) - bpy.utils.unregister_class(ARM_PT_MaterialDriverPropsPanel) - bpy.utils.unregister_class(ARM_PT_MaterialBlendingPropsPanel) - bpy.utils.unregister_class(ARM_PT_BindTexturesPropsPanel) - bpy.utils.unregister_class(ARM_PT_MaterialPropsPanel) - bpy.utils.unregister_class(ARM_OT_BindTexturesListDeleteItem) - bpy.utils.unregister_class(ARM_OT_BindTexturesListNewItem) - bpy.utils.unregister_class(ARM_UL_BindTexturesList) - bpy.utils.unregister_class(ARM_PG_BindTexturesListItem) - bpy.utils.unregister_class(ARM_PT_ArmoryPlayerPanel) - bpy.utils.unregister_class(ARM_PT_TopbarPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidAbiPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryProjectPanel) - bpy.utils.unregister_class(ARM_PT_ProjectFlagsDebugConsolePanel) - bpy.utils.unregister_class(ARM_PT_ProjectFlagsPanel) - bpy.utils.unregister_class(ARM_PT_ProjectWindowPanel) - bpy.utils.unregister_class(ARM_PT_ProjectModulesPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathRendererPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathShadowsPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathVoxelsPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathWorldPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathPostProcessPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathCompositorPanel) - bpy.utils.unregister_class(ARM_PT_BakePanel) - # bpy.utils.unregister_class(ArmVirtualInputPanel) - bpy.utils.unregister_class(ArmoryPlayButton) - bpy.utils.unregister_class(ArmoryStopButton) - bpy.utils.unregister_class(ArmoryBuildProjectButton) - bpy.utils.unregister_class(ArmoryOpenProjectFolderButton) - bpy.utils.unregister_class(ArmoryOpenEditorButton) - bpy.utils.unregister_class(CleanMenu) - bpy.utils.unregister_class(CleanButtonMenu) - bpy.utils.unregister_class(ArmoryCleanProjectButton) - bpy.utils.unregister_class(ArmoryPublishProjectButton) - bpy.utils.unregister_class(ArmGenLodButton) - bpy.utils.unregister_class(ARM_PT_LodPanel) - bpy.utils.unregister_class(ArmGenTerrainButton) - bpy.utils.unregister_class(ARM_PT_TerrainPanel) - bpy.utils.unregister_class(ARM_PT_TilesheetPanel) - bpy.utils.unregister_class(ArmPrintTraitsButton) - bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) - - bpy.utils.unregister_class(scene.TLM_PT_Settings) - bpy.utils.unregister_class(scene.TLM_PT_Denoise) - bpy.utils.unregister_class(scene.TLM_PT_Filtering) - bpy.utils.unregister_class(scene.TLM_PT_Encoding) - bpy.utils.unregister_class(scene.TLM_PT_Utility) - bpy.utils.unregister_class(scene.TLM_PT_Additional) + __unreg_classes() From e736e7f6a97375578c632deda2c1ba5fc6302709 Mon Sep 17 00:00:00 2001 From: Yvain Douard Date: Sun, 13 Aug 2023 17:16:59 +0200 Subject: [PATCH 048/175] initialize blue channel of gbuffer2 --- blender/arm/material/make_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 0cd6d3d118..5490fa852a 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -259,7 +259,7 @@ def make_deferred(con_mesh, rpasses): frag.write('vec2 posa = (wvpposition.xy / wvpposition.w) * 0.5 + 0.5;') frag.write('vec2 posb = (prevwvpposition.xy / prevwvpposition.w) * 0.5 + 0.5;') frag.write('fragColor[GBUF_IDX_2].rg = vec2(posa - posb);') - + frag.write('fragColor[GBUF_IDX_2].b = 0.0;') if mat_state.material.arm_ignore_irradiance: frag.write('fragColor[GBUF_IDX_2].b = 1.0;') From 82a941dbd9bf67d46596ba717aa20329b8a0b1a5 Mon Sep 17 00:00:00 2001 From: Yvain Douard Date: Sun, 13 Aug 2023 17:30:24 +0200 Subject: [PATCH 049/175] space. --- blender/arm/material/make_mesh.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 5490fa852a..47ca06ea05 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -260,6 +260,7 @@ def make_deferred(con_mesh, rpasses): frag.write('vec2 posb = (prevwvpposition.xy / prevwvpposition.w) * 0.5 + 0.5;') frag.write('fragColor[GBUF_IDX_2].rg = vec2(posa - posb);') frag.write('fragColor[GBUF_IDX_2].b = 0.0;') + if mat_state.material.arm_ignore_irradiance: frag.write('fragColor[GBUF_IDX_2].b = 1.0;') From 48a7c0a27765439f61221bf578e2281645dcf4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:08:48 +0200 Subject: [PATCH 050/175] Fix shader compilation for some node names with special characters --- blender/arm/material/cycles.py | 15 +++++++++++---- blender/arm/utils.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 8b92b56dc0..c56be8ff2f 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -538,7 +538,10 @@ def is_parsed(node_store_name: str): def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: """Return the name of the variable that stores the parsed result from the given node and socket.""" - return node_name(node.name) + '_' + safesrc(socket.name) + '_res' + name = node_name(node.name) + '_' + safesrc(socket.name) + '_res' + if '__' in name: # Consecutive _ are reserved + name = name.replace('_', '_x') + return name def write_result(link: bpy.types.NodeLink) -> Optional[str]: @@ -600,8 +603,12 @@ def to_uniform(inp: bpy.types.NodeSocket): state.curshader.add_uniform(glsl_type(inp.type) + ' ' + uname) return uname -def store_var_name(node: bpy.types.Node): - return node_name(node.name) + '_store' + +def store_var_name(node: bpy.types.Node) -> str: + name = node_name(node.name) + if name[-1] == "_": + return name + '_x_store' # Prevent consecutive __ + return name + '_store' def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_arm_mat_param=None): @@ -772,7 +779,7 @@ def node_name(s: str) -> str: if state.curshader.write_textures > 0: s += '_texread' s = safesrc(s) - if '__' in s: # Consecutive _ are reserved + if '__' in s: # Consecutive _ are reserved s = s.replace('_', '_x') return s diff --git a/blender/arm/utils.py b/blender/arm/utils.py index ce5c9b04f9..c6a2b3cf45 100644 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -727,7 +727,7 @@ def safesrc(s): def safestr(s: str) -> str: """Outputs a string where special characters have been replaced with '_', which can be safely used in file and path names.""" - for c in r'''[]/\;,><&*:%=+@!#^()|?^'"''': + for c in r'''[]/\;,><&*:§$%=+@!#^()|?^'"''': s = s.replace(c, '_') return ''.join([i if ord(i) < 128 else '_' for i in s]) From bd61dd3d796279cddc523ad345ec77ddb40f3807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:36:21 +0200 Subject: [PATCH 051/175] Clear warnings and errors when loading other blend files --- blender/arm/handlers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py index ca9720180c..8743a9e770 100644 --- a/blender/arm/handlers.py +++ b/blender/arm/handlers.py @@ -174,6 +174,7 @@ def on_save_pre(context): @persistent def on_load_pre(context): unload_py_libraries() + log.clear(clear_warnings=True, clear_errors=True) @persistent From b79a74b178278cb95c5cb2950603055190bcbc3d Mon Sep 17 00:00:00 2001 From: Sylvio Sell Date: Fri, 25 Aug 2023 22:57:23 +0200 Subject: [PATCH 052/175] better param-bindings and upgrading to formula haxelib version 0.4.2 --- .../armory/logicnode/MathExpressionNode.hx | 3278 +++++++++-------- 1 file changed, 1707 insertions(+), 1571 deletions(-) diff --git a/Sources/armory/logicnode/MathExpressionNode.hx b/Sources/armory/logicnode/MathExpressionNode.hx index b2c4e39644..08f046b71b 100644 --- a/Sources/armory/logicnode/MathExpressionNode.hx +++ b/Sources/armory/logicnode/MathExpressionNode.hx @@ -1,735 +1,1043 @@ package armory.logicnode; + import haxe.io.Bytes; import haxe.io.BytesInput; import haxe.io.BytesOutput; -/** - * extending TermNode with various math operations transformation and more. - * by Sylvio Sell, Rostock 2017 - * - **/ +class MathExpressionNode extends LogicNode { -class TermTransform { + public var property0: String; // Expression + public var property1: Bool; // Clamp - static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; - static var newValue:Float->TermNode = TermNode.newValue; - - /* - * Simplify: trims the length of a math expression - * - */ - static public inline function simplify(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _simplify(tnew); - return tnew; - } - - static inline function _simplify(t:TermNode):Void { - _expand(t); - - var len:Int = -1; - var len_old:Int = 0; - while (len != len_old) { - if (t.isName && t.left != null) { - simplifyStep(t.left); - } - else { - simplifyStep(t); - } - len_old = len; - len = t.length(); - } + public function new(tree: LogicTree) { + super(tree); } + - // TODO: removing this calls in subfunctions could be better to understand algorithms-recursions !!! - static function isEqualAfterSimplify(t1:TermNode, t2:TermNode):Bool { - // ----> take care, _simplify changes both TermNodes on call !!! - _simplify(t1); - _simplify(t2); - return t1.isEqual(t2, false, true); - } + static inline var paramNames = "abcdexyhijk"; - static function simplifyStep(t:TermNode):Void { - if (!t.isOperation) return; - - if (t.left != null) { - if (t.left.isValue) { - if (t.right == null) { - // setValue(result); // calculate operation with one value - return; - } - else if (t.right.isValue) { - t.setValue(t.result); // calculate result of operation with values on both sides - return; - } + override function get(from: Int): Dynamic { + var result:Float = 0.0; + var expression:String = property0; + + // Expression + try { + var formula:Formula = new Formula(expression); + + // bind variables + for (i in 0...inputs.length) { + formula.bind( (inputs[i].get():Float), paramNames.substr(i, 1) ); } - } + + result = formula.result; + + } catch(msg: String) { + #if arm_debug + trace(msg); + #end + } + + // Clamp + if (property1) result = result < 0.0 ? 0.0 : (result > 1.0 ? 1.0 : result); - switch(t.symbol) { - case '+': - if (t.left.isValue && t.left.value == 0) t.copyNodeFrom(t.right); // 0+a -> a - else if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a+0 -> a - else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)+ln(b) -> ln(a*b) - t.setOperation('ln', - newOperation('*', t.left.left.copy(), t.right.left.copy()) - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { - t.setOperation('/', // a/b+c/b -> (a+c)/b - newOperation('+', t.left.left.copy(), t.right.left.copy()), - t.left.right.copy() - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/') { // a/b+c/d -> (a*d+c*b)/(b*d) - t.setOperation('/', - newOperation('+', - newOperation('*', t.left.left.copy(), t.right.right.copy()), - newOperation('*', t.right.left.copy(), t.left.right.copy()) - ), - newOperation('*', t.left.right.copy(), t.right.right.copy()) - ); - } - arrangeAddition(t); - if(t.symbol == '+') { - _factorize(t); - } + return result; + } +} - case '-': - if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a-0 -> a - else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)-ln(b) -> ln(a/b) - t.setOperation('ln', - newOperation('/', t.left.left.copy(), t.right.left.copy()) - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { - t.setOperation('/', // a/b-c/b -> (a-c)/b - newOperation('-', t.left.left.copy(), t.right.left.copy()), - t.left.right.copy() - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/') { //a/b-c/d -> (a*d-c*b)/(b*d) - t.setOperation('/', - newOperation('-', - newOperation('*', t.left.left.copy(), t.right.right.copy()), - newOperation('*', t.right.left.copy(), t.left.right.copy()) - ), - newOperation('*', t.left.right.copy(), t.right.right.copy()) - ); - } - arrangeAddition(t); - if(t.symbol == '-') { - _factorize(t); - } - case '*': - if (t.left.isValue) { - if (t.left.value == 1) t.copyNodeFrom(t.right); // 1*a -> a - else if (t.left.value == 0) t.setValue(0); // 0*a -> 0 - } - else if (t.right.isValue) { - if (t.right.value == 1) t.copyNodeFrom(t.left); // a*1 -> a - else if (t.right.value == 0) t.setValue(0); // a*0 -> a - } - else if (t.left.symbol == '/') { // (a/b)*c -> (a*c)/b - t.setOperation('/', - newOperation('*', t.right.copy(), t.left.left.copy()), - t.left.right.copy() - ); - } - else if (t.right.symbol == '/') { // a*(b/c) -> (a*b)/c - t.setOperation('/', - newOperation('*', t.left.copy(), t.right.left.copy()), - t.right.right.copy() - ); - } - else { - arrangeMultiplication(t); - } +// -------------------------------------------------------------------- +// ------------------------ FORMULA ----------------------------------- +// -------------------------------------------------------------------- - case '/': - if (isEqualAfterSimplify(t.left, t.right)) { // x/x -> 1 - t.setValue(1); - } - else { - if (t.left.isValue && t.left.value == 0) t.setValue(0); // 0/a -> 0 - else if (t.right.symbol == '/') { - t.setOperation('/', - newOperation('*', t.right.right.copy(), t.left.copy()), - t.right.left.copy() - ); - } - else if (t.right.isValue && t.right.value == 1) t.copyNodeFrom(t.left); // a/1 -> a - else if (t.left.symbol == '/') { // (1/x)/b -> 1/(x*b) - t.setOperation('/', t.left.left.copy(), - newOperation('*', t.left.right.copy(), t.right.copy()) - ); - } - else if (t.right.symbol == '/') { // b/(1/x) -> b*x - t.setOperation('/', - newOperation('*', t.right.right.copy(), t.left.copy()), - t.right.left.copy() - ); - } - else if (t.left.symbol == '-' && t.left.left.isValue && t.left.left.value == 0) - { - t.setOperation('-', newValue(0), - newOperation('/', t.left.right.copy(), t.right.copy()) - ); - } - else{ // a*b/b -> a - simplifyfraction(t); - } - } +/* + Formula haxe library to handle mathematical expressions at runtime + Version: 0.4.2 + weblink: https://lib.haxe.org/p/formula/ + by Sylvio Sell, Rostock 2023 +*/ - case '^': - if (t.left.isValue) { - if (t.left.value == 1) t.setValue(1); // 1^a -> 1 - else if (t.left.value == 0) t.setValue(0); // 0^a -> 0 - } else if (t.right.isValue) { - if (t.right.value == 1) t.copyNodeFrom(t.left); // a^1 -> a - else if (t.right.value == 0) t.setValue(1); // a^0 -> 1 - } - else if (t.left.symbol == '^') { // (a^b)^c -> a^(b*c) - t.setOperation('^', t.left.left.copy(), - newOperation('*', t.left.right.copy(), t.right.copy()) - ); - } +@:forward( name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) +abstract Formula(TermNode) from TermNode to TermNode +{ + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - case 'ln': - if (t.left.symbol == 'e') t.setValue(1); - case 'log': - if (isEqualAfterSimplify(t.left, t.right)) { - t.setValue(1); - } - else { - t.setOperation('/', // log(a,b) -> ln(b)/ln(a) - newOperation('ln', t.right.copy()), - newOperation('ln', t.left.copy()) - ); - } - } - if (t.left != null) simplifyStep(t.left); - if (t.right != null) simplifyStep(t.right); + @param formulaString the String that representing the math expression + **/ + inline public function new(formulaString:String) { + this = TermNode.fromString(formulaString); } - - /* - * put all subterms separated by * into an array - * - */ - static function traverseMultiplication(t:TermNode, p:Array) - { - if (t.symbol != "*") { - p.push(t); + + /** + Copy all from another Formula to this (keeps the own name if it is defined) + Keeps the bindings where this formula is linked into by a parameter. + + @param formula the source formula from where the value is copyed + **/ + public inline function set(formula:Formula):Formula return this.set(formula); + + /** + Link a variable inside of this formula to another formula + + @param formula formula that will be linked into + @param paramName (optional) name of the variable to link with (e.g. if formula have no or different name) + **/ + public function bind(formula:Formula, ?paramName:String):Formula { + if (paramName != null) { + TermNode.checkValidName(paramName); + return this.bind( [paramName => formula] ); } else { - traverseMultiplication(t.left, p); - traverseMultiplication(t.right, p); + if (formula.name == null) ErrorMsg.cantBindUnnamed(this, formula); + return this.bind( [formula.name => formula] ); } } - /* - * build tree consisting of multiple * from array - * - */ - static function traverseMultiplicationBack(t:TermNode, p:Array) - { - if (p.length > 2) { - t.setOperation('*', newValue(1), p.pop()); - traverseMultiplicationBack(t.left, p); - } - else if (p.length == 2) { - t.setOperation('*', p[0].copy(), p[1].copy()); - p.pop(); - p.pop(); + /** + Link variables inside of this formula to another formulas + + @param formulas array of formulas to link to variables + @param paramNames (optional) names of the variables to link with (e.g. if formulas have no or different names) + **/ + public function bindArray(formulas:Array, ?paramNames:Array):Formula { + var map = new Map(); + if (paramNames != null) { + if (paramNames.length != formulas.length) ErrorMsg.bindArrayWrongLengths(formulas.length, paramNames.length); + for (i in 0...formulas.length) { + TermNode.checkValidName(paramNames[i]); + map.set(paramNames[i], formulas[i]); + } } else { - t.set(p.pop()); + for (formula in formulas) { + if (formula.name == null) ErrorMsg.cantBindUnnamed(this, formula); + map.set(formula.name, formula); + } } + return this.bind(map); } + + /** + Link variables inside of this formula to another formulas - /* - * put all subterms separated by * into an array - * - */ - static function traverseAddition(t:TermNode, p:Array, ?negative:Bool=false) - { - if (t.symbol == "+" && negative == false) { - traverseAddition(t.left, p); - traverseAddition(t.right, p); - } - else if (t.symbol == "-" && negative == false) { - traverseAddition(t.left, p); - traverseAddition(t.right, p, true); - } - else if (t.symbol == "+" && negative == true) { - traverseAddition(t.left, p, true); - traverseAddition(t.right, p, true); - } - else if (t.symbol == "-" && negative == true) { - traverseAddition(t.left, p, true); - traverseAddition(t.right, p); - } - else if (negative == true && !t.isValue || negative == true && t.isValue && t.value != 0) { - p.push(newOperation('-', newValue(0), t)); - } - else if (!t.isValue || t.isValue && t.value != 0) { - p.push(t); - } - return(p); + @param formulaMap map of formulas where the keys have same names as the variables to link with + **/ + public inline function bindMap(formulaMap:Map):Formula { + return this.bind(formulaMap); } + + // ------------ unbind ------------- + + /** + Delete all connections of the linked formula - /* - * build tree consisting of multiple - and + from array - * - */ - static function traverseAdditionBack(t:TermNode, p:Array) - { - if(p.length > 1) { - if (p[p.length-1].symbol == "-") { - t.set(p.pop()); - } - else { - t.setOperation("+", newValue(0), p.pop()); - } - traverseAdditionBack(t.left, p); - } - else if(p.length == 1){ - t.set(p.pop()); - } + @param formula formula that has to be unlinked + **/ + public inline function unbind(formula:Formula):Formula { + return this.unbindTerm( [formula] ); } + + /** + Delete all connections of the linked formulas - /* - * reduce a fraction - * - */ - static function simplifyfraction(t:TermNode) - { - var numerator:Array = new Array(); - traverseMultiplication(t.left, numerator); - var denominator:Array = new Array(); - traverseMultiplication(t.right, denominator); - for (n in numerator) { - for (d in denominator) { - if (isEqualAfterSimplify(n, d)) { - numerator.remove(n); - denominator.remove(d); - } - } - } - if (numerator.length > 1) { - traverseMultiplicationBack(t.left, numerator); - } - else if (numerator.length == 1) { - t.setOperation('/', numerator.pop(), newValue(1)); - } - else if (numerator.length == 0) { - t.left.setValue(1); - } - if (denominator.length > 1) { - traverseMultiplicationBack(t.right, denominator); - } - else if (denominator.length == 1) { - t.setOperation('/', t.left.copy(), denominator.pop()); - } - else if (denominator.length == 0) { - t.right.setValue(1); - } + @param formulas array of formulas that has to be unlinked + **/ + public function unbindArray(formulas:Array):Formula { + return this.unbindTerm(formulas); + } + + /** + Delete all connections to linked formulas for a given variable name + + @param paramName name of the variable where the connected formula has to unlink from + **/ + public inline function unbindParam(paramName:String):Formula { + TermNode.checkValidName(paramName); + return this.unbind( [paramName] ); } - /* - * expands a mathmatical expression recursivly into a polynomial - * - */ - static public function expand(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _expand(tnew); - return tnew; + /** + Delete all connections to linked formulas for the given variable names + + @param paramNames array of variablenames where the connected formula has to unlink from + **/ + public inline function unbindParamArray(paramNames:Array):Formula { + return this.unbind(paramNames); } + + // ----------------------------------- + + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula + + @param depth (optional) how deep the variable-bindings should be resolved + @param plOut (optional) creates formula for a special language (only "glsl" at now) + **/ + inline public function toString(?depth:Null = null, ?plOut:String = null):String return this.toString(depth, plOut); + + /** + Creates a formula from a packet Bytes representation + **/ + inline public static function fromBytes(b:Bytes):Formula return TermNode.fromBytes(b); - static function _expand(t:TermNode):Void { - - var len:Int = -1; - var len_old:Int = 0; - while(len != len_old) { - if (t.symbol == '*') { - expandStep(t); - } - else { - if(t.left != null) { - _expand(t.left); - } - if(t.right != null) { - _expand(t.right); - - } - } - len_old = len; - len = t.length(); - } + @:to inline public function toStr():String return this.toString(0); + @:to inline public function toFloat():Float return this.result; + + @:from static public function fromString(a:String):Formula return TermNode.fromString(a); + @:from static public function fromFloat(a:Float):Formula return TermNode.newValue(a); + + static inline function twoSideOp(op:String, a:Formula, b:Formula ):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a, + (b.name != null ) ? TermNode.newParam(b.name, b) : b + ); + } + @:op(A + B) static public function add (a:Formula, b:Formula):Formula return twoSideOp('+', a, b); + @:op(A - B) static public function subtract(a:Formula, b:Formula):Formula return twoSideOp('-', a, b); + @:op(A * B) static public function multiply(a:Formula, b:Formula):Formula return twoSideOp('*', a, b); + @:op(A / B) static public function divide (a:Formula, b:Formula):Formula return twoSideOp('/', a, b); + @:op(A ^ B) static public function potenz (a:Formula, b:Formula):Formula return twoSideOp('^', a, b); + @:op(A % B) static public function modulo (a:Formula, b:Formula):Formula return twoSideOp('%', a, b); + + public static inline function atan2(a:Formula, b:Formula):Formula return twoSideOp('atan2', a, b); + public static inline function log (a:Formula, b:Formula):Formula return twoSideOp('log', a, b); + public static inline function max (a:Formula, b:Formula):Formula return twoSideOp('max', a, b); + public static inline function min (a:Formula, b:Formula):Formula return twoSideOp('min', a, b); + + static inline function oneParamOp(op:String, a:Formula):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a + ); } + public static inline function abs (a:Formula):Formula return oneParamOp('abs', a); + public static inline function ln (a:Formula):Formula return oneParamOp('ln', a); + public static inline function sin (a:Formula):Formula return oneParamOp('sin', a); + public static inline function cos (a:Formula):Formula return oneParamOp('cos', a); + public static inline function tan (a:Formula):Formula return oneParamOp('tan', a); + public static inline function cot (a:Formula):Formula return oneParamOp('cot', a); + public static inline function asin(a:Formula):Formula return oneParamOp('asin', a); + public static inline function acos(a:Formula):Formula return oneParamOp('acos', a); + public static inline function atan(a:Formula):Formula return oneParamOp('atan', a); + +} + + +/** + * knot of a Tree to do math operations at runtime + * by Sylvio Sell, Rostock 2017 + * + **/ + +typedef OperationNode = {symbol:String, left:TermNode, right:TermNode, leftOperation:OperationNode, rightOperation:OperationNode, precedence:Int}; + +class TermNode { /* - * expands a mathmatical expression into a polynomial -> use only if top symbol=* + * Properties * */ - static function expandStep(t:TermNode):Void - { - var left:TermNode = t.left; - var right:TermNode = t.right; + var operation:TermNode->Float; // operation function pointer + public var symbol:String; //operator like "+" or parameter name like "x" - if (left.symbol == "+" || left.symbol == "-") { - if (right.symbol == "+" || right.symbol == "-") { - if (left.symbol == "+" && right.symbol == "+") { // (a+b)*(c+d) - t.setOperation('+', - newOperation('+', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('+', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "+" && right.symbol == "-") { // (a+b)*(c-d) - t.setOperation('+', - newOperation('-', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('-', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "-" && right.symbol == "+") { // (a-b)*(c+d) - t.setOperation('-', - newOperation('+', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('+', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "-" && right.symbol == "-") { // (a-b)*(c-d) - t.setOperation('-', - newOperation('-', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('-', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - } - else - { - if (left.symbol == "+") { // (a+b)*c - t.setOperation('+', - newOperation('*', left.left.copy(), right.copy()), - newOperation('*', left.right.copy(), right.copy()) - ); - } - else if (left.symbol == "-") { // (a-b)*c - t.setOperation('-', - newOperation('*', left.left.copy(), right.copy()), - newOperation('*', left.right.copy(), right.copy()) - ); - } - } + public var left:TermNode; // left branch of tree + public var right:TermNode; // right branch of tree + + public var value:Float; // leaf of the tree + + public var name(get, set):String; // name is stored into a param-TermNode at root of the tree + inline function get_name():String return (isName) ? symbol : null; + public static inline function checkValidName(name:String) if (!nameRegFull.match(name)) ErrorMsg.wrongCharInsideName; + inline function set_name(name:String):String { + if (name == null && isName) { + copyNodeFrom(left); } - else if (right.symbol == "+" || right.symbol == "-") { - if (right.symbol == "+") { // a*(b+c) - t.setOperation('+', - newOperation('*', left.copy(), right.left.copy()), - newOperation('*', left.copy(), right.right.copy()) - ); + else { + checkValidName(name); + if (isName) symbol = name else setName(name, copyNode()); + } + return name; + } + + /* + * returns depth of parameter bindings + * + */ + public inline function depth():Int { + if (isName && left != null) return left._depth(); + else return _depth(); + } + public inline function _depth():Int { + var l:Int = 0; + var r:Int = 0; + var d:Int = 0; + if (isParam) { + if (left == null) d = 0; + else if (!left.isName) d = 1; + } + else if (isName) d = 1; + + if (left != null) l = left._depth(); + if (right != null) r = right._depth(); + + return( d + ((l>r) ? l : r)); + } + + + /* + * Check Type of TermNode + * + */ + public var isName(get, null):Bool; // true -> root TermNode that holds name + inline function get_isName():Bool return Reflect.compareMethods(operation, opName); + + public var isParam(get, null):Bool; // true -> it's a parameter + inline function get_isParam():Bool return Reflect.compareMethods(operation, opParam); + + public var isValue(get, null):Bool; // true -> it's a value (no left and right) + inline function get_isValue():Bool return Reflect.compareMethods(operation, opValue); + + public var isOperation(get, null):Bool; // true -> it's a operation TermNode + inline function get_isOperation():Bool return !(isName||isParam||isValue); + + /* + * Calculates result of all Operations + * throws error if there is unbind param + */ + public var result(get, null):Float; // result of tree calculation + inline function get_result():Float return operation(this); + + /* + * Constructors + * + */ + public function new() {} + + public static inline function newName(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setName(name, term); + return t; + } + + public static inline function newParam(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setParam(name, term); + return t; + } + + public static inline function newValue(f:Float):TermNode { + var t:TermNode = new TermNode(); + t.setValue(f); + return t; + } + + public static inline function newOperation(s:String, ?left:TermNode, ?right:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setOperation(s, left, right); + return t; + } + + + /* + * atomic methods + * + */ + public inline function set(term:TermNode):TermNode { + // TODO: new param to keep the existing bindings if there is same parameters + if (isName) { + if (!term.isName) left = term.copy(); + else if (term.left != null) left = term.left.copy(); + else left = null; + } + else { + if (!term.isName) copyNodeFrom(term.copy()); + else if (term.left != null) copyNodeFrom(term.left.copy()); + //else return null; // TODO: check if that can ever been! + } + return this; + } + + public inline function setName(name:String, ?term:TermNode) { + operation = opName; + symbol = name; + left = term; right = null; + } + + public inline function setParam(name:String, ?term:TermNode) { + operation = opParam; + symbol = name; + left = term; right = null; + } + + public inline function setValue(f:Float):Void { + operation = opValue; + symbol = null; + value = f; + left = null; right = null; + } + + public inline function setOperation(s:String, ?left:TermNode, ?right:TermNode):Void { + operation = MathOp.get(s); + if (operation != null) + { + symbol = s; + this.left = left; + this.right = right; + } + else ErrorMsg.noValidOperation(s); + } + + /* + * returns an array of parameter-names + * + */ + public inline function params():Array { + var ret:Array = new Array(); + if (isParam) { + ret.push(symbol); + } + else { + if (left != null ) { + for (i in left.params()) if (ret.indexOf(i) < 0) ret.push(i); } - else if (right.symbol == "-") { // a*(b-c) - t.setOperation('-', - newOperation('*', left.copy(), right.left.copy()), - newOperation('*', left.copy(), right.right.copy()) - ); + if (right != null) { + for (i in right.params()) if (ret.indexOf(i) < 0) ret.push(i); } } + return ret; + } + + /* + * check if term has a param + * + */ + public function hasParam(paramName:String):Bool { + if (isParam && symbol == paramName) return true; + if (left != null ) + if (left.hasParam(paramName)) return true; + if (right != null) + if (right.hasParam(paramName)) return true; + return false; + } + + + /* + * bind terms to parameters + * + */ + public inline function bind(params:Map):TermNode { + if (isParam) { + if (params.exists(symbol)) left = params.get(symbol); + } + else { + if (left != null) left.bind(params); + if (right != null) right.bind(params); + } + return this; + } + + + /* + * unbind terms that is bind to parameter-names + * + */ + public inline function unbind(params:Array):TermNode { + if (isParam) { + if (params.indexOf(symbol) >= 0) left = null; + } + else { + if (left != null) left.unbind(params); + if (right != null) right.unbind(params); + } + return this; + } + + + /* + * unbind terms + * + */ + public inline function unbindTerm(params:Array):TermNode { + if (isParam) { + if (left != null) { + if (params.indexOf(left) >= 0) left = null; + } + } + else { + if (left != null) left.unbindTerm(params); + if (right != null) right.unbindTerm(params); + } + return this; + } + + /* + * check if a term is binded to + * + */ + public function hasBinding(term:TermNode):Bool { + if (isParam && left == term) return true; + if (left != null) + if (left.hasBinding(term)) return true; + if (right != null) + if (right.hasBinding(term)) return true; + return false; + } + + /* + * unbind all terms that is bind to parameter-names + * + */ + public inline function unbindAll():TermNode { + if (isParam) left = null; + else { + if (left != null) left.unbindAll(); + if (right != null) right.unbindAll(); + } + return this; + } + + + /* + * returns a new Term where all bindings are resolved down to + * the specified depth + */ + public inline function resolveAll(depth:Int = -1):TermNode { + // TODO: check better way + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, (left!=null) ? left.resolveAll(depth) : null); + else if (isParam) { + if (left == null) return TermNode.newParam(symbol, left); + else if (depth == 0) + return (left.isName) ? left.left : left; + else if (depth > 0) + return (left.isName) ? left.left.copy(depth).resolveAll(depth-1) : left.copy(depth).resolveAll(depth - 1); + else + return (left.isName) ? left.left.resolveAll(depth - 1) : left.resolveAll(depth - 1); + } + else return TermNode.newOperation(symbol, (left!=null) ? left.resolveAll(depth) : null, (right!=null) ? right.resolveAll(depth) : null); + } + + /* + * returns a recursive copy by starting with this TermNode + * depth can be used to define how deep it should copy the param-linked formulas + * + */ + public function copy(depth:Int = -1):TermNode + { + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, (left!=null) ? left.copy(depth) : null); + else if (isParam) return TermNode.newParam(symbol, (left!=null) ? ((depth == 0) ? left : left.copy(depth-1)) : null); + else return TermNode.newOperation(symbol, (left!=null) ? left.copy(depth) : null, (right!=null) ? right.copy(depth) : null); + } + + /* + * returns a clone of this TermNode only + * + */ + function copyNode():TermNode + { + if (isValue) return TermNode.newValue(value); + else if (isName) return TermNode.newName(symbol, left); + else if (isParam) return TermNode.newParam(symbol, left); + else return TermNode.newOperation(symbol, left, right); + } + + /* + * copy all from other TermNode to this + * + */ + public inline function copyNodeFrom(t:TermNode):Void { + if (t.isValue) setValue(t.value); + else if (t.isName) setName(t.symbol, t.left); + else if (t.isParam) setParam(t.symbol, t.left); + else setOperation(t.symbol, t.left, t.right); + } + + + /* + * number of TermNodes inside Tree + * + */ + public function length(?depth:Null=null):Int { + if (depth == null) depth = -1; + return switch(symbol) { + case s if (isValue): 1; + case s if (isName): (left == null) ? 0 : left.length(depth); + case s if (isParam): (depth == 0 || left == null) ? 1 : left.length(depth-1); + case s if (constantOpRegFull.match(s)): 1; + case s if (oneParamOpRegFull.match(s)): 1 + left.length(depth); + default: 1 + left.length(depth) + right.length(depth); + } + } + + + /* + * returns true if other term is equal in data and structure + * + */ + public function isEqual(t:TermNode, ?compareNames=false, ?compareParams=false):Bool + { + if ( !compareNames && (isName || t.isName) ) { + if (isName && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isName && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isName && t.isName); + } + + if ( !compareParams && (isParam || t.isParam) ) { + if (isParam && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isParam && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isParam && t.isParam); + } + + var is_equal:Bool = false; + + if (isValue && t.isValue) + is_equal = (value==t.value); + else if ( (isName && t.isName) || (isParam && t.isParam) || (isOperation && t.isOperation) ) + is_equal = (symbol==t.symbol); + + if (left != null) { + if (t.left != null) is_equal = is_equal && left.isEqual(t.left, compareNames, compareParams); + else is_equal = false; + } + if (right != null) { + if (t.right != null) is_equal = is_equal && right.isEqual(t.right, compareNames, compareParams); + else is_equal = false; + } + + return is_equal; + } + + /* + * static Function Pointers (to stored in this.operation) + * + */ + static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else ErrorMsg.emptyFunction(t.symbol); + static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else ErrorMsg.missingParameter(t.symbol); + static function opValue(t:TermNode):Float return t.value; + + static var MathOp:MapFloat> = [ + // two side operations + "+" => function(t) return t.left.result + t.right.result, + "-" => function(t) return t.left.result - t.right.result, + "*" => function(t) return t.left.result * t.right.result, + "/" => function(t) return t.left.result / t.right.result, + "^" => function(t) return Math.pow(t.left.result, t.right.result), + "%" => function(t) return t.left.result % t.right.result, + + // function without params (constants) + "e" => function(t) return Math.exp(1), + "pi" => function(t) return Math.PI, + + // function with one param + "abs" => function(t) return Math.abs(t.left.result), + "ln" => function(t) return Math.log(t.left.result), + "sin" => function(t) return Math.sin(t.left.result), + "cos" => function(t) return Math.cos(t.left.result), + "tan" => function(t) return Math.tan(t.left.result), + "cot" => function(t) return 1/Math.tan(t.left.result), + "asin" => function(t) return Math.asin(t.left.result), + "acos" => function(t) return Math.acos(t.left.result), + "atan" => function(t) return Math.atan(t.left.result), + + // function with two params + "atan2"=> function(t) return Math.atan2(t.left.result, t.right.result), + "log" => function(t) return Math.log(t.right.result) / Math.log(t.left.result), + "max" => function(t) return Math.max(t.left.result, t.right.result), + "min" => function(t) return Math.min(t.left.result, t.right.result), + ]; + + static var twoSideOp_ = "^,/,*,-,+,%"; // <- order here determines the operator precedence + static var constantOp_ = "e,pi"; // functions without parameters like "e() or pi()" + static var oneParamOp_ = "abs,ln,sin,cos,tan,cot,asin,acos,atan"; // functions with one parameter like "sin(2)" + static var twoParamOp_ = "atan2,log,max,min"; // functions with two parameters like "max(a,b)" + + static public var twoSideOp :Array = twoSideOp_.split(','); + static public var constantOp:Array = constantOp_.split(','); + static public var oneParamOp:Array = oneParamOp_.split(','); + static public var twoParamOp:Array = twoParamOp_.split(','); + + static var precedence:Map = [ for (i in 0...twoSideOp.length) twoSideOp[i] => i ]; + + + + /* + * Regular Expressions for parsing + * + */ + static var clearSpacesReg:EReg = ~/\s+/g; + static var trailingSpacesReg:EReg = ~/^(\s+)/; + + static var numberReg:EReg = ~/^([-+]?\d+\.?\d*)/; + static var paramReg:EReg = ~/^([a-z]+)/i; + + static var constantOpReg:EReg = new EReg("^(" + constantOp.join("|") + ")\\(\\)" , "i"); + static var oneParamOpReg:EReg = new EReg("^(" + oneParamOp.join("|") + ")\\(" , "i"); + static var twoParamOpReg:EReg = new EReg("^(" + twoParamOp.join("|") + ")\\(" , "i"); + static var twoSideOpReg: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")" , ""); + + static public var constantOpRegFull:EReg = new EReg("^(" + constantOp.join("|") + ")$" , "i"); + static public var oneParamOpRegFull:EReg = new EReg("^(" + oneParamOp.join("|") + ")$" , "i"); + static public var twoParamOpRegFull:EReg = new EReg("^(" + twoParamOp.join("|") + ")$" , "i"); + static public var twoSideOpRegFull: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")$" , ""); + + static var nameReg:EReg = ~/^([a-z]+)(\s*[:=]\s*)/i; + static var nameRegFull:EReg = ~/^([a-z]+)$/i; + + static var signReg:EReg = ~/^([-+\s]+)/i; + + public static inline function trailingSpaces(s:String):Int { + if (trailingSpacesReg.match(s)) return(trailingSpacesReg.matched(1).length); + else return 0; } - /* - * factorize a term: a*c+a*b -> a*(c+b) - * - */ - static public function factorize(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _factorize(tnew); - return tnew; + * Build Tree up from String Math Expression + * + */ + public static inline function fromString(s:String, ?bindings:Map):TermNode { + var errPos:Int = 0; + errPos = trailingSpaces(s); s = s.substr(errPos); + //s = clearSpacesReg.replace(s, ''); // clear all whitespaces + if (nameReg.match(s)) { + var name:String = nameReg.matched(1); + s = s.substr(name.length + nameReg.matched(2).length); + errPos += name.length + nameReg.matched(2).length; + if (~/^\s*$/.match(s)) ErrorMsg.cantParseFromEmptyString(errPos); + return newName(name, parseString(s, errPos, bindings)); + } + if (~/^\s*$/.match(s)) ErrorMsg.cantParseFromEmptyString(errPos); + return parseString(s, errPos, bindings); } - static function _factorize(t:TermNode):Void { - var mult_matrix:Array> = new Array(); - var add:Array = new Array(); + static function parseString(s:String, errPos:Int, ?params:Map):TermNode { + var t:TermNode = null; + var operations:Array = new Array(); + var e, f:String; + var negate:Bool; + var spaces:Int = 0; - // build matrix - addition in columns - multiplication in rows - traverseAddition(t, add); - var add_length_old:Int = 0; - for(i in add) { - if(i.symbol == "-") { - mult_matrix.push(new Array()); - traverseMultiplication(add[mult_matrix.length-1].right, mult_matrix[mult_matrix.length-1]); + while (s.length != 0) // read in terms from left + { + negate = false; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (numberReg.match(s)) { // float number + e = numberReg.matched(1); + t = newValue(Std.parseFloat(e)); + } + else if (constantOpReg.match(s)) { // like e() or pi() + e = constantOpReg.matched(1); + t = newOperation(e); + e+= "()"; + } + else if (oneParamOpReg.match(s)) { // like sin(...) + f = oneParamOpReg.matched(1); errPos += f.length; + s = "("+oneParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + t = newOperation(f, parseString(e.substring(1, e.length - 1), errPos+1, params) ); + } + else if (twoParamOpReg.match(s)) { // like atan2(... , ...) + f = twoParamOpReg.matched(1); errPos += f.length; + s = "("+twoParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + var p1:String = e.substring(1, comataPos); + var p2:String = e.substring(comataPos + 1, e.length - 1); + if (comataPos == -1) ErrorMsg.operatorNeedTwoArgs(f, errPos); + t = newOperation(f, parseString(p1, errPos+1, params), parseString(p2, errPos+1 + comataPos, params) ); + } + else if (paramReg.match(s)) { // parameter + e = paramReg.matched(1); + t = newParam(e, (params==null) ? null : params.get(e)); + } + else if (signReg.match(s)) { // start with +- + e = signReg.matched(1); + s = s.substr(e.length); errPos += e.length; + e = ~/[\s+]/g.replace(e, ''); + if (e.length % 2 > 0) { + //s = "0-" + s; + if (numberReg.match(s)) { // followed by float number + e = numberReg.matched(1); + t = newValue(-Std.parseFloat(e)); + } else { // negative signed + t = newValue(0); + s = "-" + s; + e = ""; + negate = true; + } + } else continue; // positive signed + } + else if (twoSideOpReg.match(s)) { // start with other two side op + ErrorMsg.missingLeftOperand(errPos); } else { - mult_matrix.push(new Array()); - traverseMultiplication(add[mult_matrix.length-1], mult_matrix[mult_matrix.length-1]); + e = getBrackets(s, errPos); // term inside brackets + t = parseString(e.substring(1, e.length - 1), errPos+1, params); + } + + s = s.substr(e.length); errPos += e.length; + + if (operations.length > 0) operations[operations.length - 1].right = t; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (twoSideOpReg.match(s)) { // two side operation symbol + e = twoSideOpReg.matched(1); errPos += e.length; + s = twoSideOpReg.matchedRight(); + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + operations.push( { symbol:e, left:t, right:null, leftOperation:null, rightOperation:null, precedence:((negate) ? -1 :precedence.get(e)) } ); + if (operations.length > 1) { + operations[operations.length - 2].rightOperation = operations[operations.length - 1]; + operations[operations.length - 1].leftOperation = operations[operations.length - 2]; + } + } else if (s.length > 0) { + if (s.indexOf(")") == 0) ErrorMsg.noOpeningBracket(errPos); + if (!(s.indexOf("(") == 0 || numberReg.match(s) || paramReg.match(s) || constantOpReg.match(s) || oneParamOpReg.match(s) || twoParamOpReg.match(s))) + ErrorMsg.wrongChar(errPos); + else ErrorMsg.missingOperation(errPos); } } - // find and extract common factors - var part_of_all:Array = new Array(); - factorize_extract_common(mult_matrix, part_of_all); - if(part_of_all.length != 0) { - var new_add:Array = new Array(); - var helper:TermNode = new TermNode(); - for(i in mult_matrix) { - traverseMultiplicationBack(helper, i); - var v:TermNode = new TermNode(); - v.set(helper); - new_add.push(v); - } - for(i in 0...add.length) { - if(add[i].symbol == '-' && add[i].left.value == 0) { - new_add[i].setOperation('-', newValue(0), new_add[i].copy()); + if ( operations.length > 0 ) { + if ( operations[operations.length-1].right == null ) ErrorMsg.missingRightOperand(errPos-spaces); + else { + operations.sort(function(a:OperationNode, b:OperationNode):Int + { + if (a.precedence < b.precedence) return -1; + if (a.precedence > b.precedence) return 1; + return 0; + }); + for (op in operations) { + t = TermNode.newOperation(op.symbol, op.left, op.right); + if (op.leftOperation != null && op.rightOperation != null) { + op.rightOperation.leftOperation = op.leftOperation; + op.leftOperation.rightOperation = op.rightOperation; + } + if (op.leftOperation != null) op.leftOperation.right = t; + if (op.rightOperation != null) op.rightOperation.left = t; } + return t; } - - t.setOperation('*', new TermNode(), new TermNode()); - traverseMultiplicationBack(t.left, part_of_all); - traverseAdditionBack(t.right, new_add); } + else return t; } - // delete common factors of mult_matrix and add them to part_of_all - static function factorize_extract_common(mult_matrix:Array>, part_of_all:Array):Void { - var bool:Bool = false; - var matrix_length_old:Int = -1; - var i:TermNode=new TermNode(); - var exponentiation_counter:Int = 0; - while(matrix_length_old != mult_matrix[0].length) { - matrix_length_old = mult_matrix[0].length; - for(p in mult_matrix[0]) { - if(p.symbol == '^') { - i.set(p.left); - exponentiation_counter++; - } - else if(p.symbol == '-' && p.left.isValue && p.left.value == 0) { - i.set(p.right); - } - else { - i.set(p); + static var comataPos:Int; + static function getBrackets(s:String, errPos:Int):String { + var pos:Int = 1; + if (s.indexOf("(") == 0) // check that s starts with opening bracket + { + if (~/^\(\s*\)/.match(s)) ErrorMsg.emptyBrackets(errPos); + + var i,j,k:Int; + var openBrackets:Int = 1; + comataPos = -1; + while ( openBrackets > 0 ) + { + i = s.indexOf("(", pos); + j = s.indexOf(")", pos); + + // check for commata position + if (openBrackets == 1 && comataPos == -1) { + k = s.indexOf(",", pos); + if (k0) comataPos = k; } - for(j in 1...mult_matrix.length) { - bool = false; - for(h in mult_matrix[j]) { - if(isEqualAfterSimplify(h, i)) { - bool = true; - break; - } - else if(h.symbol == '^' && isEqualAfterSimplify(h.left , i)) { - bool=true; - exponentiation_counter++; - break; - - } - else if(h.symbol == '-' && h.left.isValue && h.left.value == 0 && isEqualAfterSimplify(h.right, i)) { - bool=true; - break; - } - } - if(bool == false) { - break; - } + + if ((i>0 && j>0 && i0 && j<0)) { // found open bracket + openBrackets++; pos = i + 1; } - if(bool == true && exponentiation_counter < mult_matrix.length) { - part_of_all.push(new TermNode()); - part_of_all[part_of_all.length-1].set(i); - var helper:TermNode = new TermNode(); - helper.set(i); - delete_last_from_matrix(mult_matrix, helper); - break; + else if ((j>0 && i>0 && j0 && i<0)) { // found close bracket + openBrackets--; pos = j + 1; + } else { // no close or open found + ErrorMsg.wrongBracketNesting(errPos); } } + return s.substring(0, pos); + } + if (s.indexOf(")") == 0) ErrorMsg.noOpeningBracket(errPos); + else ErrorMsg.wrongChar(errPos); + } + + + /* + * Puts out Math Expression as a String + * + */ + public function toString(?depth:Null = null, ?plOut:String = null):String { + var t:TermNode = this; + if (isName) t = left; + var options:Int; + switch (plOut) { + case 'glsl': options = noNeg|forceFloat|forcePow|forceMod|forceLog|forceAtan|forceConst; + default: options = 0; + } + return (left != null || !isName) ? t._toString(depth, options) : ''; + } + // options + public static inline var noNeg:Int = 1; + public static inline var forceFloat:Int = 2; + public static inline var forcePow:Int = 4; + public static inline var forceMod:Int = 8; + public static inline var forceLog:Int = 16; + public static inline var forceAtan:Int = 32; + public static inline var forceConst:Int = 64; + + inline function _toString(depth:Null, options:Int, ?isFirst:Bool=true):String { + if (depth == null) depth = -1; + return switch(symbol) { + case s if (isValue): floatToString(value, options); + //case s if (isName && isFirst): (left == null) ? symbol : left.toString(depth, false); + case s if (isName): (depth == 0 || left == null) ? symbol : left._toString(depth-1, options, false); + case s if (isParam): (depth == 0 || left == null) ? symbol : left._toString(depth-((left.isName)?0:1), options, false); + case s if (twoSideOpRegFull.match(s)) : + if (symbol == "-" && left.isValue && left.value == 0 && options&noNeg == 0) symbol + right._toString(depth, options, false); + else if (symbol == "^" && options&forcePow > 0) 'pow' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else if (symbol == "%" && options&forceMod > 0) 'mod' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else ((isFirst)?"":"(") + left._toString(depth, options, false) + symbol + right._toString(depth, options, false) + ((isFirst)?'':")"); + case s if (twoParamOpRegFull.match(s)): + if (symbol == "log" && options&forceLog > 0) "(log(" + right._toString(depth, options) + ")/log(" + left._toString(depth, options) + "))"; + else if (symbol == "atan2" && options&forceAtan > 0) "atan(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + case s if (constantOpRegFull.match(s)): + if (symbol == "pi" && options & forceConst > 0) Std.string(Math.PI); + else if (symbol == "e" && options&forceConst > 0) Std.string(Math.exp(1)); + else symbol + "()"; + default: + if (symbol == "ln" && options&forceLog > 0) 'log' + "(" + left._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + ")"; } } - // deletes d from every row in mult_matrix once - static function delete_last_from_matrix(mult_matrix:Array>, d:TermNode):Void { - for(i in mult_matrix) { - if(i.length>1) { - for(j in 1...i.length+1) { - if(isEqualAfterSimplify(i[i.length-j], d)) { // a*x -> a - for(h in 0...j-1) { - i[i.length-j+h].set(i[i.length-j+h+1]); - } - i.pop(); - break; - } - else if(i[i.length-j].symbol == '^' && isEqualAfterSimplify(i[i.length-j].left, d)) { // x^n -> x^(n-1) - i[i.length-j].right.set(newOperation('-', i[i.length-j].right.copy(), newValue(1))); - break; - } - else if(i[i.length-j].symbol == '-' && i[i.length-j].left.isValue && i[i.length-j].left.value == 0 && isEqualAfterSimplify(i[i.length-j].right, d)) { - i[i.length-j].right.set(newValue(1)); - break; - } - } - } - else if(i[0].symbol == '^' && isEqualAfterSimplify(i[0].left, d)) { // x^n -> x^(n-1) - i[0].right.set(newOperation('-', i[0].right.copy(), newValue(1))); - } - else { - i[0].set(newValue(1)); - } + inline function floatToString(value:Float, ?options:Int = 0):String { + var s:String = Std.string(value); + if (options&forceFloat > 0 && s.indexOf('.') == -1) s += ".0"; + return s; + } + + /* + * enrolls terms and subterms for debugging + * + */ + public function debug() { + //TODO + var out:String = "";// "(" + depth() + ")"; + for (i in 0 ... depth()+1) { + if (i == 0) out += ((name != null) ? name : "?") + " = "; else out += " -> "; + out += toString(i); } + trace(out); } - // compare function for Array.sort() - static function formsort_compare(t1:TermNode, t2:TermNode):Int - { - if (formsort_priority(t1) > formsort_priority(t2)) { - return -1; + /* + * packs a TermNode and all sub-terms into Bytes + * + */ + public function toBytes():Bytes { + var b = new BytesOutput(); + _toBytes(b); + return b.getBytes(); + } + + inline function _toBytes(b:BytesOutput) { + // optimize (later to do): needs only 3 bit per TermNode type! + if (isValue) { + b.writeByte(0); + b.writeFloat(value); } - else if (formsort_priority(t1) < formsort_priority(t2)) { - return 1; + else if (isName) { + b.writeByte((left!=null) ? 1:2); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); } - else{ - if (t1.isValue && t2.isValue) { - if (t1.value >= t2.value) { - return(-1); - } - else{ - return(1); - } - } - else if (t1.isOperation && t2.isOperation) { - if(t1.right != null && t2.right != null) { - return(formsort_compare(t1.right, t2.right)); - } - else { - return(formsort_compare(t1.left, t2.left)); + else if (isParam) { + b.writeByte((left!=null) ? 3:4); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); + } + else if (isOperation) { + b.writeByte(5); + var i:Int = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp))).indexOf(symbol); + if (i > -1) { + b.writeByte(i); + if (oneParamOpRegFull.match(symbol)) left._toBytes(b); + else if (twoSideOpRegFull.match(symbol) || twoParamOpRegFull.match(symbol) ) { + left._toBytes(b); + right._toBytes(b); } } - else return 0; + else ErrorMsg.intoBytes(); } + else ErrorMsg.intoBytes(); } - - // priority function for formsort_compare() - static function formsort_priority(t:TermNode):Float - { - return switch(t.symbol) - { - case s if (t.isParam): t.symbol.charCodeAt(0); - case s if (t.isName): t.symbol.charCodeAt(0); - case s if (t.isValue): 1+0.00001*t.value; - case s if (TermNode.twoSideOpRegFull.match(s)) : - if(t.symbol == '-' && t.left.value == 0) { - formsort_priority(t.right); - } - else { - formsort_priority(t.left)+formsort_priority(t.right)*0.001; - } - case s if (TermNode.oneParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.indexOf(s); - case s if (TermNode.twoParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.indexOf(s); - case s if (TermNode.constantOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.indexOf(s); - - default: -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.length; - } + + inline function _writeString(s:String, b:BytesOutput):Void { + b.writeByte((s.length<255) ? s.length: 255); + for (i in 0...((s.length<255) ? s.length: 255)) b.writeByte(symbol.charCodeAt(i)); } - /* - * sort a Tree consisting of products + * unserialize packed Bytes-Term to create a TermNode structure * */ - static function arrangeMultiplication(t:TermNode):Void - { - var mult:Array = new Array(); - traverseMultiplication(t, mult); - mult.sort(formsort_compare); - traverseMultiplicationBack(t, mult); + public static function fromBytes(b:Bytes):TermNode { + return _fromBytes(new BytesInput(b)); } - + + static inline function _fromBytes(b:BytesInput):TermNode { + return switch (b.readByte()) { + case 0: TermNode.newValue(b.readFloat()); + case 1: TermNode.newName( _readString(b), _fromBytes(b) ); + case 2: TermNode.newName( _readString(b) ); + case 3: TermNode.newParam( _readString(b), _fromBytes(b) ); + case 4: TermNode.newParam( _readString(b) ); + case 5: + var op:String = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp)))[b.readByte()]; + if (oneParamOpRegFull.match(op)) TermNode.newOperation( op, _fromBytes(b) ); + else if (twoSideOpRegFull.match(op) || twoParamOpRegFull.match(op) ) TermNode.newOperation( op, _fromBytes(b), _fromBytes(b) ); + else TermNode.newOperation( op ); + default: ErrorMsg.fromBytes(); null; + } + } + + static inline function _readString(b:BytesInput):String { + var len:Int = b.readByte(); + var s:String = ""; + for (i in 0...len) s += String.fromCharCode(b.readByte()); + return s; + } + + + /************************************************************************************** + * * + * various math operations transformation and more. * + * * + * * + **************************************************************************************/ + /* - * sort a Tree consisting of addition and subtraction - * + * creates a new term that is derivate of a given term + * */ - static function arrangeAddition(t:TermNode):Void - { - var addlength_old:Int = -1; - var add:Array = new Array(); - traverseAddition(t, add); - add.sort(formsort_compare); - while(add.length != addlength_old) { - addlength_old = add.length; - for(i in 0...add.length-1) { - if(isEqualAfterSimplify(add[i], add[i+1])) { - add[i].setOperation('*', add[i].copy(), newValue(2)); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if(add[i].symbol == '*' && add[i+1].symbol == '*' && add[i].right.isValue && add[i+1].right.isValue && isEqualAfterSimplify(add[i].left, add[i+1].left)) { - add[i].right.setValue(add[i].right.value+add[i+1].right.value); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if(add[i].isValue && add[i+1].isValue) { - add[i].setValue(add[i].value+add[i+1].value); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if((add[i].symbol == '-' && add[i].left.isValue && add[i].left.value == 0 && isEqualAfterSimplify(add[i].right, add[i+1])) || (add[i+1].symbol == '-' && add[i+1].left.isValue && add[i+1].left.value == 0 && isEqualAfterSimplify(add[i+1].right, add[i]))) { - for(j in 0...add.length-i-2) { - add[i+j] = add[i+j+2]; - } - add.pop(); - add.pop(); - if(add.length == 0){ - add.push(newValue(0)); - } - break; - } - } - - if(add[0].symbol == '-' && add[0].left.value == 0) { - for(i in add) { - if(i.symbol == '-' && i.left.value == 0) { - i.set(i.right); - } - else { - i.setOperation('-', newValue(0), i.copy()); - } - } - t.setOperation('-', newValue(0), new TermNode()); - traverseAdditionBack(t.right, add); - return; - } - - } - traverseAdditionBack(t, add); - } + public function derivate(paramName:String):TermNode return TermDerivate.derivate(this, paramName); + + /* + * Simplify: trims the length of a math expression + * + */ + public function simplify():TermNode return TermTransform.simplify(this); + + /* + * expands a mathmatical expression recursivly into a polynomial + * + */ + public function expand():TermNode return TermTransform.expand(this); + + /* + * factorizes a mathmatical expression + * + */ + public function factorize():TermNode return TermTransform.factorize(this); } + /** * symbolic derivation * by Sylvio Sell, Rostock 2017 @@ -858,1057 +1166,885 @@ class TermDerivate { newOperation('/', t.left.copy(), newOperation('abs', t.left.copy()) ) ); - default: throw('derivation of "${t.symbol}" not implemented'); + default: ErrorMsg.notImplementedFor(t.symbol); null; } } + } + /** - * knot of a Tree to do math operations at runtime + * extending TermNode with various math operations transformation and more. * by Sylvio Sell, Rostock 2017 * **/ - -typedef OperationNode = {symbol:String, left:TermNode, right:TermNode, leftOperation:OperationNode, rightOperation:OperationNode, precedence:Int}; -class TermNode { - - /* - * Properties - * - */ - var operation:TermNode->Float; // operation function pointer - public var symbol:String; //operator like "+" or parameter name like "x" +class TermTransform { - public var left:TermNode; // left branch of tree - public var right:TermNode; // right branch of tree - - public var value:Float; // leaf of the tree - - public var name(get, set):String; // name is stored into a param-TermNode at root of the tree - inline function get_name():String return (isName) ? symbol : null; - public static inline function checkValidName(name:String) if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); - inline function set_name(name:String):String { - if (name == null && isName) { - copyNodeFrom(left); - } - else { - //if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); - checkValidName(name); - if (isName) symbol = name else setName(name, copyNode()); - } - return name; - } - - /* - * returns depth of parameter bindings - * - */ - public inline function depth():Int { - if (isName && left != null) return left._depth(); - else return _depth(); - } - public inline function _depth():Int { - var l:Int = 0; - var r:Int = 0; - var d:Int = 0; - if (isParam) { - if (left == null) d = 0; - else if (!left.isName) d = 1; - } - else if (isName) d = 1; - - if (left != null) l = left._depth(); - if (right != null) r = right._depth(); - - return( d + ((l>r) ? l : r)); - } - - - /* - * Check Type of TermNode - * - */ - public var isName(get, null):Bool; // true -> root TermNode that holds name - inline function get_isName():Bool return Reflect.compareMethods(operation, opName); - - public var isParam(get, null):Bool; // true -> it's a parameter - inline function get_isParam():Bool return Reflect.compareMethods(operation, opParam); - - public var isValue(get, null):Bool; // true -> it's a value (no left and right) - inline function get_isValue():Bool return Reflect.compareMethods(operation, opValue); - - public var isOperation(get, null):Bool; // true -> it's a operation TermNode - inline function get_isOperation():Bool return !(isName||isParam||isValue); - - /* - * Calculates result of all Operations - * throws error if there is unbind param - */ - public var result(get, null):Float; // result of tree calculation - inline function get_result():Float return operation(this); + static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; + static var newValue:Float->TermNode = TermNode.newValue; /* - * Constructors + * Simplify: trims the length of a math expression * */ - public function new() {} - - public static inline function newName(name:String, ?term:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setName(name, term); - return t; + static public inline function simplify(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _simplify(tnew); + return tnew; } - public static inline function newParam(name:String, ?term:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setParam(name, term); - return t; + static inline function _simplify(t:TermNode):Void { + _expand(t); + + var len:Int = -1; + var len_old:Int = 0; + while (len != len_old) { + if (t.isName && t.left != null) { + simplifyStep(t.left); + } + else { + simplifyStep(t); + } + len_old = len; + len = t.length(); + } } - public static inline function newValue(f:Float):TermNode { - var t:TermNode = new TermNode(); - t.setValue(f); - return t; + // TODO: removing this calls in subfunctions could be better to understand algorithms-recursions !!! + static function isEqualAfterSimplify(t1:TermNode, t2:TermNode):Bool { + // ----> take care, _simplify changes both TermNodes on call !!! + _simplify(t1); + _simplify(t2); + return t1.isEqual(t2, false, true); } - public static inline function newOperation(s:String, ?left:TermNode, ?right:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setOperation(s, left, right); - return t; - } + static function simplifyStep(t:TermNode):Void { + if (!t.isOperation) return; + + if (t.left != null) { + if (t.left.isValue) { + if (t.right == null) { + // setValue(result); // calculate operation with one value + return; + } + else if (t.right.isValue) { + t.setValue(t.result); // calculate result of operation with values on both sides + return; + } + } + } + + switch(t.symbol) { + case '+': + if (t.left.isValue && t.left.value == 0) t.copyNodeFrom(t.right); // 0+a -> a + else if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a+0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)+ln(b) -> ln(a*b) + t.setOperation('ln', + newOperation('*', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b+c/b -> (a+c)/b + newOperation('+', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { // a/b+c/d -> (a*d+c*b)/(b*d) + t.setOperation('/', + newOperation('+', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '+') { + _factorize(t); + } + + case '-': + if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a-0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)-ln(b) -> ln(a/b) + t.setOperation('ln', + newOperation('/', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b-c/b -> (a-c)/b + newOperation('-', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { //a/b-c/d -> (a*d-c*b)/(b*d) + t.setOperation('/', + newOperation('-', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '-') { + _factorize(t); + } + + case '*': + if (t.left.isValue) { + if (t.left.value == 1) t.copyNodeFrom(t.right); // 1*a -> a + else if (t.left.value == 0) t.setValue(0); // 0*a -> 0 + } + else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a*1 -> a + else if (t.right.value == 0) t.setValue(0); // a*0 -> a + } + else if (t.left.symbol == '/') { // (a/b)*c -> (a*c)/b + t.setOperation('/', + newOperation('*', t.right.copy(), t.left.left.copy()), + t.left.right.copy() + ); + } + else if (t.right.symbol == '/') { // a*(b/c) -> (a*b)/c + t.setOperation('/', + newOperation('*', t.left.copy(), t.right.left.copy()), + t.right.right.copy() + ); + } + else { + arrangeMultiplication(t); + } + + case '/': + if (isEqualAfterSimplify(t.left, t.right)) { // x/x -> 1 + t.setValue(1); + } + else { + if (t.left.isValue && t.left.value == 0) t.setValue(0); // 0/a -> 0 + else if (t.right.symbol == '/') { + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.right.isValue && t.right.value == 1) t.copyNodeFrom(t.left); // a/1 -> a + else if (t.left.symbol == '/') { // (1/x)/b -> 1/(x*b) + t.setOperation('/', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } + else if (t.right.symbol == '/') { // b/(1/x) -> b*x + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.left.symbol == '-' && t.left.left.isValue && t.left.left.value == 0) + { + t.setOperation('-', newValue(0), + newOperation('/', t.left.right.copy(), t.right.copy()) + ); + } + else{ // a*b/b -> a + simplifyfraction(t); + } + } - - /* - * atomic methods - * - */ - public inline function set(term:TermNode):TermNode { - // TODO: new param to keep the existing bindings if there is same parameters - if (isName) { - if (!term.isName) left = term.copy(); - else if (term.left != null) left = term.left.copy(); - else left = null; - } - else { - if (!term.isName) copyNodeFrom(term.copy()); - else if (term.left != null) copyNodeFrom(term.left.copy()); - //else return null; // TODO: check if that can ever been! - } - return this; - } - - public inline function setName(name:String, ?term:TermNode) { - operation = opName; - symbol = name; - left = term; right = null; - } - - public inline function setParam(name:String, ?term:TermNode) { - operation = opParam; - symbol = name; - left = term; right = null; - } - - public inline function setValue(f:Float):Void { - operation = opValue; - symbol = null; - value = f; - left = null; right = null; - } - - public inline function setOperation(s:String, ?left:TermNode, ?right:TermNode):Void { - operation = MathOp.get(s); - if (operation != null) - { - symbol = s; - this.left = left; - this.right = right; - } - else throw ('"$s" is no valid operation.'); - } + case '^': + if (t.left.isValue) { + if (t.left.value == 1) t.setValue(1); // 1^a -> 1 + else if (t.left.value == 0) t.setValue(0); // 0^a -> 0 + } else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a^1 -> a + else if (t.right.value == 0) t.setValue(1); // a^0 -> 1 + } + else if (t.left.symbol == '^') { // (a^b)^c -> a^(b*c) + t.setOperation('^', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } - /* - * returns an array of parameter-names - * - */ - public inline function params():Array { - var ret:Array = new Array(); - if (isParam) { - ret.push(symbol); - } - else { - if (left != null ) { - for (i in left.params()) if (ret.indexOf(i) < 0) ret.push(i); - } - if (right != null) { - for (i in right.params()) if (ret.indexOf(i) < 0) ret.push(i); - } + case 'ln': + if (t.left.symbol == 'e') t.setValue(1); + case 'log': + if (isEqualAfterSimplify(t.left, t.right)) { + t.setValue(1); + } + else { + t.setOperation('/', // log(a,b) -> ln(b)/ln(a) + newOperation('ln', t.right.copy()), + newOperation('ln', t.left.copy()) + ); + } } - return ret; - } - - /* - * check if term has a param - * - */ - public function hasParam(paramName:String):Bool { - if (isParam && symbol == paramName) return true; - if (left != null ) - if (left.hasParam(paramName)) return true; - if (right != null) - if (right.hasParam(paramName)) return true; - return false; + if (t.left != null) simplifyStep(t.left); + if (t.right != null) simplifyStep(t.right); } - - + /* - * bind terms to parameters + * put all subterms separated by * into an array * - */ - public inline function bind(params:Map):TermNode { - if (isParam) { - if (params.exists(symbol)) left = params.get(symbol); + */ + static function traverseMultiplication(t:TermNode, p:Array) + { + if (t.symbol != "*") { + p.push(t); } else { - if (left != null) left.bind(params); - if (right != null) right.bind(params); + traverseMultiplication(t.left, p); + traverseMultiplication(t.right, p); } - return this; } - /* - * unbind terms that is bind to parameter-names + * build tree consisting of multiple * from array * - */ - public inline function unbind(params:Array):TermNode { - if (isParam) { - if (params.indexOf(symbol) >= 0) left = null; + */ + static function traverseMultiplicationBack(t:TermNode, p:Array) + { + if (p.length > 2) { + t.setOperation('*', newValue(1), p.pop()); + traverseMultiplicationBack(t.left, p); + } + else if (p.length == 2) { + t.setOperation('*', p[0].copy(), p[1].copy()); + p.pop(); + p.pop(); } else { - if (left != null) left.unbind(params); - if (right != null) right.unbind(params); + t.set(p.pop()); } - return this; } - - + /* - * unbind terms - * - */ - public inline function unbindTerm(params:Array):TermNode { - if (isParam) { - if (left != null) { - if (params.indexOf(left) >= 0) left = null; - } + * put all subterms separated by * into an array + * + */ + static function traverseAddition(t:TermNode, p:Array, ?negative:Bool=false) + { + if (t.symbol == "+" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p); } - else { - if (left != null) left.unbindTerm(params); - if (right != null) right.unbindTerm(params); - } - return this; - } - - /* - * check if a term is binded to - * - */ - public function hasBinding(term:TermNode):Bool { - if (isParam && left == term) return true; - if (left != null) - if (left.hasBinding(term)) return true; - if (right != null) - if (right.hasBinding(term)) return true; - return false; - } - - /* - * unbind all terms that is bind to parameter-names - * - */ - public inline function unbindAll():TermNode { - if (isParam) left = null; - else { - if (left != null) left.unbindAll(); - if (right != null) right.unbindAll(); + else if (t.symbol == "-" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p, true); } - return this; - } - - - /* - * returns a new Term where all bindings are resolved down to - * the specified depth - */ - public inline function resolveAll(depth:Int = -1):TermNode { - // TODO: check better way - if (isValue) return TermNode.newValue(value); - else if (isName) return TermNode.newName(symbol, (left!=null) ? left.resolveAll(depth) : null); - else if (isParam) { - if (left == null) return TermNode.newParam(symbol, left); - else if (depth == 0) - return (left.isName) ? left.left : left; - else if (depth > 0) - return (left.isName) ? left.left.copy(depth).resolveAll(depth-1) : left.copy(depth).resolveAll(depth - 1); - else - return (left.isName) ? left.left.resolveAll(depth - 1) : left.resolveAll(depth - 1); + else if (t.symbol == "+" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p, true); } - else return TermNode.newOperation(symbol, (left!=null) ? left.resolveAll(depth) : null, (right!=null) ? right.resolveAll(depth) : null); - } - - /* - * returns a recursive copy by starting with this TermNode - * depth can be used to define how deep it should copy the param-linked formulas - * - */ - public function copy(depth:Int = -1):TermNode - { - if (isValue) return TermNode.newValue(value); - else if (isName) return TermNode.newName(symbol, (left!=null) ? left.copy(depth) : null); - else if (isParam) return TermNode.newParam(symbol, (left!=null) ? ((depth == 0) ? left : left.copy(depth-1)) : null); - else return TermNode.newOperation(symbol, (left!=null) ? left.copy(depth) : null, (right!=null) ? right.copy(depth) : null); + else if (t.symbol == "-" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p); + } + else if (negative == true && !t.isValue || negative == true && t.isValue && t.value != 0) { + p.push(newOperation('-', newValue(0), t)); + } + else if (!t.isValue || t.isValue && t.value != 0) { + p.push(t); + } + return(p); } /* - * returns a clone of this TermNode only - * - */ - function copyNode():TermNode + * build tree consisting of multiple - and + from array + * + */ + static function traverseAdditionBack(t:TermNode, p:Array) { - if (isValue) return TermNode.newValue(value); - else if (isName) return TermNode.newName(symbol, left); - else if (isParam) return TermNode.newParam(symbol, left); - else return TermNode.newOperation(symbol, left, right); + if(p.length > 1) { + if (p[p.length-1].symbol == "-") { + t.set(p.pop()); + } + else { + t.setOperation("+", newValue(0), p.pop()); + } + traverseAdditionBack(t.left, p); + } + else if(p.length == 1){ + t.set(p.pop()); + } } /* - * copy all from other TermNode to this - * - */ - public inline function copyNodeFrom(t:TermNode):Void { - if (t.isValue) setValue(t.value); - else if (t.isName) setName(t.symbol, t.left); - else if (t.isParam) setParam(t.symbol, t.left); - else setOperation(t.symbol, t.left, t.right); - } - - - /* - * number of TermNodes inside Tree - * - */ - public function length(?depth:Null=null):Int { - if (depth == null) depth = -1; - return switch(symbol) { - case s if (isValue): 1; - case s if (isName): (left == null) ? 0 : left.length(depth); - case s if (isParam): (depth == 0 || left == null) ? 1 : left.length(depth-1); - case s if (constantOpRegFull.match(s)): 1; - case s if (oneParamOpRegFull.match(s)): 1 + left.length(depth); - default: 1 + left.length(depth) + right.length(depth); - } - } - - - /* - * returns true if other term is equal in data and structure + * reduce a fraction * - */ - public function isEqual(t:TermNode, ?compareNames=false, ?compareParams=false):Bool + */ + static function simplifyfraction(t:TermNode) { - if ( !compareNames && (isName || t.isName) ) { - if (isName && left != null) return left.isEqual(t, compareNames, compareParams); - if (t.isName && t.left != null) return isEqual(t.left, compareNames, compareParams); - return (isName && t.isName); + var numerator:Array = new Array(); + traverseMultiplication(t.left, numerator); + var denominator:Array = new Array(); + traverseMultiplication(t.right, denominator); + for (n in numerator) { + for (d in denominator) { + if (isEqualAfterSimplify(n, d)) { + numerator.remove(n); + denominator.remove(d); + } + } } - - if ( !compareParams && (isParam || t.isParam) ) { - if (isParam && left != null) return left.isEqual(t, compareNames, compareParams); - if (t.isParam && t.left != null) return isEqual(t.left, compareNames, compareParams); - return (isParam && t.isParam); + if (numerator.length > 1) { + traverseMultiplicationBack(t.left, numerator); } - - var is_equal:Bool = false; - - if (isValue && t.isValue) - is_equal = (value==t.value); - else if ( (isName && t.isName) || (isParam && t.isParam) || (isOperation && t.isOperation) ) - is_equal = (symbol==t.symbol); - - if (left != null) { - if (t.left != null) is_equal = is_equal && left.isEqual(t.left, compareNames, compareParams); - else is_equal = false; + else if (numerator.length == 1) { + t.setOperation('/', numerator.pop(), newValue(1)); } - if (right != null) { - if (t.right != null) is_equal = is_equal && right.isEqual(t.right, compareNames, compareParams); - else is_equal = false; + else if (numerator.length == 0) { + t.left.setValue(1); + } + if (denominator.length > 1) { + traverseMultiplicationBack(t.right, denominator); + } + else if (denominator.length == 1) { + t.setOperation('/', t.left.copy(), denominator.pop()); + } + else if (denominator.length == 0) { + t.right.setValue(1); } - - return is_equal; } /* - * static Function Pointers (to stored in this.operation) - * - */ - static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else throw('Empty function "${t.symbol}".'); - static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else throw('Missing parameter "${t.symbol}".'); - static function opValue(t:TermNode):Float return t.value; + * expands a mathmatical expression recursivly into a polynomial + * + */ + static public function expand(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _expand(tnew); + return tnew; + } - static var MathOp:MapFloat> = [ - // two side operations - "+" => function(t) return t.left.result + t.right.result, - "-" => function(t) return t.left.result - t.right.result, - "*" => function(t) return t.left.result * t.right.result, - "/" => function(t) return t.left.result / t.right.result, - "^" => function(t) return Math.pow(t.left.result, t.right.result), - "%" => function(t) return t.left.result % t.right.result, - - // function without params (constants) - "e" => function(t) return Math.exp(1), - "pi" => function(t) return Math.PI, - - // function with one param - "abs" => function(t) return Math.abs(t.left.result), - "ln" => function(t) return Math.log(t.left.result), - "sin" => function(t) return Math.sin(t.left.result), - "cos" => function(t) return Math.cos(t.left.result), - "tan" => function(t) return Math.tan(t.left.result), - "cot" => function(t) return 1/Math.tan(t.left.result), - "asin" => function(t) return Math.asin(t.left.result), - "acos" => function(t) return Math.acos(t.left.result), - "atan" => function(t) return Math.atan(t.left.result), + static function _expand(t:TermNode):Void { - // function with two params - "atan2"=> function(t) return Math.atan2(t.left.result, t.right.result), - "log" => function(t) return Math.log(t.right.result) / Math.log(t.left.result), - "max" => function(t) return Math.max(t.left.result, t.right.result), - "min" => function(t) return Math.min(t.left.result, t.right.result), - ]; - - static var twoSideOp_ = "^,/,*,-,+,%"; // <- order here determines the operator precedence - static var constantOp_ = "e,pi"; // functions without parameters like "e() or pi()" - static var oneParamOp_ = "abs,ln,sin,cos,tan,cot,asin,acos,atan"; // functions with one parameter like "sin(2)" - static var twoParamOp_ = "atan2,log,max,min"; // functions with two parameters like "max(a,b)" - - static public var twoSideOp :Array = twoSideOp_.split(','); - static public var constantOp:Array = constantOp_.split(','); - static public var oneParamOp:Array = oneParamOp_.split(','); - static public var twoParamOp:Array = twoParamOp_.split(','); - - static var precedence:Map = [ for (i in 0...twoSideOp.length) twoSideOp[i] => i ]; - + var len:Int = -1; + var len_old:Int = 0; + while(len != len_old) { + if (t.symbol == '*') { + expandStep(t); + } + else { + if(t.left != null) { + _expand(t.left); + } + if(t.right != null) { + _expand(t.right); + + } + } + len_old = len; + len = t.length(); + } + } - /* - * Regular Expressions for parsing + * expands a mathmatical expression into a polynomial -> use only if top symbol=* * - */ - static var clearSpacesReg:EReg = ~/\s+/g; - static var trailingSpacesReg:EReg = ~/^(\s+)/; - - static var numberReg:EReg = ~/^([-+]?\d+\.?\d*)/; - static var paramReg:EReg = ~/^([a-z]+)/i; - - static var constantOpReg:EReg = new EReg("^(" + constantOp.join("|") + ")\\(\\)" , "i"); - static var oneParamOpReg:EReg = new EReg("^(" + oneParamOp.join("|") + ")\\(" , "i"); - static var twoParamOpReg:EReg = new EReg("^(" + twoParamOp.join("|") + ")\\(" , "i"); - static var twoSideOpReg: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")" , ""); - - static public var constantOpRegFull:EReg = new EReg("^(" + constantOp.join("|") + ")$" , "i"); - static public var oneParamOpRegFull:EReg = new EReg("^(" + oneParamOp.join("|") + ")$" , "i"); - static public var twoParamOpRegFull:EReg = new EReg("^(" + twoParamOp.join("|") + ")$" , "i"); - static public var twoSideOpRegFull: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")$" , ""); - - static var nameReg:EReg = ~/^([a-z]+)(\s*[:=]\s*)/i; - static var nameRegFull:EReg = ~/^([a-z]+)$/i; - - static var signReg:EReg = ~/^([-+\s]+)/i; + */ + static function expandStep(t:TermNode):Void + { + var left:TermNode = t.left; + var right:TermNode = t.right; - public static inline function trailingSpaces(s:String):Int { - if (trailingSpacesReg.match(s)) return(trailingSpacesReg.matched(1).length); - else return 0; + if (left.symbol == "+" || left.symbol == "-") { + if (right.symbol == "+" || right.symbol == "-") { + if (left.symbol == "+" && right.symbol == "+") { // (a+b)*(c+d) + t.setOperation('+', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "+" && right.symbol == "-") { // (a+b)*(c-d) + t.setOperation('+', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "+") { // (a-b)*(c+d) + t.setOperation('-', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "-") { // (a-b)*(c-d) + t.setOperation('-', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + } + else + { + if (left.symbol == "+") { // (a+b)*c + t.setOperation('+', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } + else if (left.symbol == "-") { // (a-b)*c + t.setOperation('-', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } + } + } + else if (right.symbol == "+" || right.symbol == "-") { + if (right.symbol == "+") { // a*(b+c) + t.setOperation('+', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); + } + else if (right.symbol == "-") { // a*(b-c) + t.setOperation('-', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); + } + } } + /* - * Build Tree up from String Math Expression - * - */ - public static inline function fromString(s:String, ?bindings:Map):TermNode { - var errPos:Int = 0; - errPos = trailingSpaces(s); s = s.substr(errPos); - //s = clearSpacesReg.replace(s, ''); // clear all whitespaces - if (nameReg.match(s)) { - var name:String = nameReg.matched(1); - s = s.substr(name.length + nameReg.matched(2).length); - errPos += name.length + nameReg.matched(2).length; - if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); - return newName(name, parseString(s, errPos, bindings)); - } - if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); - return parseString(s, errPos, bindings); + * factorize a term: a*c+a*b -> a*(c+b) + * + */ + static public function factorize(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _factorize(tnew); + return tnew; } - static function parseString(s:String, errPos:Int, ?params:Map):TermNode { - var t:TermNode = null; - var operations:Array = new Array(); - var e, f:String; - var negate:Bool; - var spaces:Int = 0; + static function _factorize(t:TermNode):Void { + var mult_matrix:Array> = new Array(); + var add:Array = new Array(); - while (s.length != 0) // read in terms from left - { - negate = false; - - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - - if (numberReg.match(s)) { // float number - e = numberReg.matched(1); - t = newValue(Std.parseFloat(e)); - } - else if (constantOpReg.match(s)) { // like e() or pi() - e = constantOpReg.matched(1); - t = newOperation(e); - e+= "()"; - } - else if (oneParamOpReg.match(s)) { // like sin(...) - f = oneParamOpReg.matched(1); errPos += f.length; - s = "("+oneParamOpReg.matchedRight(); - e = getBrackets(s, errPos); - t = newOperation(f, parseString(e.substring(1, e.length - 1), errPos+1, params) ); - } - else if (twoParamOpReg.match(s)) { // like atan2(... , ...) - f = twoParamOpReg.matched(1); errPos += f.length; - s = "("+twoParamOpReg.matchedRight(); - e = getBrackets(s, errPos); - var p1:String = e.substring(1, comataPos); - var p2:String = e.substring(comataPos + 1, e.length - 1); - if (comataPos == -1) throw({"msg":f+"() needs two parameter separated by comma.","pos":errPos}); - t = newOperation(f, parseString(p1, errPos+1, params), parseString(p2, errPos+1 + comataPos, params) ); - } - else if (paramReg.match(s)) { // parameter - e = paramReg.matched(1); - t = newParam(e, (params==null) ? null : params.get(e)); - } - else if (signReg.match(s)) { // start with +- - e = signReg.matched(1); - s = s.substr(e.length); errPos += e.length; - e = ~/[\s+]/g.replace(e, ''); - if (e.length % 2 > 0) { - //s = "0-" + s; - if (numberReg.match(s)) { // followed by float number - e = numberReg.matched(1); - t = newValue(-Std.parseFloat(e)); - } else { // negative signed - t = newValue(0); - s = "-" + s; - e = ""; - negate = true; - } - } else continue; // positive signed - } - else if (twoSideOpReg.match(s)) { // start with other two side op - throw({"msg":"Missing left operand.","pos":errPos}); + // build matrix - addition in columns - multiplication in rows + traverseAddition(t, add); + var add_length_old:Int = 0; + for(i in add) { + if(i.symbol == "-") { + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1].right, mult_matrix[mult_matrix.length-1]); } else { - e = getBrackets(s, errPos); // term inside brackets - t = parseString(e.substring(1, e.length - 1), errPos+1, params); - } - - s = s.substr(e.length); errPos += e.length; - - if (operations.length > 0) operations[operations.length - 1].right = t; - - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - - if (twoSideOpReg.match(s)) { // two side operation symbol - e = twoSideOpReg.matched(1); errPos += e.length; - s = twoSideOpReg.matchedRight(); - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - operations.push( { symbol:e, left:t, right:null, leftOperation:null, rightOperation:null, precedence:((negate) ? -1 :precedence.get(e)) } ); - if (operations.length > 1) { - operations[operations.length - 2].rightOperation = operations[operations.length - 1]; - operations[operations.length - 1].leftOperation = operations[operations.length - 2]; - } - } else if (s.length > 0) { - if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.","pos":errPos}); - if (!(s.indexOf("(") == 0 || numberReg.match(s) || paramReg.match(s) || constantOpReg.match(s) || oneParamOpReg.match(s) || twoParamOpReg.match(s))) - throw({"msg":"Wrong char.","pos":errPos}); - else throw({"msg":"Missing operation.","pos":errPos}); + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1], mult_matrix[mult_matrix.length-1]); } } - if ( operations.length > 0 ) { - if ( operations[operations.length-1].right == null ) throw({"msg":"Missing right operand.","pos":errPos-spaces}); - else { - operations.sort(function(a:OperationNode, b:OperationNode):Int - { - if (a.precedence < b.precedence) return -1; - if (a.precedence > b.precedence) return 1; - return 0; - }); - for (op in operations) { - t = TermNode.newOperation(op.symbol, op.left, op.right); - if (op.leftOperation != null && op.rightOperation != null) { - op.rightOperation.leftOperation = op.leftOperation; - op.leftOperation.rightOperation = op.rightOperation; - } - if (op.leftOperation != null) op.leftOperation.right = t; - if (op.rightOperation != null) op.rightOperation.left = t; + // find and extract common factors + var part_of_all:Array = new Array(); + factorize_extract_common(mult_matrix, part_of_all); + if(part_of_all.length != 0) { + var new_add:Array = new Array(); + var helper:TermNode = new TermNode(); + for(i in mult_matrix) { + traverseMultiplicationBack(helper, i); + var v:TermNode = new TermNode(); + v.set(helper); + new_add.push(v); + } + for(i in 0...add.length) { + if(add[i].symbol == '-' && add[i].left.value == 0) { + new_add[i].setOperation('-', newValue(0), new_add[i].copy()); } - return t; } + + t.setOperation('*', new TermNode(), new TermNode()); + traverseMultiplicationBack(t.left, part_of_all); + traverseAdditionBack(t.right, new_add); } - else return t; } - static var comataPos:Int; - static function getBrackets(s:String, errPos:Int):String { - var pos:Int = 1; - if (s.indexOf("(") == 0) // check that s starts with opening bracket - { - if (~/^\(\s*\)/.match(s)) throw({"msg":"Empty brackets.", "pos":errPos}); - - var i,j,k:Int; - var openBrackets:Int = 1; - comataPos = -1; - while ( openBrackets > 0 ) - { - i = s.indexOf("(", pos); - j = s.indexOf(")", pos); - - // check for commata position - if (openBrackets == 1 && comataPos == -1) { - k = s.indexOf(",", pos); - if (k0) comataPos = k; + // delete common factors of mult_matrix and add them to part_of_all + static function factorize_extract_common(mult_matrix:Array>, part_of_all:Array):Void { + var bool:Bool = false; + var matrix_length_old:Int = -1; + var i:TermNode=new TermNode(); + var exponentiation_counter:Int = 0; + while(matrix_length_old != mult_matrix[0].length) { + matrix_length_old = mult_matrix[0].length; + for(p in mult_matrix[0]) { + if(p.symbol == '^') { + i.set(p.left); + exponentiation_counter++; } - - if ((i>0 && j>0 && i0 && j<0)) { // found open bracket - openBrackets++; pos = i + 1; + else if(p.symbol == '-' && p.left.isValue && p.left.value == 0) { + i.set(p.right); + } + else { + i.set(p); + } + for(j in 1...mult_matrix.length) { + bool = false; + for(h in mult_matrix[j]) { + if(isEqualAfterSimplify(h, i)) { + bool = true; + break; + } + else if(h.symbol == '^' && isEqualAfterSimplify(h.left , i)) { + bool=true; + exponentiation_counter++; + break; + + } + else if(h.symbol == '-' && h.left.isValue && h.left.value == 0 && isEqualAfterSimplify(h.right, i)) { + bool=true; + break; + } + } + if(bool == false) { + break; + } } - else if ((j>0 && i>0 && j0 && i<0)) { // found close bracket - openBrackets--; pos = j + 1; - } else { // no close or open found - throw({"msg":"Wrong bracket nesting.","pos":errPos}); + if(bool == true && exponentiation_counter < mult_matrix.length) { + part_of_all.push(new TermNode()); + part_of_all[part_of_all.length-1].set(i); + var helper:TermNode = new TermNode(); + helper.set(i); + delete_last_from_matrix(mult_matrix, helper); + break; } } - return s.substring(0, pos); - } - if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.", "pos":errPos}); - else throw({"msg":"Wrong char.","pos":errPos}); - } - - - /* - * Puts out Math Expression as a String - * - */ - public function toString(?depth:Null = null, ?plOut:String = null):String { - var t:TermNode = this; - if (isName) t = left; - var options:Int; - switch (plOut) { - case 'glsl': options = noNeg|forceFloat|forcePow|forceMod|forceLog|forceAtan|forceConst; - default: options = 0; - } - return (left != null || !isName) ? t._toString(depth, options) : ''; - } - // options - public static inline var noNeg:Int = 1; - public static inline var forceFloat:Int = 2; - public static inline var forcePow:Int = 4; - public static inline var forceMod:Int = 8; - public static inline var forceLog:Int = 16; - public static inline var forceAtan:Int = 32; - public static inline var forceConst:Int = 64; - - inline function _toString(depth:Null, options:Int, ?isFirst:Bool=true):String { - if (depth == null) depth = -1; - return switch(symbol) { - case s if (isValue): floatToString(value, options); - //case s if (isName && isFirst): (left == null) ? symbol : left.toString(depth, false); - case s if (isName): (depth == 0 || left == null) ? symbol : left._toString(depth-1, options, false); - case s if (isParam): (depth == 0 || left == null) ? symbol : left._toString(depth-((left.isName)?0:1), options, false); - case s if (twoSideOpRegFull.match(s)) : - if (symbol == "-" && left.isValue && left.value == 0 && options&noNeg == 0) symbol + right._toString(depth, options, false); - else if (symbol == "^" && options&forcePow > 0) 'pow' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else if (symbol == "%" && options&forceMod > 0) 'mod' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else ((isFirst)?"":"(") + left._toString(depth, options, false) + symbol + right._toString(depth, options, false) + ((isFirst)?'':")"); - case s if (twoParamOpRegFull.match(s)): - if (symbol == "log" && options&forceLog > 0) "(log(" + right._toString(depth, options) + ")/log(" + left._toString(depth, options) + "))"; - else if (symbol == "atan2" && options&forceAtan > 0) "atan(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else symbol + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - case s if (constantOpRegFull.match(s)): - if (symbol == "pi" && options & forceConst > 0) Std.string(Math.PI); - else if (symbol == "e" && options&forceConst > 0) Std.string(Math.exp(1)); - else symbol + "()"; - default: - if (symbol == "ln" && options&forceLog > 0) 'log' + "(" + left._toString(depth, options) + ")"; - else symbol + "(" + left._toString(depth, options) + ")"; } } - inline function floatToString(value:Float, ?options:Int = 0):String { - var s:String = Std.string(value); - if (options&forceFloat > 0 && s.indexOf('.') == -1) s += ".0"; - return s; - } - - /* - * enrolls terms and subterms for debugging - * - */ - public function debug() { - //TODO - var out:String = "";// "(" + depth() + ")"; - for (i in 0 ... depth()+1) { - if (i == 0) out += ((name != null) ? name : "?") + " = "; else out += " -> "; - out += toString(i); + // deletes d from every row in mult_matrix once + static function delete_last_from_matrix(mult_matrix:Array>, d:TermNode):Void { + for(i in mult_matrix) { + if(i.length>1) { + for(j in 1...i.length+1) { + if(isEqualAfterSimplify(i[i.length-j], d)) { // a*x -> a + for(h in 0...j-1) { + i[i.length-j+h].set(i[i.length-j+h+1]); + } + i.pop(); + break; + } + else if(i[i.length-j].symbol == '^' && isEqualAfterSimplify(i[i.length-j].left, d)) { // x^n -> x^(n-1) + i[i.length-j].right.set(newOperation('-', i[i.length-j].right.copy(), newValue(1))); + break; + } + else if(i[i.length-j].symbol == '-' && i[i.length-j].left.isValue && i[i.length-j].left.value == 0 && isEqualAfterSimplify(i[i.length-j].right, d)) { + i[i.length-j].right.set(newValue(1)); + break; + } + } + } + else if(i[0].symbol == '^' && isEqualAfterSimplify(i[0].left, d)) { // x^n -> x^(n-1) + i[0].right.set(newOperation('-', i[0].right.copy(), newValue(1))); + } + else { + i[0].set(newValue(1)); + } } - trace(out); - } - - /* - * packs a TermNode and all sub-terms into Bytes - * - */ - public function toBytes():Bytes { - var b = new BytesOutput(); - _toBytes(b); - return b.getBytes(); } - inline function _toBytes(b:BytesOutput) { - // optimize (later to do): needs only 3 bit per TermNode type! - if (isValue) { - b.writeByte(0); - b.writeFloat(value); - } - else if (isName) { - b.writeByte((left!=null) ? 1:2); - _writeString(symbol, b); - if (left!=null) left._toBytes(b); + // compare function for Array.sort() + static function formsort_compare(t1:TermNode, t2:TermNode):Int + { + if (formsort_priority(t1) > formsort_priority(t2)) { + return -1; } - else if (isParam) { - b.writeByte((left!=null) ? 3:4); - _writeString(symbol, b); - if (left!=null) left._toBytes(b); + else if (formsort_priority(t1) < formsort_priority(t2)) { + return 1; } - else if (isOperation) { - b.writeByte(5); - var i:Int = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp))).indexOf(symbol); - if (i > -1) { - b.writeByte(i); - if (oneParamOpRegFull.match(symbol)) left._toBytes(b); - else if (twoSideOpRegFull.match(symbol) || twoParamOpRegFull.match(symbol) ) { - left._toBytes(b); - right._toBytes(b); + else{ + if (t1.isValue && t2.isValue) { + if (t1.value >= t2.value) { + return(-1); + } + else{ + return(1); + } + } + else if (t1.isOperation && t2.isOperation) { + if(t1.right != null && t2.right != null) { + return(formsort_compare(t1.right, t2.right)); + } + else { + return(formsort_compare(t1.left, t2.left)); } } - else throw("Error in _toBytes"); + else return 0; } - else throw("Error in _toBytes"); - } - - inline function _writeString(s:String, b:BytesOutput):Void { - b.writeByte((s.length<255) ? s.length: 255); - for (i in 0...((s.length<255) ? s.length: 255)) b.writeByte(symbol.charCodeAt(i)); - } - /* - * unserialize packed Bytes-Term to create a TermNode structure - * - */ - public static function fromBytes(b:Bytes):TermNode { - return _fromBytes(new BytesInput(b)); } - - static inline function _fromBytes(b:BytesInput):TermNode { - return switch (b.readByte()) { - case 0: TermNode.newValue(b.readFloat()); - case 1: TermNode.newName( _readString(b), _fromBytes(b) ); - case 2: TermNode.newName( _readString(b) ); - case 3: TermNode.newParam( _readString(b), _fromBytes(b) ); - case 4: TermNode.newParam( _readString(b) ); - case 5: - var op:String = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp)))[b.readByte()]; - if (oneParamOpRegFull.match(op)) TermNode.newOperation( op, _fromBytes(b) ); - else if (twoSideOpRegFull.match(op) || twoParamOpRegFull.match(op) ) TermNode.newOperation( op, _fromBytes(b), _fromBytes(b) ); - else TermNode.newOperation( op ); - default: throw("Error in _fromBytes"); + + // priority function for formsort_compare() + static function formsort_priority(t:TermNode):Float + { + return switch(t.symbol) + { + case s if (t.isParam): t.symbol.charCodeAt(0); + case s if (t.isName): t.symbol.charCodeAt(0); + case s if (t.isValue): 1+0.00001*t.value; + case s if (TermNode.twoSideOpRegFull.match(s)) : + if(t.symbol == '-' && t.left.value == 0) { + formsort_priority(t.right); + } + else { + formsort_priority(t.left)+formsort_priority(t.right)*0.001; + } + case s if (TermNode.oneParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.indexOf(s); + case s if (TermNode.twoParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.indexOf(s); + case s if (TermNode.constantOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.indexOf(s); + + default: -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.length; } } - - static inline function _readString(b:BytesInput):String { - var len:Int = b.readByte(); - var s:String = ""; - for (i in 0...len) s += String.fromCharCode(b.readByte()); - return s; - } - - - /************************************************************************************** - * * - * various math operations transformation and more. * - * * - * * - **************************************************************************************/ - + /* - * creates a new term that is derivate of a given term + * sort a Tree consisting of products * */ - public function derivate(paramName:String):TermNode return TermDerivate.derivate(this, paramName); - + static function arrangeMultiplication(t:TermNode):Void + { + var mult:Array = new Array(); + traverseMultiplication(t, mult); + mult.sort(formsort_compare); + traverseMultiplicationBack(t, mult); + } + /* - * Simplify: trims the length of a math expression - * + * sort a Tree consisting of addition and subtraction + * */ - public function simplify():TermNode return TermTransform.simplify(this); + static function arrangeAddition(t:TermNode):Void + { + var addlength_old:Int = -1; + var add:Array = new Array(); + traverseAddition(t, add); + add.sort(formsort_compare); + while(add.length != addlength_old) { + addlength_old = add.length; + for(i in 0...add.length-1) { + if(isEqualAfterSimplify(add[i], add[i+1])) { + add[i].setOperation('*', add[i].copy(), newValue(2)); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].symbol == '*' && add[i+1].symbol == '*' && add[i].right.isValue && add[i+1].right.isValue && isEqualAfterSimplify(add[i].left, add[i+1].left)) { + add[i].right.setValue(add[i].right.value+add[i+1].right.value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].isValue && add[i+1].isValue) { + add[i].setValue(add[i].value+add[i+1].value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if((add[i].symbol == '-' && add[i].left.isValue && add[i].left.value == 0 && isEqualAfterSimplify(add[i].right, add[i+1])) || (add[i+1].symbol == '-' && add[i+1].left.isValue && add[i+1].left.value == 0 && isEqualAfterSimplify(add[i+1].right, add[i]))) { + for(j in 0...add.length-i-2) { + add[i+j] = add[i+j+2]; + } + add.pop(); + add.pop(); + if(add.length == 0){ + add.push(newValue(0)); + } + break; + } + } - /* - * expands a mathmatical expression recursivly into a polynomial - * - */ - public function expand():TermNode return TermTransform.expand(this); + if(add[0].symbol == '-' && add[0].left.value == 0) { + for(i in add) { + if(i.symbol == '-' && i.left.value == 0) { + i.set(i.right); + } + else { + i.setOperation('-', newValue(0), i.copy()); + } + } + t.setOperation('-', newValue(0), new TermNode()); + traverseAdditionBack(t.right, add); + return; + } + + } - /* - * factorizes a mathmatical expression - * - */ - public function factorize():TermNode return TermTransform.factorize(this); + traverseAdditionBack(t, add); + } + } + /** - * abstract wrapper around TermNode + * solving one-dimensional, definite integrals RAM-efficiently using the trapezoidal rule + * by samusake * - * by Sylvio Sell, Rostock 2017 - */ + **/ +class Integrate { -@:forward( name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) -abstract Formula(TermNode) from TermNode to TermNode -{ - /** - Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - @param formulaString the String that representing the math expression - **/ - inline public function new(formulaString:String) { - this = TermNode.fromString(formulaString); - } - - /** - Copy all from another Formula to this (keeps the own name if it is defined) - Keeps the bindings where this formula is linked into by a parameter. - @param formula the source formula from where the value is copyed - **/ - public inline function set(formula:Formula):Formula return this.set(formula); + static public inline function trapz(f:Formula, int_var:String, lower_bound:Float, higher_bound:Float, int_points:Int):Float { + var x:Formula, int:Float, x_low:Float, dx:Float, helper:Float; + x_low = lower_bound; + x=1.0;//x_low; + x.name = int_var; + f.bind(x); + int = 0; + dx = (higher_bound - lower_bound) / int_points; - /** - Link a variable inside of this formula to another formula - @param formula formula that will be linked into - @param paramName (optional) name of the variable to link with (e.g. if formula have no or different name) - **/ - public function bind(formula:Formula, ?paramName:String):Formula { - if (paramName != null) { - TermNode.checkValidName(paramName); - return this.bind( [paramName => formula] ); - } - else { - if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; - return this.bind( [formula.name => formula] ); - } - } - - /** - Link variables inside of this formula to another formulas - @param formulas array of formulas to link to variables - @param paramNames (optional) names of the variables to link with (e.g. if formulas have no or different names) - **/ - public function bindArray(formulas:Array, ?paramNames:Array):Formula { - var map = new Map(); - if (paramNames != null) { - if (paramNames.length != formulas.length) throw 'paramNames need to have the same length as formulas for bindArray().'; - for (i in 0...formulas.length) { - TermNode.checkValidName(paramNames[i]); - map.set(paramNames[i], formulas[i]); - } - } - else { - for (formula in formulas) { - if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; - map.set(formula.name, formula); - } + for (i in 0...int_points) { + helper = f.result; + x_low += dx; + x.set(x_low); + int += (helper + f.result) / 2.0 * dx; } - return this.bind(map); - } - - /** - Link variables inside of this formula to another formulas - @param formulaMap map of formulas where the keys have same names as the variables to link with - **/ - public inline function bindMap(formulaMap:Map):Formula { - return this.bind(formulaMap); - } - - // ------------ unbind ------------- - - /** - Delete all connections of the linked formula - @param formula formula that has to be unlinked - **/ - public inline function unbind(formula:Formula):Formula { - return this.unbindTerm( [formula] ); - } - - /** - Delete all connections of the linked formulas - @param formulas array of formulas that has to be unlinked - **/ - public function unbindArray(formulas:Array):Formula { - return this.unbindTerm(formulas); - } - /** - Delete all connections to linked formulas for a given variable name - @param paramName name of the variable where the connected formula has to unlink from - **/ - public inline function unbindParam(paramName:String):Formula { - TermNode.checkValidName(paramName); - return this.unbind( [paramName] ); - } - - /** - Delete all connections to linked formulas for the given variable names - @param paramNames array of variablenames where the connected formula has to unlink from - **/ - public inline function unbindParamArray(paramNames:Array):Formula { - return this.unbind(paramNames); + return(int); } - // ----------------------------------- - - /** - Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - @param depth (optional) how deep the variable-bindings should be resolved - @param plOut (optional) creates formula for a special language (only "glsl" at now) - **/ - inline public function toString(?depth:Null = null, ?plOut:String = null):String return this.toString(depth, plOut); +} - /** - Creates a formula from a packet Bytes representation - **/ - inline public static function fromBytes(b:Bytes):Formula return TermNode.fromBytes(b); - - @:to inline public function toStr():String return this.toString(0); - @:to inline public function toFloat():Float return this.result; - - @:from static public function fromString(a:String):Formula return TermNode.fromString(a); - @:from static public function fromFloat(a:Float):Formula return TermNode.newValue(a); - static inline function twoSideOp(op:String, a:Formula, b:Formula ):Formula { - return TermNode.newOperation( op, - (a.name != null ) ? TermNode.newParam(a.name, a) : a, - (b.name != null ) ? TermNode.newParam(b.name, b) : b - ); - } - @:op(A + B) static public function add (a:Formula, b:Formula):Formula return twoSideOp('+', a, b); - @:op(A - B) static public function subtract(a:Formula, b:Formula):Formula return twoSideOp('-', a, b); - @:op(A * B) static public function multiply(a:Formula, b:Formula):Formula return twoSideOp('*', a, b); - @:op(A / B) static public function divide (a:Formula, b:Formula):Formula return twoSideOp('/', a, b); - @:op(A ^ B) static public function potenz (a:Formula, b:Formula):Formula return twoSideOp('^', a, b); - @:op(A % B) static public function modulo (a:Formula, b:Formula):Formula return twoSideOp('%', a, b); +/** + * expetions for try/catch errorhandling + * + * by Sylvio Sell, Rostock 2022 + */ - public static inline function atan2(a:Formula, b:Formula):Formula return twoSideOp('atan2', a, b); - public static inline function log (a:Formula, b:Formula):Formula return twoSideOp('log', a, b); - public static inline function max (a:Formula, b:Formula):Formula return twoSideOp('max', a, b); - public static inline function min (a:Formula, b:Formula):Formula return twoSideOp('min', a, b); +#if (haxe_ver >= "4.1.0") - static inline function oneParamOp(op:String, a:Formula):Formula { - return TermNode.newOperation( op, - (a.name != null ) ? TermNode.newParam(a.name, a) : a - ); +class FormulaException extends haxe.Exception +{ + public function new(msg:String, pos:Int) { + super(msg); + this.pos = pos; } - public static inline function abs (a:Formula):Formula return oneParamOp('abs', a); - public static inline function ln (a:Formula):Formula return oneParamOp('ln', a); - public static inline function sin (a:Formula):Formula return oneParamOp('sin', a); - public static inline function cos (a:Formula):Formula return oneParamOp('cos', a); - public static inline function tan (a:Formula):Formula return oneParamOp('tan', a); - public static inline function cot (a:Formula):Formula return oneParamOp('cot', a); - public static inline function asin(a:Formula):Formula return oneParamOp('asin', a); - public static inline function acos(a:Formula):Formula return oneParamOp('acos', a); - public static inline function atan(a:Formula):Formula return oneParamOp('atan', a); + public var msg(get, never):String; + inline function get_msg() return message; + + public var pos:Int; } -class MathExpressionNode extends LogicNode { +#else - public var property0: String; // Expression - public var property1: Bool; // Clamp +typedef FormulaException = Dynamic; - public function new(tree: LogicTree) { - super(tree); - } +#end - override function get(from: Int): Dynamic { - var r: Float = 0.0; - var exp: String = property0; - // Variable - var a: Float = inputs[0].get(); - var b: Float = inputs[1].get(); - exp = StringTools.replace(exp, 'a', Std.string(a)); - exp = StringTools.replace(exp, 'b', Std.string(b)); - var c: Float = 0.0; - var d: Float = 0.0; - var e: Float = 0.0; - var x: Float = 0.0; - var y: Float = 0.0; - var h: Float = 0.0; - var i: Float = 0.0; - var k: Float = 0.0; - var i = 2; - while (i < inputs.length) { - switch (i) { - case 2: - c = inputs[i].get(); - exp = StringTools.replace(exp, 'c', Std.string(c)); - case 3: - d = inputs[i].get(); - exp = StringTools.replace(exp, 'd', Std.string(d)); - case 4: - e = inputs[i].get(); - exp = StringTools.replace(exp, 'e', Std.string(e)); - case 5: - x = inputs[i].get(); - exp = StringTools.replace(exp, 'x', Std.string(x)); - case 6: - y = inputs[i].get(); - exp = StringTools.replace(exp, 'y', Std.string(y)); - case 7: - h = inputs[i].get(); - exp = StringTools.replace(exp, 'h', Std.string(h)); - case 8: - i = inputs[i].get(); - exp = StringTools.replace(exp, 'i', Std.string(i)); - case 9: - k = inputs[i].get(); - exp = StringTools.replace(exp, 'k', Std.string(k)); - } - i++; - } - // Expression - try { - var f: Formula = new Formula(exp); - r = f.result; - } catch(msg: String) { - #if arm_debug - trace(msg); - #end - } - // Clamp - if (property1) r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r); - return r; +/** + * collect all error messages + * + * by Sylvio Sell, Rostock 2022 + */ + +class ErrorMsg +{ + static inline function error(msg:String, pos:Int = 0) { + #if (haxe_ver >= "4.1.0") + throw new FormulaException(msg, pos); + #else + throw({msg:msg, pos:pos}); + #end } + + + // --------------- Formula ---------------- + + public static inline function cantBindUnnamed(formula:Formula, bindFormula:Formula) + error('Can\'t bind unnamed formula: \'${bindFormula.toString()}\'. Specify the available parameter: \'${formula.params().join("\' ,\'")}\' or name the formula to one.'); + + public static inline function bindArrayWrongLengths(nf:Int, np:Int) + error('The array-length of formulas ($nf) have to be the same as paramNames ($np) for bindArray().'); + + + + // --------------- TermNode --------------- + + public static inline function noValidOperation(s:String) + error('"$s" is no valid operation.'); + + public static inline function wrongCharInsideName(name:String) + error('Wrong character inside name "$name".'); + + public static inline function emptyFunction(s:String) + error('Empty function "$s".'); + + public static inline function missingParameter(s:String) + error('Missing parameter "$s".'); + + + // formula parsing + public static inline function cantParseFromEmptyString(pos:Int) + error("Can't parse Term from empty string.", pos); + + public static inline function operatorNeedTwoArgs(op:String, pos:Int) + error('Operation "$op()" needs two arguments separated by comma.', pos); + + public static inline function missingLeftOperand(pos:Int) + error("Missing left operand.", pos); + + public static inline function noOpeningBracket(pos:Int) + error("No opening bracket.", pos); + + public static inline function wrongChar(pos:Int) + error("Wrong char.", pos); + + public static inline function missingOperation(pos:Int) + error("Missing operation.", pos); + + public static inline function missingRightOperand(pos:Int) + error("Missing right operand.", pos); + + public static inline function emptyBrackets(pos:Int) + error("Empty brackets.", pos); + + public static inline function wrongBracketNesting(pos:Int) + error("Wrong bracket nesting.", pos); + + + // by en/decoding to/from Bytes + public static inline function intoBytes() + error("Can't encode into Bytes."); + + public static inline function fromBytes() + error("Can't decode from Bytes."); + + + + // --------------- TermDerivate --------------- + + public static inline function notImplementedFor(s:String) + error('Derivation of "$s" is not implemented.'); + } \ No newline at end of file From 10fb58e93527b09d19080e7e10f73c24eea6ad41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 26 Aug 2023 23:47:23 +0200 Subject: [PATCH 053/175] Fix too bright environment and barely visible shadows if irradiance is off --- Shaders/deferred_light/deferred_light.frag.glsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index 3407822df7..60c1efc9ba 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -233,7 +233,7 @@ void main() { if (g2.b < 0.5) { envl = envl; } else { - envl = vec3(1.0); + envl = vec3(0.0); } #endif @@ -241,7 +241,7 @@ void main() { envl /= PI; #endif #else - vec3 envl = vec3(1.0); + vec3 envl = vec3(0.0); #endif #ifdef _Rad From 99bde84ac090b45fa027a9a9215beb8008ad7745 Mon Sep 17 00:00:00 2001 From: yvain Date: Sun, 27 Aug 2023 18:09:53 +0200 Subject: [PATCH 054/175] more steps for rayCast --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 8628965f2e..b34bb333f6 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -25,7 +25,7 @@ vec3 hitCoord; float depth; const int numBinarySearchSteps = 7; -const int maxSteps = 18; +#define maxSteps int(ceil(1.0 / ssrRayStep) * ssrSearchDist) vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); From cf7f5a369dca61b108d4d0f8b9fc7c6c91be46eb Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sun, 3 Sep 2023 17:47:53 +0200 Subject: [PATCH 055/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index e9f05298f5..30ebdbdeda 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.8' +arm_version = '2023.9' arm_commit = '$Id$' def get_project_html5_copy(self): From 69c651078e3e91ce92f4c28b57acf82e283ac32b Mon Sep 17 00:00:00 2001 From: Sylvio Sell Date: Sat, 9 Sep 2023 14:26:52 +0200 Subject: [PATCH 056/175] optimize MathExpressionNode to parse formula-expr. only at start, fix python part to allow all math-functions (no more error-check) --- .../armory/logicnode/MathExpressionNode.hx | 72 ++++-- .../arm/logicnode/math/LN_math_expression.py | 209 +++++------------- 2 files changed, 98 insertions(+), 183 deletions(-) diff --git a/Sources/armory/logicnode/MathExpressionNode.hx b/Sources/armory/logicnode/MathExpressionNode.hx index 08f046b71b..49c7ff136d 100644 --- a/Sources/armory/logicnode/MathExpressionNode.hx +++ b/Sources/armory/logicnode/MathExpressionNode.hx @@ -3,43 +3,67 @@ package armory.logicnode; import haxe.io.Bytes; import haxe.io.BytesInput; import haxe.io.BytesOutput; +import haxe.ds.Vector; class MathExpressionNode extends LogicNode { - public var property0: String; // Expression - public var property1: Bool; // Clamp - + var formula:Formula; + var paramFormula:Vector; + + public var property0:Bool; // Clamp + public var property1:Int; // Number of params + + public var property2(default, set):String; // Formula Expression + + function set_property2(s:String):String { + try { + formula = Formula.fromString(s); + paramFormula = new Vector(property1); + // trace("number of params:", property1); + for (i in 0...property1) { + var p:Formula = 0.0; + p.name = String.fromCharCode(97+i); + paramFormula.set(i, p); + if( formula.hasParam(p.name) ) { + // trace("bind to param:", p.name); + formula.bind( paramFormula.get(i) ); + } + } + } + catch(msg: String) { + #if arm_debug + trace("Math Expression Node - " + msg); + #end + } + return property2 = s; + } + + public function new(tree: LogicTree) { super(tree); } - - - static inline var paramNames = "abcdexyhijk"; override function get(from: Int): Dynamic { var result:Float = 0.0; - var expression:String = property0; - - // Expression - try { - var formula:Formula = new Formula(expression); - - // bind variables - for (i in 0...inputs.length) { - formula.bind( (inputs[i].get():Float), paramNames.substr(i, 1) ); + + // Expression + try { + // set new values to param-subformulas + for (i in 0...property1) { + // paramFormula.get(i).set( (inputs[i].get():Float) ); + paramFormula.get(i).left.value = inputs[i].get(); // optimized } - - result = formula.result; - - } catch(msg: String) { + result = formula.result; + } + catch(msg:String) { #if arm_debug - trace(msg); + trace("Math Expression Node - " + msg); #end - } + } // Clamp - if (property1) result = result < 0.0 ? 0.0 : (result > 1.0 ? 1.0 : result); - + if (property0) result = result < 0.0 ? 0.0 : (result > 1.0 ? 1.0 : result); + return result; } } @@ -56,7 +80,7 @@ class MathExpressionNode extends LogicNode { by Sylvio Sell, Rostock 2023 */ -@:forward( name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) +@:forward( value, left, name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) abstract Formula(TermNode) from TermNode to TermNode { /** diff --git a/blender/arm/logicnode/math/LN_math_expression.py b/blender/arm/logicnode/math/LN_math_expression.py index 0795d6a810..a301599912 100644 --- a/blender/arm/logicnode/math/LN_math_expression.py +++ b/blender/arm/logicnode/math/LN_math_expression.py @@ -5,185 +5,76 @@ class MathExpressionNode(ArmLogicTreeNode): """Mathematical operations on values.""" bl_idname = 'LNMathExpressionNode' bl_label = 'Math Expression' - arm_version = 1 - min_inputs = 2 - max_inputs = 10 + arm_version = 2 - @staticmethod - def get_variable_name(index): - return { - 0: 'a', - 1: 'b', - 2: 'c', - 3: 'd', - 4: 'e', - 5: 'x', - 6: 'y', - 7: 'h', - 8: 'i', - 9: 'k' - }.get(index, 'a') - - @staticmethod - def get_clear_exp(value): - return re.sub(r'[\-\+\*\/\(\)\^\%abcdexyhik0123456789. ]', '', value).strip() - - @staticmethod - def get_invalid_characters(value): - value = value.replace(' ', '') - len_v = len(value) - arg = ['a', 'b', 'c', 'd', 'e', 'x', 'y', 'h', 'i', 'k'] - for i in range(len_v): - s = value[i] - if s == '.': - if ((i - 1) < 0) or ((i + 1) >= len_v) or (not value[i - 1].isnumeric()) or (not value[i + 1].isnumeric()): - return False - oper = ['+', '-', '*', '/', '%', '^'] - if s == '(': - if (i > 0) and ((value[i - 1] not in oper) and (value[i - 1] != '(')): - return False - if (i < (len_v - 1)) and ((value[i + 1] not in arg) and (not value[i + 1].isnumeric()) and (value[i + 1] != '(')): - return False - if s == ')': - if (i > 0) and ((value[i - 1] not in arg) and (not value[i - 1].isnumeric()) and (value[i - 1] != ')')): - return False - if (i < (len_v - 1)) and (not value[i + 1].isnumeric()) and ((value[i + 1] not in oper) and (value[i + 1] != ')')): - return False - if s in oper: - if ((i > 0) and (value[i - 1] in oper)) or ((i < (len_v - 1)) and (value[i + 1] in oper)): - return False - last_sym = value[len_v - 1] - if (not last_sym.isnumeric()) and (last_sym not in arg) and (last_sym != ')'): - return False - return True - - @staticmethod - def check_variable(self, value): - variables = re.sub(r'[\-\+\*\/\(\)\^\%0123456789. ]', '', value).strip() - for vr in variables: - check = False - for inp_key in self.inputs.keys(): - if (vr == inp_key): - check = True - break - if not check: - return False - return True - - @staticmethod - def matches(line, opendelim='(', closedelim=')'): - stack = [] - for m in re.finditer(r'[{}{}]'.format(opendelim, closedelim), line): - pos = m.start() - if line[pos-1] == '\\': - # Skip escape sequence - continue - c = line[pos] - if c == opendelim: - stack.append(pos+1) - elif c == closedelim: - if len(stack) > 0: - prevpos = stack.pop() - yield (True, prevpos, pos, len(stack)) - else: - # Error - yield (False, 0, 0, 0) - pass - if len(stack) > 0: - for pos in stack: - yield (False, 0, 0, 0) - - @staticmethod - def isPartCorrect(s): - if len(s.replace('p', '').replace(' ', '').split()) == 0: - return True - REGEX = re.compile(r"(([abcdexyhikp]|\d+)[\+\-\/\*\^\%]){1,}([abcdexyhikp]|\d+)(=([abcdexyhikp]|\d+))?") - result = False - if REGEX.match(s): - result = True - return result + num_params: IntProperty(default=2, min=0) @staticmethod - def isCorrect(self, s): - result = True - if s.find("(") >=0 or s.find(")") >= 0: - for correct, openpos, closepos, level in self.matches(s): - if correct: - part = s[openpos:closepos] - if part.find("(") == -1 and part.find(")") == -1: - if not self.isPartCorrect(part): - result = False - break - part = s[openpos-1:closepos+1] - replaced = s.replace(part, "p") - if replaced.find("(") >=0 or replaced.find(")") >= 0: - if not self.isCorrect(self, replaced): - result = False - break - else: - if not self.isPartCorrect(replaced): - result = False - break - else: - result = False - break - else: - result = self.isPartCorrect(s) - return result - - def set_exp_error(self, value): - self['exp_error'] = value - - def get_exp_error(self): - return self.get('exp_error', False) + def get_variable_name(index): + return chr( range(ord('a'), ord('z')+1)[index] ) def set_exp(self, value): - value = value.lower() - self['property0'] = value - # Check errors - val_error = False - if len(self.get_clear_exp(value)) > 0: - val_error = True - elif not self.get_invalid_characters(value): - val_error = True - elif not self.check_variable(self, value): - val_error = True - elif not self.isCorrect(self, value.replace(' ', '')): - val_error = True - self.set_exp_error(val_error) + self['property2'] = value + # TODO: Check expression for errors + self['exp_error'] = False def get_exp(self): - return self.get('property0', 'a + b') + return self.get('property2', 'a + b') - property0: HaxeStringProperty('property0', name='', description='Expression (operation: +, -, *, /, ^, (, ), %)', set=set_exp, get=get_exp) - property1: HaxeBoolProperty('property1', name='Clamp', default=False) + property0: HaxeBoolProperty('property0', name='Clamp Result', default=False) + property1: HaxeIntProperty('property1', name='Number of Params', default=2) + property2: HaxeStringProperty('property2', name='', description='Math Expression: +, -, *, /, ^, %, (, ), log(a, b), ln(a), abs(a), max(a,b), min(a,b), sin(a), cos(a), tan(a), cot(a), asin(a), acos(a), atan(a), atan2(a,b), pi(), e()', set=set_exp, get=get_exp) def __init__(self): - array_nodes[str(id(self))] = self + super(MathExpressionNode, self).__init__() + self.register_id() + def arm_init(self, context): + + # OUTPUTS: + self.add_output('ArmFloatSocket', 'Result') + + # two default parameters at start self.add_input('ArmFloatSocket', self.get_variable_name(0), default_value=0.0) self.add_input('ArmFloatSocket', self.get_variable_name(1), default_value=0.0) - self.add_output('ArmFloatSocket', 'Result') + + def add_sockets(self): + if self.num_params < 26: + self.add_input('ArmFloatSocket', self.get_variable_name(self.num_params), default_value=0.0) + self.num_params += 1 + self['property1'] = self.num_params + + def remove_sockets(self): + if self.num_params > 0: + self.inputs.remove(self.inputs.values()[-1]) + self.num_params -= 1 + self['property1'] = self.num_params def draw_buttons(self, context, layout): - layout.prop(self, 'property1') - # Expression + # Clamp Property + layout.prop(self, 'property0') + + # Expression Property row = layout.row(align=True) column = row.column(align=True) - column.alert = self.get_exp_error() - column.prop(self, 'property0', icon='FORCE_HARMONIC') - # Buttons + # TODO: + #column.alert = self['exp_error'] + column.prop(self, 'property2', icon='FORCE_HARMONIC') + + # Button ADD parameter row = layout.row(align=True) column = row.column(align=True) - op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) - if len(self.inputs) == 10: + op = column.operator('arm.node_call_func', text='Add Param', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_sockets' + if self.num_params == 26: column.enabled = False - op.node_index = str(id(self)) - op.socket_type = 'ArmFloatSocket' - op.name_format = self.get_variable_name(len(self.inputs)) + + # Button REMOVE parameter column = row.column(align=True) - op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) - op.node_index = str(id(self)) - if len(self.inputs) == 2: + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'remove_sockets' + if self.num_params == 0: column.enabled = False From fca9511534bb8996b0e7e86f302b17854e9a41d8 Mon Sep 17 00:00:00 2001 From: yvain Date: Tue, 12 Sep 2023 13:18:23 +0200 Subject: [PATCH 057/175] squash history. --- .editorconfig | 11 + .vscode/settings.json | 4 +- .../deferred_light/deferred_light.frag.glsl | 4 +- Shaders/ssr_pass/ssr_pass.frag.glsl | 55 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 171 +- Shaders/std/brdf.glsl | 3 +- Shaders/std/conetrace.glsl | 2 +- .../translucent_resolve.frag.glsl | 1 - Shaders/water_pass/water_pass.frag.glsl | 6 +- Sources/armory/logicnode/CreateMapNode.hx | 2 +- Sources/armory/logicnode/DrawArcNode.hx | 4 + Sources/armory/logicnode/DrawCircleNode.hx | 4 + Sources/armory/logicnode/DrawCurveNode.hx | 4 + Sources/armory/logicnode/DrawEllipseNode.hx | 4 + Sources/armory/logicnode/DrawPolygonNode.hx | 4 + .../logicnode/DrawTextAreaStringNode.hx | 6 +- .../armory/logicnode/MathExpressionNode.hx | 3472 +++++++++-------- .../logicnode/NetworkHttpRequestNode.hx | 4 +- Sources/armory/logicnode/RotationMathNode.hx | 16 +- Sources/armory/logicnode/SetVisibleNode.hx | 51 +- .../armory/logicnode/SpawnObjectByNameNode.hx | 1 - Sources/armory/logicnode/SpawnObjectNode.hx | 1 - Sources/armory/renderpath/Inc.hx | 3 - .../armory/renderpath/RenderPathDeferred.hx | 2 +- .../armory/renderpath/RenderPathForward.hx | 69 +- .../trait/physics/bullet/DebugDrawHelper.hx | 224 ++ .../trait/physics/bullet/PhysicsHook.hx | 8 +- .../trait/physics/bullet/PhysicsWorld.hx | 189 +- .../armory/trait/physics/bullet/SoftBody.hx | 282 +- Sources/armory/ui/Canvas.hx | 2 + blender/arm/exporter.py | 29 +- blender/arm/exporter_opt.py | 24 +- blender/arm/handlers.py | 13 + blender/arm/logicnode/arm_node_group.py | 4 +- blender/arm/logicnode/arm_nodes.py | 4 +- blender/arm/logicnode/arm_sockets.py | 4 +- .../arm/logicnode/math/LN_math_expression.py | 209 +- .../logicnode/miscellaneous/LN_group_input.py | 4 +- .../miscellaneous/LN_group_output.py | 4 +- .../logicnode/object/LN_set_object_visible.py | 17 +- blender/arm/logicnode/tree_variables.py | 8 +- blender/arm/make.py | 7 +- blender/arm/make_renderpath.py | 4 +- blender/arm/material/cycles.py | 18 +- .../arm/material/cycles_nodes/nodes_color.py | 51 +- .../material/cycles_nodes/nodes_converter.py | 21 +- .../arm/material/cycles_nodes/nodes_shader.py | 5 +- blender/arm/material/make_mesh.py | 23 +- blender/arm/material/mat_batch.py | 9 +- blender/arm/material/mat_utils.py | 2 +- blender/arm/material/node_meta.py | 2 +- blender/arm/nodes_logic.py | 48 +- blender/arm/props.py | 45 +- blender/arm/props_bake.py | 79 +- blender/arm/props_exporter.py | 8 +- blender/arm/props_lod.py | 24 +- blender/arm/props_properties.py | 24 +- blender/arm/props_renderpath.py | 26 +- blender/arm/props_tilesheet.py | 42 +- blender/arm/props_traits.py | 81 +- blender/arm/props_traits_props.py | 16 +- blender/arm/props_ui.py | 260 +- blender/arm/utils.py | 12 +- blender/arm/write_data.py | 37 +- 64 files changed, 3283 insertions(+), 2490 deletions(-) create mode 100644 .editorconfig create mode 100644 Sources/armory/trait/physics/bullet/DebugDrawHelper.hx diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..9c9dbaeff2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +tab_width = 4 +indent_style = tab + +[*.py] +indent_size = 4 +tab_width = 4 +indent_style = space diff --git a/.vscode/settings.json b/.vscode/settings.json index 7153bbb6d1..6778ea66a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ { "editor.detectIndentation": false, "editor.tabSize": 4, - "editor.insertSpaces": false + "editor.insertSpaces": false, "[python]": { "editor.insertSpaces": true - }, + } } diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index 3407822df7..60c1efc9ba 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -233,7 +233,7 @@ void main() { if (g2.b < 0.5) { envl = envl; } else { - envl = vec3(1.0); + envl = vec3(0.0); } #endif @@ -241,7 +241,7 @@ void main() { envl /= PI; #endif #else - vec3 envl = vec3(1.0); + vec3 envl = vec3(0.0); #endif #ifdef _Rad diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index aa8eecceaa..ba193cb399 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -24,16 +24,15 @@ out vec4 fragColor; vec3 hitCoord; float depth; -const int numBinarySearchSteps = 7; -const int maxSteps = 18; +#define maxSteps int(ceil(1.0 / ssrRayStep) * ssrSearchDist) vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); projectedCoord.xy /= projectedCoord.w; projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; - #ifdef _InvY +#ifdef _InvY projectedCoord.y = 1.0 - projectedCoord.y; - #endif +#endif return projectedCoord.xy; } @@ -45,27 +44,27 @@ float getDeltaDepth(const vec3 hit) { vec4 binarySearch(vec3 dir) { float ddepth; - for (int i = 0; i < numBinarySearchSteps; i++) { + for (int i = 0; i < 7; i++) { dir *= 0.5; hitCoord -= dir; ddepth = getDeltaDepth(hitCoord); if (ddepth < 0.0) hitCoord += dir; } // Ugly discard of hits too far away - #ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); - #else - if (abs(ddepth) > ssrSearchDist / 500) return vec4(0.0); - #endif +#ifdef _CPostprocess + if (abs(ddepth) > PPComp9.z) return vec4(0.0); +#else + if (abs(ddepth) > ssrSearchDist) return vec4(0.0); +#endif return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } vec4 rayCast(vec3 dir) { - #ifdef _CPostprocess - dir *= PPComp9.x; - #else - dir *= ssrRayStep; - #endif +#ifdef _CPostprocess + dir *= PPComp9.x; +#else + dir *= ssrRayStep; +#endif for (int i = 0; i < maxSteps; i++) { hitCoord += dir; if (getDeltaDepth(hitCoord) > 0.0) return binarySearch(dir); @@ -92,27 +91,25 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 reflected = normalize(reflect(viewPos, viewNormal)); + vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; - #ifdef _CPostprocess - vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; - #else - vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; - #endif +#ifdef _CPostprocess + vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; +#else + vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; +#endif - // * max(ssrMinRayStep, -viewPos.z) vec4 coords = rayCast(dir); - vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); - float reflectivity = 1.0 - roughness; - #ifdef _CPostprocess - float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; - #else - float intensity = pow(reflectivity, ssrFalloffExp) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((ssrSearchDist - length(viewPos - hitCoord)) * (1.0 / ssrSearchDist), 0.0, 1.0) * coords.w; - #endif + +#ifdef _CPostprocess + float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; +#else + float intensity = pow(reflectivity, ssrFalloffExp) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((ssrSearchDist - length(viewPos - hitCoord)) * (1.0 / ssrSearchDist), 0.0, 1.0) * coords.w; +#endif intensity = clamp(intensity, 0.0, 1.0); vec3 reflCol = textureLod(tex, coords.xy, 0.0).rgb; diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index d587c01c9c..2088b8fcb2 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -1,4 +1,3 @@ -//refraction from modified reflection by Yvain. #version 450 #include "compiled.inc" @@ -13,7 +12,7 @@ uniform sampler2D tex1; uniform sampler2D gbufferD; uniform sampler2D gbuffer0; uniform sampler2D gbufferD1; -uniform sampler2D gbuffer_refraction; //ior\opacity +uniform sampler2D gbuffer_refraction; // ior\opacity uniform mat4 P; uniform mat3 V3; uniform vec2 cameraProj; @@ -28,105 +27,103 @@ vec3 hitCoord; float depth; vec3 viewPos; - -const int numBinarySearchSteps = 7; -const int maxSteps = 18; +#define maxSteps int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist) vec2 getProjectedCoord(const vec3 hit) { - vec4 projectedCoord = P * vec4(hit, 1.0); - projectedCoord.xy /= projectedCoord.w; - projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; - #ifdef _InvY - projectedCoord.y = 1.0 - projectedCoord.y; - #endif - return projectedCoord.xy; + vec4 projectedCoord = P * vec4(hit, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; +#ifdef _InvY + projectedCoord.y = 1.0 - projectedCoord.y; +#endif + return projectedCoord.xy; } float getDeltaDepth(const vec3 hit) { - float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(-viewRay, depth, cameraProj); - return viewPos.z - hit.z; + float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); + return viewPos.z - hit.z; } -/* vec4 binarySearch(vec3 dir) { - float d; - for (int i = 0; i < numBinarySearchSteps; i++) { - dir *= 0.5; - hitCoord -= dir; - d = getDeltaDepth(hitCoord); - if(d < depth) - hitCoord += dir; - } - // Ugly discard of hits too far away - #ifdef _CPostprocess - if (abs(d) > PPComp9.z) return vec4(texCoord, 0.0, 1.0); - #else - if (abs(d) > ss_refractionSearchDist) return vec4(texCoord, 0.0, 1.0); - #endif - return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); + float d; + for (int i = 0; i < 7; i++) { + dir *= 0.5; + hitCoord -= dir; + d = getDeltaDepth(hitCoord); + if (d < 0.0) + hitCoord += dir; + } + // Ugly discard of hits too far away +#ifdef _CPostprocess + if (abs(d) > PPComp9.z) return vec4(texCoord, 0.0, 1.0); +#else + if (abs(d) > ss_refractionSearchDist) return vec4(texCoord, 0.0, 1.0); +#endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } -*/ vec4 rayCast(vec3 dir) { - float d; - #ifdef _CPostprocess - dir *= PPComp9.x; - #else - dir *= ss_refractionRayStep; - #endif - for (int i = 0; i < maxSteps; i++) { - hitCoord += dir; - d = getDeltaDepth(hitCoord); - if(d > depth) return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); - } - return vec4(texCoord, 0.0, 1.0); + float d; +#ifdef _CPostprocess + dir *= PPComp9.x; +#else + dir *= ss_refractionRayStep; +#endif + for (int i = 0; i < maxSteps; i++) { + hitCoord += dir; + d = getDeltaDepth(hitCoord); + if (d > 0.0) return vec4(getProjectedCoord(dir), 0.0, 1.0); + } + return vec4(texCoord, 0.0, 1.0); } void main() { - vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); - float roughness = g0.z; - vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0); - float ior = gr.x; - float opac = gr.y; - - depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; - - if(depth == 1.0 || ior == 1.0 || opac == 1.0) { - fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; - return; - } - - vec2 enc = g0.rg; - vec3 n; - n.z = 1.0 - abs(enc.x) - abs(enc.y); - n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); + vec4 g0 = textureLod(gbuffer0, texCoord, 0.0); + float roughness = g0.z; + vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0); + float ior = gr.x; + float opac = gr.y; + + depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + + if (depth == 1.0 || ior == 1.0 || opac == 1.0) { + fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; + return; + } + + vec2 enc = g0.rg; + vec3 n; + n.z = 1.0 - abs(enc.x) - abs(enc.y); + n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); n = normalize(n); - vec3 viewNormal = V3 * n; - vec3 viewPos = getPosView(-viewRay, depth, cameraProj); - vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); - hitCoord = -viewPos; - - #ifdef _CPostprocess - vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; - #else - vec3 dir = refracted * (1.0 - rand(texCoord) * ss_refractionJitter * roughness) * 2.0; - #endif - - vec4 coords = rayCast(dir); - vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); - float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); - - float refractivity = 1.0; - #ifdef _CPostprocess - float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; - #else - float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((ss_refractionSearchDist - length(viewPos - hitCoord)) * (1.0 / ss_refractionSearchDist), 0.0, 1.0) * coords.w; - #endif - - intensity = clamp(intensity, 0.0, 1.0); - vec3 refractionCol = textureLod(tex1, coords.xy, 0.0).rgb; - refractionCol = clamp(refractionCol, 0.0, 1.0); - fragColor.rgb = mix(refractionCol * intensity, textureLod(tex, texCoord.xy, 0.0).rgb, opac); + vec3 viewNormal = V3 * n; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); + hitCoord = viewPos; + +#ifdef _CPostprocess + vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; +#else + vec3 dir = refracted * (1.0 - rand(texCoord) * ss_refractionJitter * roughness) * 2.0; +#endif + + vec4 coords = rayCast(dir); + vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); + float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); + + float refractivity = 1.0; + +#ifdef _CPostprocess + float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; +#else + float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((ss_refractionSearchDist - length(viewPos - hitCoord)) * (1.0 / ss_refractionSearchDist), 0.0, 1.0) * coords.w; +#endif + + intensity = clamp(intensity, 0.0, 1.0); + vec3 refractionCol = textureLod(tex1, coords.xy, 0.0).rgb; + refractionCol = clamp(refractionCol, 0.0, 1.0); + vec3 color = textureLod(tex, texCoord.xy, 0.0).rgb; + fragColor.rgb = mix(refractionCol * intensity + color, color, opac); } diff --git a/Shaders/std/brdf.glsl b/Shaders/std/brdf.glsl index d6f2def2da..cce227e893 100644 --- a/Shaders/std/brdf.glsl +++ b/Shaders/std/brdf.glsl @@ -29,7 +29,8 @@ float g2_approx(const float NdotL, const float NdotV, const float alpha) float d_ggx(const float nh, const float a) { float a2 = a * a; - float denom = pow(nh * nh * (a2 - 1.0) + 1.0, 2.0); + float denom = nh * nh * (a2 - 1.0) + 1.0; + denom = max(denom * denom, 0.00006103515625 /* 2^-14 = smallest possible half float value, prevent div by zero */); return a2 * (1.0 / 3.1415926535) / denom; } diff --git a/Shaders/std/conetrace.glsl b/Shaders/std/conetrace.glsl index b63fb80ede..a8c8c0b7fe 100644 --- a/Shaders/std/conetrace.glsl +++ b/Shaders/std/conetrace.glsl @@ -50,7 +50,7 @@ float traceConeAO(sampler3D voxels, const vec3 origin, vec3 dir, const float ape float traceConeAOShadow(sampler3D voxels, const vec3 origin, vec3 dir, const float aperture, const float maxDist, const float offset) { dir = normalize(dir); float sampleCol = 0.0; - float dist = 1.5 * VOXEL_SIZE * voxelgiOffset * 2.5; // + float dist = 1.5 * VOXEL_SIZE * voxelgiOffset * 2.5; float diam = dist * aperture; vec3 samplePos; while (sampleCol < 1.0 && dist < maxDist) { diff --git a/Shaders/translucent_resolve/translucent_resolve.frag.glsl b/Shaders/translucent_resolve/translucent_resolve.frag.glsl index 33d8140b3f..52962545e9 100644 --- a/Shaders/translucent_resolve/translucent_resolve.frag.glsl +++ b/Shaders/translucent_resolve/translucent_resolve.frag.glsl @@ -3,7 +3,6 @@ #include "compiled.inc" - uniform sampler2D gbuffer0; // accum uniform sampler2D gbuffer1; // revealage diff --git a/Shaders/water_pass/water_pass.frag.glsl b/Shaders/water_pass/water_pass.frag.glsl index 332c444288..0e34e92c87 100644 --- a/Shaders/water_pass/water_pass.frag.glsl +++ b/Shaders/water_pass/water_pass.frag.glsl @@ -38,8 +38,8 @@ out vec4 fragColor; vec3 hitCoord; float depth; -const int numBinarySearchSteps = 16; -const int maxSteps = 16; +const int numBinarySearchSteps = 7; +#define maxSteps int(ceil(1.0 / ssrRayStep) * ssrSearchDist) vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); @@ -152,7 +152,7 @@ void main() { vec3 viewNormal = n2; vec3 viewPos = getPosView(viewRay, gdepth, cameraProj); - vec3 reflected = normalize(reflect(viewPos, viewNormal)); + vec3 reflected = reflect(viewPos, viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Sources/armory/logicnode/CreateMapNode.hx b/Sources/armory/logicnode/CreateMapNode.hx index f06fc7b35b..6a230b986c 100644 --- a/Sources/armory/logicnode/CreateMapNode.hx +++ b/Sources/armory/logicnode/CreateMapNode.hx @@ -7,7 +7,7 @@ import armory.system.Event; class CreateMapNode extends LogicNode { public var property0: String; public var property1: String; - public var map: Map; + public var map: Dynamic; public function new(tree:LogicTree) { diff --git a/Sources/armory/logicnode/DrawArcNode.hx b/Sources/armory/logicnode/DrawArcNode.hx index 9ac22b79bd..29620aa346 100644 --- a/Sources/armory/logicnode/DrawArcNode.hx +++ b/Sources/armory/logicnode/DrawArcNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawArcNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawArcNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawArcNode"); final colorVec = inputs[1].get(); @@ -30,6 +33,7 @@ class DrawArcNode extends LogicNode { } else { RenderToTexture.g.drawArc(cx, cy, radius, sAngle, eAngle, inputs[3].get(), ccw, segments); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawCircleNode.hx b/Sources/armory/logicnode/DrawCircleNode.hx index 822efd4caa..0501e5e7e2 100644 --- a/Sources/armory/logicnode/DrawCircleNode.hx +++ b/Sources/armory/logicnode/DrawCircleNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawCircleNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawCircleNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawCircleNode"); final colorVec = inputs[1].get(); @@ -28,6 +31,7 @@ class DrawCircleNode extends LogicNode { else { RenderToTexture.g.drawCircle(cx, cy, radius, inputs[3].get(), segments); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawCurveNode.hx b/Sources/armory/logicnode/DrawCurveNode.hx index 213bfd1914..df3176de59 100644 --- a/Sources/armory/logicnode/DrawCurveNode.hx +++ b/Sources/armory/logicnode/DrawCurveNode.hx @@ -3,7 +3,9 @@ package armory.logicnode; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawCurveNode extends LogicNode { @@ -12,6 +14,7 @@ class DrawCurveNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawCurveNode"); final colorVec = inputs[1].get(); @@ -22,6 +25,7 @@ class DrawCurveNode extends LogicNode { [inputs[5].get(), inputs[7].get(), inputs[9].get(), inputs[11].get()], inputs[3].get(), inputs[2].get() ); + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawEllipseNode.hx b/Sources/armory/logicnode/DrawEllipseNode.hx index 85adf67e23..7b37b79ab8 100644 --- a/Sources/armory/logicnode/DrawEllipseNode.hx +++ b/Sources/armory/logicnode/DrawEllipseNode.hx @@ -4,7 +4,9 @@ import iron.math.Vec4; import kha.Color; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawEllipseNode extends LogicNode { @@ -13,6 +15,7 @@ class DrawEllipseNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawEllipseNode"); final colorVec: Vec4 = inputs[1].get(); @@ -41,6 +44,7 @@ class DrawEllipseNode extends LogicNode { RenderToTexture.g.rotate(-angle, cx, cy); RenderToTexture.g.scale(1.0, scaleInv); + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawPolygonNode.hx b/Sources/armory/logicnode/DrawPolygonNode.hx index da2a3cf387..a84a904ca0 100644 --- a/Sources/armory/logicnode/DrawPolygonNode.hx +++ b/Sources/armory/logicnode/DrawPolygonNode.hx @@ -4,7 +4,9 @@ import kha.Color; import kha.math.Vector2; import armory.renderpath.RenderToTexture; +#if arm_ui using zui.GraphicsExtension; +#end class DrawPolygonNode extends LogicNode { static inline var numStaticInputs = 6; @@ -16,6 +18,7 @@ class DrawPolygonNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawPolygonNode"); if (vertices == null) { @@ -43,6 +46,7 @@ class DrawPolygonNode extends LogicNode { } else { RenderToTexture.g.drawPolygon(inputs[4].get(), inputs[5].get(), vertices, inputs[3].get()); } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/DrawTextAreaStringNode.hx b/Sources/armory/logicnode/DrawTextAreaStringNode.hx index 4a524df5e3..dd9a812623 100644 --- a/Sources/armory/logicnode/DrawTextAreaStringNode.hx +++ b/Sources/armory/logicnode/DrawTextAreaStringNode.hx @@ -7,10 +7,10 @@ import armory.renderpath.RenderToTexture; import kha.graphics2.VerTextAlignment; import kha.graphics2.HorTextAlignment; -using zui.GraphicsExtension; - #if arm_ui import armory.ui.Canvas; + +using zui.GraphicsExtension; #end class DrawTextAreaStringNode extends LogicNode { @@ -29,6 +29,7 @@ class DrawTextAreaStringNode extends LogicNode { } override function run(from: Int) { + #if arm_ui RenderToTexture.ensure2DContext("DrawTextAreaStringNode"); var string:String = Std.string(inputs[1].get()); @@ -124,6 +125,7 @@ class DrawTextAreaStringNode extends LogicNode { ++index; } + #end runOutput(0); } diff --git a/Sources/armory/logicnode/MathExpressionNode.hx b/Sources/armory/logicnode/MathExpressionNode.hx index b2c4e39644..49c7ff136d 100644 --- a/Sources/armory/logicnode/MathExpressionNode.hx +++ b/Sources/armory/logicnode/MathExpressionNode.hx @@ -1,1142 +1,523 @@ package armory.logicnode; + import haxe.io.Bytes; import haxe.io.BytesInput; import haxe.io.BytesOutput; +import haxe.ds.Vector; -/** - * extending TermNode with various math operations transformation and more. - * by Sylvio Sell, Rostock 2017 - * - **/ - -class TermTransform { +class MathExpressionNode extends LogicNode { - static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; - static var newValue:Float->TermNode = TermNode.newValue; - - /* - * Simplify: trims the length of a math expression - * - */ - static public inline function simplify(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _simplify(tnew); - return tnew; - } - - static inline function _simplify(t:TermNode):Void { - _expand(t); - - var len:Int = -1; - var len_old:Int = 0; - while (len != len_old) { - if (t.isName && t.left != null) { - simplifyStep(t.left); - } - else { - simplifyStep(t); + var formula:Formula; + var paramFormula:Vector; + + public var property0:Bool; // Clamp + public var property1:Int; // Number of params + + public var property2(default, set):String; // Formula Expression + + function set_property2(s:String):String { + try { + formula = Formula.fromString(s); + paramFormula = new Vector(property1); + // trace("number of params:", property1); + for (i in 0...property1) { + var p:Formula = 0.0; + p.name = String.fromCharCode(97+i); + paramFormula.set(i, p); + if( formula.hasParam(p.name) ) { + // trace("bind to param:", p.name); + formula.bind( paramFormula.get(i) ); + } } - len_old = len; - len = t.length(); } + catch(msg: String) { + #if arm_debug + trace("Math Expression Node - " + msg); + #end + } + return property2 = s; } - // TODO: removing this calls in subfunctions could be better to understand algorithms-recursions !!! - static function isEqualAfterSimplify(t1:TermNode, t2:TermNode):Bool { - // ----> take care, _simplify changes both TermNodes on call !!! - _simplify(t1); - _simplify(t2); - return t1.isEqual(t2, false, true); + + public function new(tree: LogicTree) { + super(tree); } - static function simplifyStep(t:TermNode):Void { - if (!t.isOperation) return; + override function get(from: Int): Dynamic { + var result:Float = 0.0; - if (t.left != null) { - if (t.left.isValue) { - if (t.right == null) { - // setValue(result); // calculate operation with one value - return; - } - else if (t.right.isValue) { - t.setValue(t.result); // calculate result of operation with values on both sides - return; - } + // Expression + try { + // set new values to param-subformulas + for (i in 0...property1) { + // paramFormula.get(i).set( (inputs[i].get():Float) ); + paramFormula.get(i).left.value = inputs[i].get(); // optimized } + result = formula.result; } + catch(msg:String) { + #if arm_debug + trace("Math Expression Node - " + msg); + #end + } + + // Clamp + if (property0) result = result < 0.0 ? 0.0 : (result > 1.0 ? 1.0 : result); + + return result; + } +} - switch(t.symbol) { - case '+': - if (t.left.isValue && t.left.value == 0) t.copyNodeFrom(t.right); // 0+a -> a - else if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a+0 -> a - else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)+ln(b) -> ln(a*b) - t.setOperation('ln', - newOperation('*', t.left.left.copy(), t.right.left.copy()) - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { - t.setOperation('/', // a/b+c/b -> (a+c)/b - newOperation('+', t.left.left.copy(), t.right.left.copy()), - t.left.right.copy() - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/') { // a/b+c/d -> (a*d+c*b)/(b*d) - t.setOperation('/', - newOperation('+', - newOperation('*', t.left.left.copy(), t.right.right.copy()), - newOperation('*', t.right.left.copy(), t.left.right.copy()) - ), - newOperation('*', t.left.right.copy(), t.right.right.copy()) - ); - } - arrangeAddition(t); - if(t.symbol == '+') { - _factorize(t); - } - - case '-': - if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a-0 -> a - else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)-ln(b) -> ln(a/b) - t.setOperation('ln', - newOperation('/', t.left.left.copy(), t.right.left.copy()) - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { - t.setOperation('/', // a/b-c/b -> (a-c)/b - newOperation('-', t.left.left.copy(), t.right.left.copy()), - t.left.right.copy() - ); - } - else if (t.left.symbol == '/' && t.right.symbol == '/') { //a/b-c/d -> (a*d-c*b)/(b*d) - t.setOperation('/', - newOperation('-', - newOperation('*', t.left.left.copy(), t.right.right.copy()), - newOperation('*', t.right.left.copy(), t.left.right.copy()) - ), - newOperation('*', t.left.right.copy(), t.right.right.copy()) - ); - } - arrangeAddition(t); - if(t.symbol == '-') { - _factorize(t); - } - case '*': - if (t.left.isValue) { - if (t.left.value == 1) t.copyNodeFrom(t.right); // 1*a -> a - else if (t.left.value == 0) t.setValue(0); // 0*a -> 0 - } - else if (t.right.isValue) { - if (t.right.value == 1) t.copyNodeFrom(t.left); // a*1 -> a - else if (t.right.value == 0) t.setValue(0); // a*0 -> a - } - else if (t.left.symbol == '/') { // (a/b)*c -> (a*c)/b - t.setOperation('/', - newOperation('*', t.right.copy(), t.left.left.copy()), - t.left.right.copy() - ); - } - else if (t.right.symbol == '/') { // a*(b/c) -> (a*b)/c - t.setOperation('/', - newOperation('*', t.left.copy(), t.right.left.copy()), - t.right.right.copy() - ); - } - else { - arrangeMultiplication(t); - } +// -------------------------------------------------------------------- +// ------------------------ FORMULA ----------------------------------- +// -------------------------------------------------------------------- - case '/': - if (isEqualAfterSimplify(t.left, t.right)) { // x/x -> 1 - t.setValue(1); - } - else { - if (t.left.isValue && t.left.value == 0) t.setValue(0); // 0/a -> 0 - else if (t.right.symbol == '/') { - t.setOperation('/', - newOperation('*', t.right.right.copy(), t.left.copy()), - t.right.left.copy() - ); - } - else if (t.right.isValue && t.right.value == 1) t.copyNodeFrom(t.left); // a/1 -> a - else if (t.left.symbol == '/') { // (1/x)/b -> 1/(x*b) - t.setOperation('/', t.left.left.copy(), - newOperation('*', t.left.right.copy(), t.right.copy()) - ); - } - else if (t.right.symbol == '/') { // b/(1/x) -> b*x - t.setOperation('/', - newOperation('*', t.right.right.copy(), t.left.copy()), - t.right.left.copy() - ); - } - else if (t.left.symbol == '-' && t.left.left.isValue && t.left.left.value == 0) - { - t.setOperation('-', newValue(0), - newOperation('/', t.left.right.copy(), t.right.copy()) - ); - } - else{ // a*b/b -> a - simplifyfraction(t); - } - } +/* + Formula haxe library to handle mathematical expressions at runtime + Version: 0.4.2 + weblink: https://lib.haxe.org/p/formula/ + by Sylvio Sell, Rostock 2023 +*/ - case '^': - if (t.left.isValue) { - if (t.left.value == 1) t.setValue(1); // 1^a -> 1 - else if (t.left.value == 0) t.setValue(0); // 0^a -> 0 - } else if (t.right.isValue) { - if (t.right.value == 1) t.copyNodeFrom(t.left); // a^1 -> a - else if (t.right.value == 0) t.setValue(1); // a^0 -> 1 - } - else if (t.left.symbol == '^') { // (a^b)^c -> a^(b*c) - t.setOperation('^', t.left.left.copy(), - newOperation('*', t.left.right.copy(), t.right.copy()) - ); - } +@:forward( value, left, name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) +abstract Formula(TermNode) from TermNode to TermNode +{ + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - case 'ln': - if (t.left.symbol == 'e') t.setValue(1); - case 'log': - if (isEqualAfterSimplify(t.left, t.right)) { - t.setValue(1); - } - else { - t.setOperation('/', // log(a,b) -> ln(b)/ln(a) - newOperation('ln', t.right.copy()), - newOperation('ln', t.left.copy()) - ); - } - } - if (t.left != null) simplifyStep(t.left); - if (t.right != null) simplifyStep(t.right); + @param formulaString the String that representing the math expression + **/ + inline public function new(formulaString:String) { + this = TermNode.fromString(formulaString); } - - /* - * put all subterms separated by * into an array - * - */ - static function traverseMultiplication(t:TermNode, p:Array) - { - if (t.symbol != "*") { - p.push(t); + + /** + Copy all from another Formula to this (keeps the own name if it is defined) + Keeps the bindings where this formula is linked into by a parameter. + + @param formula the source formula from where the value is copyed + **/ + public inline function set(formula:Formula):Formula return this.set(formula); + + /** + Link a variable inside of this formula to another formula + + @param formula formula that will be linked into + @param paramName (optional) name of the variable to link with (e.g. if formula have no or different name) + **/ + public function bind(formula:Formula, ?paramName:String):Formula { + if (paramName != null) { + TermNode.checkValidName(paramName); + return this.bind( [paramName => formula] ); } else { - traverseMultiplication(t.left, p); - traverseMultiplication(t.right, p); + if (formula.name == null) ErrorMsg.cantBindUnnamed(this, formula); + return this.bind( [formula.name => formula] ); } } - /* - * build tree consisting of multiple * from array - * - */ - static function traverseMultiplicationBack(t:TermNode, p:Array) - { - if (p.length > 2) { - t.setOperation('*', newValue(1), p.pop()); - traverseMultiplicationBack(t.left, p); - } - else if (p.length == 2) { - t.setOperation('*', p[0].copy(), p[1].copy()); - p.pop(); - p.pop(); + /** + Link variables inside of this formula to another formulas + + @param formulas array of formulas to link to variables + @param paramNames (optional) names of the variables to link with (e.g. if formulas have no or different names) + **/ + public function bindArray(formulas:Array, ?paramNames:Array):Formula { + var map = new Map(); + if (paramNames != null) { + if (paramNames.length != formulas.length) ErrorMsg.bindArrayWrongLengths(formulas.length, paramNames.length); + for (i in 0...formulas.length) { + TermNode.checkValidName(paramNames[i]); + map.set(paramNames[i], formulas[i]); + } } else { - t.set(p.pop()); + for (formula in formulas) { + if (formula.name == null) ErrorMsg.cantBindUnnamed(this, formula); + map.set(formula.name, formula); + } } + return this.bind(map); } + + /** + Link variables inside of this formula to another formulas - /* - * put all subterms separated by * into an array - * - */ - static function traverseAddition(t:TermNode, p:Array, ?negative:Bool=false) - { - if (t.symbol == "+" && negative == false) { - traverseAddition(t.left, p); - traverseAddition(t.right, p); - } - else if (t.symbol == "-" && negative == false) { - traverseAddition(t.left, p); - traverseAddition(t.right, p, true); - } - else if (t.symbol == "+" && negative == true) { - traverseAddition(t.left, p, true); - traverseAddition(t.right, p, true); - } - else if (t.symbol == "-" && negative == true) { - traverseAddition(t.left, p, true); - traverseAddition(t.right, p); - } - else if (negative == true && !t.isValue || negative == true && t.isValue && t.value != 0) { - p.push(newOperation('-', newValue(0), t)); - } - else if (!t.isValue || t.isValue && t.value != 0) { - p.push(t); - } - return(p); + @param formulaMap map of formulas where the keys have same names as the variables to link with + **/ + public inline function bindMap(formulaMap:Map):Formula { + return this.bind(formulaMap); } + + // ------------ unbind ------------- + + /** + Delete all connections of the linked formula - /* - * build tree consisting of multiple - and + from array - * - */ - static function traverseAdditionBack(t:TermNode, p:Array) - { - if(p.length > 1) { - if (p[p.length-1].symbol == "-") { - t.set(p.pop()); - } - else { - t.setOperation("+", newValue(0), p.pop()); - } - traverseAdditionBack(t.left, p); - } - else if(p.length == 1){ - t.set(p.pop()); - } + @param formula formula that has to be unlinked + **/ + public inline function unbind(formula:Formula):Formula { + return this.unbindTerm( [formula] ); } + + /** + Delete all connections of the linked formulas - /* - * reduce a fraction - * - */ - static function simplifyfraction(t:TermNode) - { - var numerator:Array = new Array(); - traverseMultiplication(t.left, numerator); - var denominator:Array = new Array(); - traverseMultiplication(t.right, denominator); - for (n in numerator) { - for (d in denominator) { - if (isEqualAfterSimplify(n, d)) { - numerator.remove(n); - denominator.remove(d); - } - } - } - if (numerator.length > 1) { - traverseMultiplicationBack(t.left, numerator); - } - else if (numerator.length == 1) { - t.setOperation('/', numerator.pop(), newValue(1)); - } - else if (numerator.length == 0) { - t.left.setValue(1); - } - if (denominator.length > 1) { - traverseMultiplicationBack(t.right, denominator); - } - else if (denominator.length == 1) { - t.setOperation('/', t.left.copy(), denominator.pop()); - } - else if (denominator.length == 0) { - t.right.setValue(1); - } + @param formulas array of formulas that has to be unlinked + **/ + public function unbindArray(formulas:Array):Formula { + return this.unbindTerm(formulas); + } + + /** + Delete all connections to linked formulas for a given variable name + + @param paramName name of the variable where the connected formula has to unlink from + **/ + public inline function unbindParam(paramName:String):Formula { + TermNode.checkValidName(paramName); + return this.unbind( [paramName] ); } - /* - * expands a mathmatical expression recursivly into a polynomial - * - */ - static public function expand(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _expand(tnew); - return tnew; + /** + Delete all connections to linked formulas for the given variable names + + @param paramNames array of variablenames where the connected formula has to unlink from + **/ + public inline function unbindParamArray(paramNames:Array):Formula { + return this.unbind(paramNames); } + + // ----------------------------------- + + /** + Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula + + @param depth (optional) how deep the variable-bindings should be resolved + @param plOut (optional) creates formula for a special language (only "glsl" at now) + **/ + inline public function toString(?depth:Null = null, ?plOut:String = null):String return this.toString(depth, plOut); + + /** + Creates a formula from a packet Bytes representation + **/ + inline public static function fromBytes(b:Bytes):Formula return TermNode.fromBytes(b); - static function _expand(t:TermNode):Void { - - var len:Int = -1; - var len_old:Int = 0; - while(len != len_old) { - if (t.symbol == '*') { - expandStep(t); - } - else { - if(t.left != null) { - _expand(t.left); - } - if(t.right != null) { - _expand(t.right); - - } - } - len_old = len; - len = t.length(); - } + @:to inline public function toStr():String return this.toString(0); + @:to inline public function toFloat():Float return this.result; + + @:from static public function fromString(a:String):Formula return TermNode.fromString(a); + @:from static public function fromFloat(a:Float):Formula return TermNode.newValue(a); + + static inline function twoSideOp(op:String, a:Formula, b:Formula ):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a, + (b.name != null ) ? TermNode.newParam(b.name, b) : b + ); + } + @:op(A + B) static public function add (a:Formula, b:Formula):Formula return twoSideOp('+', a, b); + @:op(A - B) static public function subtract(a:Formula, b:Formula):Formula return twoSideOp('-', a, b); + @:op(A * B) static public function multiply(a:Formula, b:Formula):Formula return twoSideOp('*', a, b); + @:op(A / B) static public function divide (a:Formula, b:Formula):Formula return twoSideOp('/', a, b); + @:op(A ^ B) static public function potenz (a:Formula, b:Formula):Formula return twoSideOp('^', a, b); + @:op(A % B) static public function modulo (a:Formula, b:Formula):Formula return twoSideOp('%', a, b); + + public static inline function atan2(a:Formula, b:Formula):Formula return twoSideOp('atan2', a, b); + public static inline function log (a:Formula, b:Formula):Formula return twoSideOp('log', a, b); + public static inline function max (a:Formula, b:Formula):Formula return twoSideOp('max', a, b); + public static inline function min (a:Formula, b:Formula):Formula return twoSideOp('min', a, b); + + static inline function oneParamOp(op:String, a:Formula):Formula { + return TermNode.newOperation( op, + (a.name != null ) ? TermNode.newParam(a.name, a) : a + ); } + public static inline function abs (a:Formula):Formula return oneParamOp('abs', a); + public static inline function ln (a:Formula):Formula return oneParamOp('ln', a); + public static inline function sin (a:Formula):Formula return oneParamOp('sin', a); + public static inline function cos (a:Formula):Formula return oneParamOp('cos', a); + public static inline function tan (a:Formula):Formula return oneParamOp('tan', a); + public static inline function cot (a:Formula):Formula return oneParamOp('cot', a); + public static inline function asin(a:Formula):Formula return oneParamOp('asin', a); + public static inline function acos(a:Formula):Formula return oneParamOp('acos', a); + public static inline function atan(a:Formula):Formula return oneParamOp('atan', a); + +} + + +/** + * knot of a Tree to do math operations at runtime + * by Sylvio Sell, Rostock 2017 + * + **/ + +typedef OperationNode = {symbol:String, left:TermNode, right:TermNode, leftOperation:OperationNode, rightOperation:OperationNode, precedence:Int}; + +class TermNode { /* - * expands a mathmatical expression into a polynomial -> use only if top symbol=* + * Properties * */ - static function expandStep(t:TermNode):Void - { - var left:TermNode = t.left; - var right:TermNode = t.right; + var operation:TermNode->Float; // operation function pointer + public var symbol:String; //operator like "+" or parameter name like "x" - if (left.symbol == "+" || left.symbol == "-") { - if (right.symbol == "+" || right.symbol == "-") { - if (left.symbol == "+" && right.symbol == "+") { // (a+b)*(c+d) - t.setOperation('+', - newOperation('+', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('+', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "+" && right.symbol == "-") { // (a+b)*(c-d) - t.setOperation('+', - newOperation('-', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('-', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "-" && right.symbol == "+") { // (a-b)*(c+d) - t.setOperation('-', - newOperation('+', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('+', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - else if (left.symbol == "-" && right.symbol == "-") { // (a-b)*(c-d) - t.setOperation('-', - newOperation('-', - newOperation('*', left.left.copy(), right.left.copy()), - newOperation('*', left.left.copy(), right.right.copy()) - ), - newOperation('-', - newOperation('*', left.right.copy(), right.left.copy()), - newOperation('*', left.right.copy(), right.right.copy()) - ) - ); - } - } - else - { - if (left.symbol == "+") { // (a+b)*c - t.setOperation('+', - newOperation('*', left.left.copy(), right.copy()), - newOperation('*', left.right.copy(), right.copy()) - ); - } - else if (left.symbol == "-") { // (a-b)*c - t.setOperation('-', - newOperation('*', left.left.copy(), right.copy()), - newOperation('*', left.right.copy(), right.copy()) - ); - } - } + public var left:TermNode; // left branch of tree + public var right:TermNode; // right branch of tree + + public var value:Float; // leaf of the tree + + public var name(get, set):String; // name is stored into a param-TermNode at root of the tree + inline function get_name():String return (isName) ? symbol : null; + public static inline function checkValidName(name:String) if (!nameRegFull.match(name)) ErrorMsg.wrongCharInsideName; + inline function set_name(name:String):String { + if (name == null && isName) { + copyNodeFrom(left); } - else if (right.symbol == "+" || right.symbol == "-") { - if (right.symbol == "+") { // a*(b+c) - t.setOperation('+', - newOperation('*', left.copy(), right.left.copy()), - newOperation('*', left.copy(), right.right.copy()) - ); - } - else if (right.symbol == "-") { // a*(b-c) - t.setOperation('-', - newOperation('*', left.copy(), right.left.copy()), - newOperation('*', left.copy(), right.right.copy()) - ); - } + else { + checkValidName(name); + if (isName) symbol = name else setName(name, copyNode()); } + return name; } - + /* - * factorize a term: a*c+a*b -> a*(c+b) - * - */ - static public function factorize(t:TermNode):TermNode { - var tnew:TermNode = t.copy(); - _factorize(tnew); - return tnew; + * returns depth of parameter bindings + * + */ + public inline function depth():Int { + if (isName && left != null) return left._depth(); + else return _depth(); } - - static function _factorize(t:TermNode):Void { - var mult_matrix:Array> = new Array(); - var add:Array = new Array(); - - // build matrix - addition in columns - multiplication in rows - traverseAddition(t, add); - var add_length_old:Int = 0; - for(i in add) { - if(i.symbol == "-") { - mult_matrix.push(new Array()); - traverseMultiplication(add[mult_matrix.length-1].right, mult_matrix[mult_matrix.length-1]); - } - else { - mult_matrix.push(new Array()); - traverseMultiplication(add[mult_matrix.length-1], mult_matrix[mult_matrix.length-1]); - } + public inline function _depth():Int { + var l:Int = 0; + var r:Int = 0; + var d:Int = 0; + if (isParam) { + if (left == null) d = 0; + else if (!left.isName) d = 1; } + else if (isName) d = 1; - // find and extract common factors - var part_of_all:Array = new Array(); - factorize_extract_common(mult_matrix, part_of_all); - if(part_of_all.length != 0) { - var new_add:Array = new Array(); - var helper:TermNode = new TermNode(); - for(i in mult_matrix) { - traverseMultiplicationBack(helper, i); - var v:TermNode = new TermNode(); - v.set(helper); - new_add.push(v); - } - for(i in 0...add.length) { - if(add[i].symbol == '-' && add[i].left.value == 0) { - new_add[i].setOperation('-', newValue(0), new_add[i].copy()); - } - } - - t.setOperation('*', new TermNode(), new TermNode()); - traverseMultiplicationBack(t.left, part_of_all); - traverseAdditionBack(t.right, new_add); - } - } - - // delete common factors of mult_matrix and add them to part_of_all - static function factorize_extract_common(mult_matrix:Array>, part_of_all:Array):Void { - var bool:Bool = false; - var matrix_length_old:Int = -1; - var i:TermNode=new TermNode(); - var exponentiation_counter:Int = 0; - while(matrix_length_old != mult_matrix[0].length) { - matrix_length_old = mult_matrix[0].length; - for(p in mult_matrix[0]) { - if(p.symbol == '^') { - i.set(p.left); - exponentiation_counter++; - } - else if(p.symbol == '-' && p.left.isValue && p.left.value == 0) { - i.set(p.right); - } - else { - i.set(p); - } - for(j in 1...mult_matrix.length) { - bool = false; - for(h in mult_matrix[j]) { - if(isEqualAfterSimplify(h, i)) { - bool = true; - break; - } - else if(h.symbol == '^' && isEqualAfterSimplify(h.left , i)) { - bool=true; - exponentiation_counter++; - break; + if (left != null) l = left._depth(); + if (right != null) r = right._depth(); - } - else if(h.symbol == '-' && h.left.isValue && h.left.value == 0 && isEqualAfterSimplify(h.right, i)) { - bool=true; - break; - } - } - if(bool == false) { - break; - } - } - if(bool == true && exponentiation_counter < mult_matrix.length) { - part_of_all.push(new TermNode()); - part_of_all[part_of_all.length-1].set(i); - var helper:TermNode = new TermNode(); - helper.set(i); - delete_last_from_matrix(mult_matrix, helper); - break; - } - } - } + return( d + ((l>r) ? l : r)); } - // deletes d from every row in mult_matrix once - static function delete_last_from_matrix(mult_matrix:Array>, d:TermNode):Void { - for(i in mult_matrix) { - if(i.length>1) { - for(j in 1...i.length+1) { - if(isEqualAfterSimplify(i[i.length-j], d)) { // a*x -> a - for(h in 0...j-1) { - i[i.length-j+h].set(i[i.length-j+h+1]); - } - i.pop(); - break; - } - else if(i[i.length-j].symbol == '^' && isEqualAfterSimplify(i[i.length-j].left, d)) { // x^n -> x^(n-1) - i[i.length-j].right.set(newOperation('-', i[i.length-j].right.copy(), newValue(1))); - break; - } - else if(i[i.length-j].symbol == '-' && i[i.length-j].left.isValue && i[i.length-j].left.value == 0 && isEqualAfterSimplify(i[i.length-j].right, d)) { - i[i.length-j].right.set(newValue(1)); - break; - } - } - } - else if(i[0].symbol == '^' && isEqualAfterSimplify(i[0].left, d)) { // x^n -> x^(n-1) - i[0].right.set(newOperation('-', i[0].right.copy(), newValue(1))); - } - else { - i[0].set(newValue(1)); - } - } - } - // compare function for Array.sort() - static function formsort_compare(t1:TermNode, t2:TermNode):Int - { - if (formsort_priority(t1) > formsort_priority(t2)) { - return -1; - } - else if (formsort_priority(t1) < formsort_priority(t2)) { - return 1; - } - else{ - if (t1.isValue && t2.isValue) { - if (t1.value >= t2.value) { - return(-1); - } - else{ - return(1); - } - } - else if (t1.isOperation && t2.isOperation) { - if(t1.right != null && t2.right != null) { - return(formsort_compare(t1.right, t2.right)); - } - else { - return(formsort_compare(t1.left, t2.left)); - } - } - else return 0; - } + /* + * Check Type of TermNode + * + */ + public var isName(get, null):Bool; // true -> root TermNode that holds name + inline function get_isName():Bool return Reflect.compareMethods(operation, opName); + + public var isParam(get, null):Bool; // true -> it's a parameter + inline function get_isParam():Bool return Reflect.compareMethods(operation, opParam); + + public var isValue(get, null):Bool; // true -> it's a value (no left and right) + inline function get_isValue():Bool return Reflect.compareMethods(operation, opValue); + + public var isOperation(get, null):Bool; // true -> it's a operation TermNode + inline function get_isOperation():Bool return !(isName||isParam||isValue); + + /* + * Calculates result of all Operations + * throws error if there is unbind param + */ + public var result(get, null):Float; // result of tree calculation + inline function get_result():Float return operation(this); + + /* + * Constructors + * + */ + public function new() {} + + public static inline function newName(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setName(name, term); + return t; } - - // priority function for formsort_compare() - static function formsort_priority(t:TermNode):Float - { - return switch(t.symbol) - { - case s if (t.isParam): t.symbol.charCodeAt(0); - case s if (t.isName): t.symbol.charCodeAt(0); - case s if (t.isValue): 1+0.00001*t.value; - case s if (TermNode.twoSideOpRegFull.match(s)) : - if(t.symbol == '-' && t.left.value == 0) { - formsort_priority(t.right); - } - else { - formsort_priority(t.left)+formsort_priority(t.right)*0.001; - } - case s if (TermNode.oneParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.indexOf(s); - case s if (TermNode.twoParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.indexOf(s); - case s if (TermNode.constantOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.indexOf(s); - - default: -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.length; - } + + public static inline function newParam(name:String, ?term:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setParam(name, term); + return t; + } + + public static inline function newValue(f:Float):TermNode { + var t:TermNode = new TermNode(); + t.setValue(f); + return t; + } + + public static inline function newOperation(s:String, ?left:TermNode, ?right:TermNode):TermNode { + var t:TermNode = new TermNode(); + t.setOperation(s, left, right); + return t; } + /* - * sort a Tree consisting of products + * atomic methods * */ - static function arrangeMultiplication(t:TermNode):Void - { - var mult:Array = new Array(); - traverseMultiplication(t, mult); - mult.sort(formsort_compare); - traverseMultiplicationBack(t, mult); + public inline function set(term:TermNode):TermNode { + // TODO: new param to keep the existing bindings if there is same parameters + if (isName) { + if (!term.isName) left = term.copy(); + else if (term.left != null) left = term.left.copy(); + else left = null; + } + else { + if (!term.isName) copyNodeFrom(term.copy()); + else if (term.left != null) copyNodeFrom(term.left.copy()); + //else return null; // TODO: check if that can ever been! + } + return this; + } + + public inline function setName(name:String, ?term:TermNode) { + operation = opName; + symbol = name; + left = term; right = null; + } + + public inline function setParam(name:String, ?term:TermNode) { + operation = opParam; + symbol = name; + left = term; right = null; + } + + public inline function setValue(f:Float):Void { + operation = opValue; + symbol = null; + value = f; + left = null; right = null; + } + + public inline function setOperation(s:String, ?left:TermNode, ?right:TermNode):Void { + operation = MathOp.get(s); + if (operation != null) + { + symbol = s; + this.left = left; + this.right = right; + } + else ErrorMsg.noValidOperation(s); } /* - * sort a Tree consisting of addition and subtraction - * - */ - static function arrangeAddition(t:TermNode):Void - { - var addlength_old:Int = -1; - var add:Array = new Array(); - traverseAddition(t, add); - add.sort(formsort_compare); - while(add.length != addlength_old) { - addlength_old = add.length; - for(i in 0...add.length-1) { - if(isEqualAfterSimplify(add[i], add[i+1])) { - add[i].setOperation('*', add[i].copy(), newValue(2)); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if(add[i].symbol == '*' && add[i+1].symbol == '*' && add[i].right.isValue && add[i+1].right.isValue && isEqualAfterSimplify(add[i].left, add[i+1].left)) { - add[i].right.setValue(add[i].right.value+add[i+1].right.value); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if(add[i].isValue && add[i+1].isValue) { - add[i].setValue(add[i].value+add[i+1].value); - for(j in 1...add.length-i-1) { - add[i+j] = add[i+j+1]; - } - add.pop(); - break; - } - if((add[i].symbol == '-' && add[i].left.isValue && add[i].left.value == 0 && isEqualAfterSimplify(add[i].right, add[i+1])) || (add[i+1].symbol == '-' && add[i+1].left.isValue && add[i+1].left.value == 0 && isEqualAfterSimplify(add[i+1].right, add[i]))) { - for(j in 0...add.length-i-2) { - add[i+j] = add[i+j+2]; - } - add.pop(); - add.pop(); - if(add.length == 0){ - add.push(newValue(0)); - } - break; - } + * returns an array of parameter-names + * + */ + public inline function params():Array { + var ret:Array = new Array(); + if (isParam) { + ret.push(symbol); + } + else { + if (left != null ) { + for (i in left.params()) if (ret.indexOf(i) < 0) ret.push(i); } - - if(add[0].symbol == '-' && add[0].left.value == 0) { - for(i in add) { - if(i.symbol == '-' && i.left.value == 0) { - i.set(i.right); - } - else { - i.setOperation('-', newValue(0), i.copy()); - } - } - t.setOperation('-', newValue(0), new TermNode()); - traverseAdditionBack(t.right, add); - return; + if (right != null) { + for (i in right.params()) if (ret.indexOf(i) < 0) ret.push(i); } - } - traverseAdditionBack(t, add); - } -} - -/** - * symbolic derivation - * by Sylvio Sell, Rostock 2017 - * - **/ - -class TermDerivate { - - static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; - static var newValue:Float->TermNode = TermNode.newValue; - + return ret; + } + /* - * creates a new term that is derivate of a given term + * check if term has a param * - */ - static public inline function derivate(t:TermNode, p:String):TermNode { - return switch (t.symbol) - { - case s if (t.isName): TermNode.newName( t.symbol, derivate(t.left, p) ); - case s if (t.isValue || TermNode.constantOpRegFull.match(s)): newValue(0); - case s if (t.isParam): (t.symbol == p) ? newValue(1) : newValue(0); - case '+' | '-': - newOperation(t.symbol, derivate(t.left, p), derivate(t.right, p)); - case '*': - newOperation('+', - newOperation('*', derivate(t.left, p), t.right.copy()), - newOperation('*', t.left.copy(), derivate(t.right, p)) - ); - case '/': - newOperation('/', - newOperation('-', - newOperation('*', derivate(t.left, p), t.right.copy()), - newOperation('*', t.left.copy(), derivate(t.right, p)) - ), - newOperation('^', t.right.copy(), newValue(2) ) - ); - case '^': - if (t.left.symbol == 'e') - newOperation('*', derivate(t.right, p), - newOperation('^', newOperation('e'), t.left.copy()) - ); - else - newOperation('*', - newOperation('^', t.left.copy(), t.right.copy()), - newOperation('*', - t.right.copy(), - newOperation('ln', t.left.copy()) - ).derivate(p) - ); - case 'sin': - newOperation('*', derivate(t.left, p), - newOperation('cos', t.left.copy()) - ); - case 'cos': - newOperation('*', derivate(t.left, p), - newOperation('-', newValue(0), - newOperation('sin', t.left.copy() ) - ) - ); - case 'tan': - newOperation('*', derivate(t.left, p), - newOperation('+', newValue(1), - newOperation('^', - newOperation('tan', t.left.copy() ), - newValue(2) - ) - ) - ); - case 'cot': - newOperation('/', - newValue(1), - newOperation('tan', t.left.copy()) - ).derivate(p); - case 'atan': - newOperation('*', derivate(t.left, p), - newOperation('/', newValue(1), - newOperation('+', newValue(1), - newOperation('^', t.left.copy(), newValue(2)) - ) - ) - ); - case 'atan2': - newOperation('/', - newOperation('-', - newOperation('*', t.right.copy(), derivate(t.left, p)), - newOperation('*', t.left.copy(), derivate(t.right, p)) - ), - newOperation('+', - newOperation('*', t.left.copy(), t.left.copy()), - newOperation('*', t.right.copy(), t.right.copy()) - ) - ); - case 'asin': - newOperation('*', derivate(t.left, p), - newOperation('/', newValue(1), - newOperation('^', - newOperation('-', newValue(1), - newOperation('^', t.left.copy(), newValue(2)) - ), newOperation('/', newValue(1), newValue(2)) - ) - ) - ); - case 'acos': - newOperation('*', derivate(t.left, p), - newOperation('-', newValue(0), - newOperation('/', newValue(1), - newOperation('^', - newOperation('-', newValue(1), - newOperation('^', t.left.copy(), newValue(2)) - ), newOperation('/', newValue(1), newValue(2)) - ) - ) - ) - ); - case 'log': - newOperation('/', - newOperation('ln', t.right.copy()), - newOperation('ln', t.left.copy()) - ).derivate(p); - case 'ln': - newOperation('*', derivate(t.left, p), - newOperation('/', newValue(1), t.left.copy()) - ); - case 'abs': - newOperation('*', derivate(t.left, p), - newOperation('/', t.left.copy(), newOperation('abs', t.left.copy()) ) - ); - - default: throw('derivation of "${t.symbol}" not implemented'); - } - + */ + public function hasParam(paramName:String):Bool { + if (isParam && symbol == paramName) return true; + if (left != null ) + if (left.hasParam(paramName)) return true; + if (right != null) + if (right.hasParam(paramName)) return true; + return false; } -} - -/** - * knot of a Tree to do math operations at runtime - * by Sylvio Sell, Rostock 2017 - * - **/ -typedef OperationNode = {symbol:String, left:TermNode, right:TermNode, leftOperation:OperationNode, rightOperation:OperationNode, precedence:Int}; - -class TermNode { - + /* - * Properties + * bind terms to parameters * - */ - var operation:TermNode->Float; // operation function pointer - public var symbol:String; //operator like "+" or parameter name like "x" - - public var left:TermNode; // left branch of tree - public var right:TermNode; // right branch of tree - - public var value:Float; // leaf of the tree - - public var name(get, set):String; // name is stored into a param-TermNode at root of the tree - inline function get_name():String return (isName) ? symbol : null; - public static inline function checkValidName(name:String) if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); - inline function set_name(name:String):String { - if (name == null && isName) { - copyNodeFrom(left); + */ + public inline function bind(params:Map):TermNode { + if (isParam) { + if (params.exists(symbol)) left = params.get(symbol); } else { - //if (!nameRegFull.match(name)) throw('Not allowed characters for name $name".'); - checkValidName(name); - if (isName) symbol = name else setName(name, copyNode()); + if (left != null) left.bind(params); + if (right != null) right.bind(params); } - return name; + return this; } + /* - * returns depth of parameter bindings + * unbind terms that is bind to parameter-names * */ - public inline function depth():Int { - if (isName && left != null) return left._depth(); - else return _depth(); - } - public inline function _depth():Int { - var l:Int = 0; - var r:Int = 0; - var d:Int = 0; + public inline function unbind(params:Array):TermNode { if (isParam) { - if (left == null) d = 0; - else if (!left.isName) d = 1; + if (params.indexOf(symbol) >= 0) left = null; } - else if (isName) d = 1; - - if (left != null) l = left._depth(); - if (right != null) r = right._depth(); - - return( d + ((l>r) ? l : r)); + else { + if (left != null) left.unbind(params); + if (right != null) right.unbind(params); + } + return this; } /* - * Check Type of TermNode + * unbind terms * - */ - public var isName(get, null):Bool; // true -> root TermNode that holds name - inline function get_isName():Bool return Reflect.compareMethods(operation, opName); - - public var isParam(get, null):Bool; // true -> it's a parameter - inline function get_isParam():Bool return Reflect.compareMethods(operation, opParam); - - public var isValue(get, null):Bool; // true -> it's a value (no left and right) - inline function get_isValue():Bool return Reflect.compareMethods(operation, opValue); - - public var isOperation(get, null):Bool; // true -> it's a operation TermNode - inline function get_isOperation():Bool return !(isName||isParam||isValue); - - /* - * Calculates result of all Operations - * throws error if there is unbind param - */ - public var result(get, null):Float; // result of tree calculation - inline function get_result():Float return operation(this); + */ + public inline function unbindTerm(params:Array):TermNode { + if (isParam) { + if (left != null) { + if (params.indexOf(left) >= 0) left = null; + } + } + else { + if (left != null) left.unbindTerm(params); + if (right != null) right.unbindTerm(params); + } + return this; + } /* - * Constructors + * check if a term is binded to * - */ - public function new() {} - - public static inline function newName(name:String, ?term:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setName(name, term); - return t; - } - - public static inline function newParam(name:String, ?term:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setParam(name, term); - return t; - } - - public static inline function newValue(f:Float):TermNode { - var t:TermNode = new TermNode(); - t.setValue(f); - return t; - } - - public static inline function newOperation(s:String, ?left:TermNode, ?right:TermNode):TermNode { - var t:TermNode = new TermNode(); - t.setOperation(s, left, right); - return t; + */ + public function hasBinding(term:TermNode):Bool { + if (isParam && left == term) return true; + if (left != null) + if (left.hasBinding(term)) return true; + if (right != null) + if (right.hasBinding(term)) return true; + return false; } - /* - * atomic methods + * unbind all terms that is bind to parameter-names * - */ - public inline function set(term:TermNode):TermNode { - // TODO: new param to keep the existing bindings if there is same parameters - if (isName) { - if (!term.isName) left = term.copy(); - else if (term.left != null) left = term.left.copy(); - else left = null; - } + */ + public inline function unbindAll():TermNode { + if (isParam) left = null; else { - if (!term.isName) copyNodeFrom(term.copy()); - else if (term.left != null) copyNodeFrom(term.left.copy()); - //else return null; // TODO: check if that can ever been! + if (left != null) left.unbindAll(); + if (right != null) right.unbindAll(); } return this; - } - - public inline function setName(name:String, ?term:TermNode) { - operation = opName; - symbol = name; - left = term; right = null; - } - - public inline function setParam(name:String, ?term:TermNode) { - operation = opParam; - symbol = name; - left = term; right = null; - } - - public inline function setValue(f:Float):Void { - operation = opValue; - symbol = null; - value = f; - left = null; right = null; - } - - public inline function setOperation(s:String, ?left:TermNode, ?right:TermNode):Void { - operation = MathOp.get(s); - if (operation != null) - { - symbol = s; - this.left = left; - this.right = right; - } - else throw ('"$s" is no valid operation.'); - } - - /* - * returns an array of parameter-names - * - */ - public inline function params():Array { - var ret:Array = new Array(); - if (isParam) { - ret.push(symbol); - } - else { - if (left != null ) { - for (i in left.params()) if (ret.indexOf(i) < 0) ret.push(i); - } - if (right != null) { - for (i in right.params()) if (ret.indexOf(i) < 0) ret.push(i); - } - } - return ret; - } - - /* - * check if term has a param - * - */ - public function hasParam(paramName:String):Bool { - if (isParam && symbol == paramName) return true; - if (left != null ) - if (left.hasParam(paramName)) return true; - if (right != null) - if (right.hasParam(paramName)) return true; - return false; - } - - - /* - * bind terms to parameters - * - */ - public inline function bind(params:Map):TermNode { - if (isParam) { - if (params.exists(symbol)) left = params.get(symbol); - } - else { - if (left != null) left.bind(params); - if (right != null) right.bind(params); - } - return this; - } - - - /* - * unbind terms that is bind to parameter-names - * - */ - public inline function unbind(params:Array):TermNode { - if (isParam) { - if (params.indexOf(symbol) >= 0) left = null; - } - else { - if (left != null) left.unbind(params); - if (right != null) right.unbind(params); - } - return this; - } - - - /* - * unbind terms - * - */ - public inline function unbindTerm(params:Array):TermNode { - if (isParam) { - if (left != null) { - if (params.indexOf(left) >= 0) left = null; - } - } - else { - if (left != null) left.unbindTerm(params); - if (right != null) right.unbindTerm(params); - } - return this; - } - - /* - * check if a term is binded to - * - */ - public function hasBinding(term:TermNode):Bool { - if (isParam && left == term) return true; - if (left != null) - if (left.hasBinding(term)) return true; - if (right != null) - if (right.hasBinding(term)) return true; - return false; - } - - /* - * unbind all terms that is bind to parameter-names - * - */ - public inline function unbindAll():TermNode { - if (isParam) left = null; - else { - if (left != null) left.unbindAll(); - if (right != null) right.unbindAll(); - } - return this; - } + } /* @@ -1211,704 +592,1483 @@ class TermNode { default: 1 + left.length(depth) + right.length(depth); } } - + + + /* + * returns true if other term is equal in data and structure + * + */ + public function isEqual(t:TermNode, ?compareNames=false, ?compareParams=false):Bool + { + if ( !compareNames && (isName || t.isName) ) { + if (isName && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isName && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isName && t.isName); + } + + if ( !compareParams && (isParam || t.isParam) ) { + if (isParam && left != null) return left.isEqual(t, compareNames, compareParams); + if (t.isParam && t.left != null) return isEqual(t.left, compareNames, compareParams); + return (isParam && t.isParam); + } + + var is_equal:Bool = false; + + if (isValue && t.isValue) + is_equal = (value==t.value); + else if ( (isName && t.isName) || (isParam && t.isParam) || (isOperation && t.isOperation) ) + is_equal = (symbol==t.symbol); + + if (left != null) { + if (t.left != null) is_equal = is_equal && left.isEqual(t.left, compareNames, compareParams); + else is_equal = false; + } + if (right != null) { + if (t.right != null) is_equal = is_equal && right.isEqual(t.right, compareNames, compareParams); + else is_equal = false; + } + + return is_equal; + } + + /* + * static Function Pointers (to stored in this.operation) + * + */ + static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else ErrorMsg.emptyFunction(t.symbol); + static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else ErrorMsg.missingParameter(t.symbol); + static function opValue(t:TermNode):Float return t.value; + + static var MathOp:MapFloat> = [ + // two side operations + "+" => function(t) return t.left.result + t.right.result, + "-" => function(t) return t.left.result - t.right.result, + "*" => function(t) return t.left.result * t.right.result, + "/" => function(t) return t.left.result / t.right.result, + "^" => function(t) return Math.pow(t.left.result, t.right.result), + "%" => function(t) return t.left.result % t.right.result, + + // function without params (constants) + "e" => function(t) return Math.exp(1), + "pi" => function(t) return Math.PI, + + // function with one param + "abs" => function(t) return Math.abs(t.left.result), + "ln" => function(t) return Math.log(t.left.result), + "sin" => function(t) return Math.sin(t.left.result), + "cos" => function(t) return Math.cos(t.left.result), + "tan" => function(t) return Math.tan(t.left.result), + "cot" => function(t) return 1/Math.tan(t.left.result), + "asin" => function(t) return Math.asin(t.left.result), + "acos" => function(t) return Math.acos(t.left.result), + "atan" => function(t) return Math.atan(t.left.result), + + // function with two params + "atan2"=> function(t) return Math.atan2(t.left.result, t.right.result), + "log" => function(t) return Math.log(t.right.result) / Math.log(t.left.result), + "max" => function(t) return Math.max(t.left.result, t.right.result), + "min" => function(t) return Math.min(t.left.result, t.right.result), + ]; + + static var twoSideOp_ = "^,/,*,-,+,%"; // <- order here determines the operator precedence + static var constantOp_ = "e,pi"; // functions without parameters like "e() or pi()" + static var oneParamOp_ = "abs,ln,sin,cos,tan,cot,asin,acos,atan"; // functions with one parameter like "sin(2)" + static var twoParamOp_ = "atan2,log,max,min"; // functions with two parameters like "max(a,b)" + + static public var twoSideOp :Array = twoSideOp_.split(','); + static public var constantOp:Array = constantOp_.split(','); + static public var oneParamOp:Array = oneParamOp_.split(','); + static public var twoParamOp:Array = twoParamOp_.split(','); + + static var precedence:Map = [ for (i in 0...twoSideOp.length) twoSideOp[i] => i ]; + + + + /* + * Regular Expressions for parsing + * + */ + static var clearSpacesReg:EReg = ~/\s+/g; + static var trailingSpacesReg:EReg = ~/^(\s+)/; + + static var numberReg:EReg = ~/^([-+]?\d+\.?\d*)/; + static var paramReg:EReg = ~/^([a-z]+)/i; + + static var constantOpReg:EReg = new EReg("^(" + constantOp.join("|") + ")\\(\\)" , "i"); + static var oneParamOpReg:EReg = new EReg("^(" + oneParamOp.join("|") + ")\\(" , "i"); + static var twoParamOpReg:EReg = new EReg("^(" + twoParamOp.join("|") + ")\\(" , "i"); + static var twoSideOpReg: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")" , ""); + + static public var constantOpRegFull:EReg = new EReg("^(" + constantOp.join("|") + ")$" , "i"); + static public var oneParamOpRegFull:EReg = new EReg("^(" + oneParamOp.join("|") + ")$" , "i"); + static public var twoParamOpRegFull:EReg = new EReg("^(" + twoParamOp.join("|") + ")$" , "i"); + static public var twoSideOpRegFull: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")$" , ""); + + static var nameReg:EReg = ~/^([a-z]+)(\s*[:=]\s*)/i; + static var nameRegFull:EReg = ~/^([a-z]+)$/i; + + static var signReg:EReg = ~/^([-+\s]+)/i; + + public static inline function trailingSpaces(s:String):Int { + if (trailingSpacesReg.match(s)) return(trailingSpacesReg.matched(1).length); + else return 0; + } + /* + * Build Tree up from String Math Expression + * + */ + public static inline function fromString(s:String, ?bindings:Map):TermNode { + var errPos:Int = 0; + errPos = trailingSpaces(s); s = s.substr(errPos); + //s = clearSpacesReg.replace(s, ''); // clear all whitespaces + if (nameReg.match(s)) { + var name:String = nameReg.matched(1); + s = s.substr(name.length + nameReg.matched(2).length); + errPos += name.length + nameReg.matched(2).length; + if (~/^\s*$/.match(s)) ErrorMsg.cantParseFromEmptyString(errPos); + return newName(name, parseString(s, errPos, bindings)); + } + if (~/^\s*$/.match(s)) ErrorMsg.cantParseFromEmptyString(errPos); + return parseString(s, errPos, bindings); + } + + static function parseString(s:String, errPos:Int, ?params:Map):TermNode { + var t:TermNode = null; + var operations:Array = new Array(); + var e, f:String; + var negate:Bool; + var spaces:Int = 0; + + while (s.length != 0) // read in terms from left + { + negate = false; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (numberReg.match(s)) { // float number + e = numberReg.matched(1); + t = newValue(Std.parseFloat(e)); + } + else if (constantOpReg.match(s)) { // like e() or pi() + e = constantOpReg.matched(1); + t = newOperation(e); + e+= "()"; + } + else if (oneParamOpReg.match(s)) { // like sin(...) + f = oneParamOpReg.matched(1); errPos += f.length; + s = "("+oneParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + t = newOperation(f, parseString(e.substring(1, e.length - 1), errPos+1, params) ); + } + else if (twoParamOpReg.match(s)) { // like atan2(... , ...) + f = twoParamOpReg.matched(1); errPos += f.length; + s = "("+twoParamOpReg.matchedRight(); + e = getBrackets(s, errPos); + var p1:String = e.substring(1, comataPos); + var p2:String = e.substring(comataPos + 1, e.length - 1); + if (comataPos == -1) ErrorMsg.operatorNeedTwoArgs(f, errPos); + t = newOperation(f, parseString(p1, errPos+1, params), parseString(p2, errPos+1 + comataPos, params) ); + } + else if (paramReg.match(s)) { // parameter + e = paramReg.matched(1); + t = newParam(e, (params==null) ? null : params.get(e)); + } + else if (signReg.match(s)) { // start with +- + e = signReg.matched(1); + s = s.substr(e.length); errPos += e.length; + e = ~/[\s+]/g.replace(e, ''); + if (e.length % 2 > 0) { + //s = "0-" + s; + if (numberReg.match(s)) { // followed by float number + e = numberReg.matched(1); + t = newValue(-Std.parseFloat(e)); + } else { // negative signed + t = newValue(0); + s = "-" + s; + e = ""; + negate = true; + } + } else continue; // positive signed + } + else if (twoSideOpReg.match(s)) { // start with other two side op + ErrorMsg.missingLeftOperand(errPos); + } + else { + e = getBrackets(s, errPos); // term inside brackets + t = parseString(e.substring(1, e.length - 1), errPos+1, params); + } + + s = s.substr(e.length); errPos += e.length; + + if (operations.length > 0) operations[operations.length - 1].right = t; + + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + + if (twoSideOpReg.match(s)) { // two side operation symbol + e = twoSideOpReg.matched(1); errPos += e.length; + s = twoSideOpReg.matchedRight(); + spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; + operations.push( { symbol:e, left:t, right:null, leftOperation:null, rightOperation:null, precedence:((negate) ? -1 :precedence.get(e)) } ); + if (operations.length > 1) { + operations[operations.length - 2].rightOperation = operations[operations.length - 1]; + operations[operations.length - 1].leftOperation = operations[operations.length - 2]; + } + } else if (s.length > 0) { + if (s.indexOf(")") == 0) ErrorMsg.noOpeningBracket(errPos); + if (!(s.indexOf("(") == 0 || numberReg.match(s) || paramReg.match(s) || constantOpReg.match(s) || oneParamOpReg.match(s) || twoParamOpReg.match(s))) + ErrorMsg.wrongChar(errPos); + else ErrorMsg.missingOperation(errPos); + } + } + + if ( operations.length > 0 ) { + if ( operations[operations.length-1].right == null ) ErrorMsg.missingRightOperand(errPos-spaces); + else { + operations.sort(function(a:OperationNode, b:OperationNode):Int + { + if (a.precedence < b.precedence) return -1; + if (a.precedence > b.precedence) return 1; + return 0; + }); + for (op in operations) { + t = TermNode.newOperation(op.symbol, op.left, op.right); + if (op.leftOperation != null && op.rightOperation != null) { + op.rightOperation.leftOperation = op.leftOperation; + op.leftOperation.rightOperation = op.rightOperation; + } + if (op.leftOperation != null) op.leftOperation.right = t; + if (op.rightOperation != null) op.rightOperation.left = t; + } + return t; + } + } + else return t; + } + + static var comataPos:Int; + static function getBrackets(s:String, errPos:Int):String { + var pos:Int = 1; + if (s.indexOf("(") == 0) // check that s starts with opening bracket + { + if (~/^\(\s*\)/.match(s)) ErrorMsg.emptyBrackets(errPos); + + var i,j,k:Int; + var openBrackets:Int = 1; + comataPos = -1; + while ( openBrackets > 0 ) + { + i = s.indexOf("(", pos); + j = s.indexOf(")", pos); + + // check for commata position + if (openBrackets == 1 && comataPos == -1) { + k = s.indexOf(",", pos); + if (k0) comataPos = k; + } + + if ((i>0 && j>0 && i0 && j<0)) { // found open bracket + openBrackets++; pos = i + 1; + } + else if ((j>0 && i>0 && j0 && i<0)) { // found close bracket + openBrackets--; pos = j + 1; + } else { // no close or open found + ErrorMsg.wrongBracketNesting(errPos); + } + } + return s.substring(0, pos); + } + if (s.indexOf(")") == 0) ErrorMsg.noOpeningBracket(errPos); + else ErrorMsg.wrongChar(errPos); + } + + + /* + * Puts out Math Expression as a String + * + */ + public function toString(?depth:Null = null, ?plOut:String = null):String { + var t:TermNode = this; + if (isName) t = left; + var options:Int; + switch (plOut) { + case 'glsl': options = noNeg|forceFloat|forcePow|forceMod|forceLog|forceAtan|forceConst; + default: options = 0; + } + return (left != null || !isName) ? t._toString(depth, options) : ''; + } + // options + public static inline var noNeg:Int = 1; + public static inline var forceFloat:Int = 2; + public static inline var forcePow:Int = 4; + public static inline var forceMod:Int = 8; + public static inline var forceLog:Int = 16; + public static inline var forceAtan:Int = 32; + public static inline var forceConst:Int = 64; + + inline function _toString(depth:Null, options:Int, ?isFirst:Bool=true):String { + if (depth == null) depth = -1; + return switch(symbol) { + case s if (isValue): floatToString(value, options); + //case s if (isName && isFirst): (left == null) ? symbol : left.toString(depth, false); + case s if (isName): (depth == 0 || left == null) ? symbol : left._toString(depth-1, options, false); + case s if (isParam): (depth == 0 || left == null) ? symbol : left._toString(depth-((left.isName)?0:1), options, false); + case s if (twoSideOpRegFull.match(s)) : + if (symbol == "-" && left.isValue && left.value == 0 && options&noNeg == 0) symbol + right._toString(depth, options, false); + else if (symbol == "^" && options&forcePow > 0) 'pow' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else if (symbol == "%" && options&forceMod > 0) 'mod' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else ((isFirst)?"":"(") + left._toString(depth, options, false) + symbol + right._toString(depth, options, false) + ((isFirst)?'':")"); + case s if (twoParamOpRegFull.match(s)): + if (symbol == "log" && options&forceLog > 0) "(log(" + right._toString(depth, options) + ")/log(" + left._toString(depth, options) + "))"; + else if (symbol == "atan2" && options&forceAtan > 0) "atan(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; + case s if (constantOpRegFull.match(s)): + if (symbol == "pi" && options & forceConst > 0) Std.string(Math.PI); + else if (symbol == "e" && options&forceConst > 0) Std.string(Math.exp(1)); + else symbol + "()"; + default: + if (symbol == "ln" && options&forceLog > 0) 'log' + "(" + left._toString(depth, options) + ")"; + else symbol + "(" + left._toString(depth, options) + ")"; + } + } + + inline function floatToString(value:Float, ?options:Int = 0):String { + var s:String = Std.string(value); + if (options&forceFloat > 0 && s.indexOf('.') == -1) s += ".0"; + return s; + } + + /* + * enrolls terms and subterms for debugging + * + */ + public function debug() { + //TODO + var out:String = "";// "(" + depth() + ")"; + for (i in 0 ... depth()+1) { + if (i == 0) out += ((name != null) ? name : "?") + " = "; else out += " -> "; + out += toString(i); + } + trace(out); + } + + /* + * packs a TermNode and all sub-terms into Bytes + * + */ + public function toBytes():Bytes { + var b = new BytesOutput(); + _toBytes(b); + return b.getBytes(); + } + + inline function _toBytes(b:BytesOutput) { + // optimize (later to do): needs only 3 bit per TermNode type! + if (isValue) { + b.writeByte(0); + b.writeFloat(value); + } + else if (isName) { + b.writeByte((left!=null) ? 1:2); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); + } + else if (isParam) { + b.writeByte((left!=null) ? 3:4); + _writeString(symbol, b); + if (left!=null) left._toBytes(b); + } + else if (isOperation) { + b.writeByte(5); + var i:Int = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp))).indexOf(symbol); + if (i > -1) { + b.writeByte(i); + if (oneParamOpRegFull.match(symbol)) left._toBytes(b); + else if (twoSideOpRegFull.match(symbol) || twoParamOpRegFull.match(symbol) ) { + left._toBytes(b); + right._toBytes(b); + } + } + else ErrorMsg.intoBytes(); + } + else ErrorMsg.intoBytes(); + } + + inline function _writeString(s:String, b:BytesOutput):Void { + b.writeByte((s.length<255) ? s.length: 255); + for (i in 0...((s.length<255) ? s.length: 255)) b.writeByte(symbol.charCodeAt(i)); + } + /* + * unserialize packed Bytes-Term to create a TermNode structure + * + */ + public static function fromBytes(b:Bytes):TermNode { + return _fromBytes(new BytesInput(b)); + } + + static inline function _fromBytes(b:BytesInput):TermNode { + return switch (b.readByte()) { + case 0: TermNode.newValue(b.readFloat()); + case 1: TermNode.newName( _readString(b), _fromBytes(b) ); + case 2: TermNode.newName( _readString(b) ); + case 3: TermNode.newParam( _readString(b), _fromBytes(b) ); + case 4: TermNode.newParam( _readString(b) ); + case 5: + var op:String = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp)))[b.readByte()]; + if (oneParamOpRegFull.match(op)) TermNode.newOperation( op, _fromBytes(b) ); + else if (twoSideOpRegFull.match(op) || twoParamOpRegFull.match(op) ) TermNode.newOperation( op, _fromBytes(b), _fromBytes(b) ); + else TermNode.newOperation( op ); + default: ErrorMsg.fromBytes(); null; + } + } + + static inline function _readString(b:BytesInput):String { + var len:Int = b.readByte(); + var s:String = ""; + for (i in 0...len) s += String.fromCharCode(b.readByte()); + return s; + } + + + /************************************************************************************** + * * + * various math operations transformation and more. * + * * + * * + **************************************************************************************/ + + /* + * creates a new term that is derivate of a given term + * + */ + public function derivate(paramName:String):TermNode return TermDerivate.derivate(this, paramName); + + /* + * Simplify: trims the length of a math expression + * + */ + public function simplify():TermNode return TermTransform.simplify(this); + + /* + * expands a mathmatical expression recursivly into a polynomial + * + */ + public function expand():TermNode return TermTransform.expand(this); + + /* + * factorizes a mathmatical expression + * + */ + public function factorize():TermNode return TermTransform.factorize(this); +} + + +/** + * symbolic derivation + * by Sylvio Sell, Rostock 2017 + * + **/ + +class TermDerivate { + + static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; + static var newValue:Float->TermNode = TermNode.newValue; + + /* + * creates a new term that is derivate of a given term + * + */ + static public inline function derivate(t:TermNode, p:String):TermNode { + return switch (t.symbol) + { + case s if (t.isName): TermNode.newName( t.symbol, derivate(t.left, p) ); + case s if (t.isValue || TermNode.constantOpRegFull.match(s)): newValue(0); + case s if (t.isParam): (t.symbol == p) ? newValue(1) : newValue(0); + case '+' | '-': + newOperation(t.symbol, derivate(t.left, p), derivate(t.right, p)); + case '*': + newOperation('+', + newOperation('*', derivate(t.left, p), t.right.copy()), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ); + case '/': + newOperation('/', + newOperation('-', + newOperation('*', derivate(t.left, p), t.right.copy()), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ), + newOperation('^', t.right.copy(), newValue(2) ) + ); + case '^': + if (t.left.symbol == 'e') + newOperation('*', derivate(t.right, p), + newOperation('^', newOperation('e'), t.left.copy()) + ); + else + newOperation('*', + newOperation('^', t.left.copy(), t.right.copy()), + newOperation('*', + t.right.copy(), + newOperation('ln', t.left.copy()) + ).derivate(p) + ); + case 'sin': + newOperation('*', derivate(t.left, p), + newOperation('cos', t.left.copy()) + ); + case 'cos': + newOperation('*', derivate(t.left, p), + newOperation('-', newValue(0), + newOperation('sin', t.left.copy() ) + ) + ); + case 'tan': + newOperation('*', derivate(t.left, p), + newOperation('+', newValue(1), + newOperation('^', + newOperation('tan', t.left.copy() ), + newValue(2) + ) + ) + ); + case 'cot': + newOperation('/', + newValue(1), + newOperation('tan', t.left.copy()) + ).derivate(p); + case 'atan': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), + newOperation('+', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ) + ) + ); + case 'atan2': + newOperation('/', + newOperation('-', + newOperation('*', t.right.copy(), derivate(t.left, p)), + newOperation('*', t.left.copy(), derivate(t.right, p)) + ), + newOperation('+', + newOperation('*', t.left.copy(), t.left.copy()), + newOperation('*', t.right.copy(), t.right.copy()) + ) + ); + case 'asin': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), + newOperation('^', + newOperation('-', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ), newOperation('/', newValue(1), newValue(2)) + ) + ) + ); + case 'acos': + newOperation('*', derivate(t.left, p), + newOperation('-', newValue(0), + newOperation('/', newValue(1), + newOperation('^', + newOperation('-', newValue(1), + newOperation('^', t.left.copy(), newValue(2)) + ), newOperation('/', newValue(1), newValue(2)) + ) + ) + ) + ); + case 'log': + newOperation('/', + newOperation('ln', t.right.copy()), + newOperation('ln', t.left.copy()) + ).derivate(p); + case 'ln': + newOperation('*', derivate(t.left, p), + newOperation('/', newValue(1), t.left.copy()) + ); + case 'abs': + newOperation('*', derivate(t.left, p), + newOperation('/', t.left.copy(), newOperation('abs', t.left.copy()) ) + ); + + default: ErrorMsg.notImplementedFor(t.symbol); null; + } + + } + +} + + +/** + * extending TermNode with various math operations transformation and more. + * by Sylvio Sell, Rostock 2017 + * + **/ + +class TermTransform { + + static var newOperation:String->?TermNode->?TermNode->TermNode = TermNode.newOperation; + static var newValue:Float->TermNode = TermNode.newValue; + + /* + * Simplify: trims the length of a math expression + * + */ + static public inline function simplify(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _simplify(tnew); + return tnew; + } + + static inline function _simplify(t:TermNode):Void { + _expand(t); + + var len:Int = -1; + var len_old:Int = 0; + while (len != len_old) { + if (t.isName && t.left != null) { + simplifyStep(t.left); + } + else { + simplifyStep(t); + } + len_old = len; + len = t.length(); + } + } + + // TODO: removing this calls in subfunctions could be better to understand algorithms-recursions !!! + static function isEqualAfterSimplify(t1:TermNode, t2:TermNode):Bool { + // ----> take care, _simplify changes both TermNodes on call !!! + _simplify(t1); + _simplify(t2); + return t1.isEqual(t2, false, true); + } + + static function simplifyStep(t:TermNode):Void { + if (!t.isOperation) return; + + if (t.left != null) { + if (t.left.isValue) { + if (t.right == null) { + // setValue(result); // calculate operation with one value + return; + } + else if (t.right.isValue) { + t.setValue(t.result); // calculate result of operation with values on both sides + return; + } + } + } + + switch(t.symbol) { + case '+': + if (t.left.isValue && t.left.value == 0) t.copyNodeFrom(t.right); // 0+a -> a + else if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a+0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)+ln(b) -> ln(a*b) + t.setOperation('ln', + newOperation('*', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b+c/b -> (a+c)/b + newOperation('+', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { // a/b+c/d -> (a*d+c*b)/(b*d) + t.setOperation('/', + newOperation('+', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '+') { + _factorize(t); + } + + case '-': + if (t.right.isValue && t.right.value == 0) t.copyNodeFrom(t.left); // a-0 -> a + else if (t.left.symbol == 'ln' && t.right.symbol == 'ln') { // ln(a)-ln(b) -> ln(a/b) + t.setOperation('ln', + newOperation('/', t.left.left.copy(), t.right.left.copy()) + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/' && isEqualAfterSimplify(t.left.right, t.right.right)) { + t.setOperation('/', // a/b-c/b -> (a-c)/b + newOperation('-', t.left.left.copy(), t.right.left.copy()), + t.left.right.copy() + ); + } + else if (t.left.symbol == '/' && t.right.symbol == '/') { //a/b-c/d -> (a*d-c*b)/(b*d) + t.setOperation('/', + newOperation('-', + newOperation('*', t.left.left.copy(), t.right.right.copy()), + newOperation('*', t.right.left.copy(), t.left.right.copy()) + ), + newOperation('*', t.left.right.copy(), t.right.right.copy()) + ); + } + arrangeAddition(t); + if(t.symbol == '-') { + _factorize(t); + } + + case '*': + if (t.left.isValue) { + if (t.left.value == 1) t.copyNodeFrom(t.right); // 1*a -> a + else if (t.left.value == 0) t.setValue(0); // 0*a -> 0 + } + else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a*1 -> a + else if (t.right.value == 0) t.setValue(0); // a*0 -> a + } + else if (t.left.symbol == '/') { // (a/b)*c -> (a*c)/b + t.setOperation('/', + newOperation('*', t.right.copy(), t.left.left.copy()), + t.left.right.copy() + ); + } + else if (t.right.symbol == '/') { // a*(b/c) -> (a*b)/c + t.setOperation('/', + newOperation('*', t.left.copy(), t.right.left.copy()), + t.right.right.copy() + ); + } + else { + arrangeMultiplication(t); + } + + case '/': + if (isEqualAfterSimplify(t.left, t.right)) { // x/x -> 1 + t.setValue(1); + } + else { + if (t.left.isValue && t.left.value == 0) t.setValue(0); // 0/a -> 0 + else if (t.right.symbol == '/') { + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.right.isValue && t.right.value == 1) t.copyNodeFrom(t.left); // a/1 -> a + else if (t.left.symbol == '/') { // (1/x)/b -> 1/(x*b) + t.setOperation('/', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } + else if (t.right.symbol == '/') { // b/(1/x) -> b*x + t.setOperation('/', + newOperation('*', t.right.right.copy(), t.left.copy()), + t.right.left.copy() + ); + } + else if (t.left.symbol == '-' && t.left.left.isValue && t.left.left.value == 0) + { + t.setOperation('-', newValue(0), + newOperation('/', t.left.right.copy(), t.right.copy()) + ); + } + else{ // a*b/b -> a + simplifyfraction(t); + } + } + + case '^': + if (t.left.isValue) { + if (t.left.value == 1) t.setValue(1); // 1^a -> 1 + else if (t.left.value == 0) t.setValue(0); // 0^a -> 0 + } else if (t.right.isValue) { + if (t.right.value == 1) t.copyNodeFrom(t.left); // a^1 -> a + else if (t.right.value == 0) t.setValue(1); // a^0 -> 1 + } + else if (t.left.symbol == '^') { // (a^b)^c -> a^(b*c) + t.setOperation('^', t.left.left.copy(), + newOperation('*', t.left.right.copy(), t.right.copy()) + ); + } + + case 'ln': + if (t.left.symbol == 'e') t.setValue(1); + case 'log': + if (isEqualAfterSimplify(t.left, t.right)) { + t.setValue(1); + } + else { + t.setOperation('/', // log(a,b) -> ln(b)/ln(a) + newOperation('ln', t.right.copy()), + newOperation('ln', t.left.copy()) + ); + } + } + if (t.left != null) simplifyStep(t.left); + if (t.right != null) simplifyStep(t.right); + } + + /* + * put all subterms separated by * into an array + * + */ + static function traverseMultiplication(t:TermNode, p:Array) + { + if (t.symbol != "*") { + p.push(t); + } + else { + traverseMultiplication(t.left, p); + traverseMultiplication(t.right, p); + } + } /* - * returns true if other term is equal in data and structure + * build tree consisting of multiple * from array * - */ - public function isEqual(t:TermNode, ?compareNames=false, ?compareParams=false):Bool + */ + static function traverseMultiplicationBack(t:TermNode, p:Array) { - if ( !compareNames && (isName || t.isName) ) { - if (isName && left != null) return left.isEqual(t, compareNames, compareParams); - if (t.isName && t.left != null) return isEqual(t.left, compareNames, compareParams); - return (isName && t.isName); - } - - if ( !compareParams && (isParam || t.isParam) ) { - if (isParam && left != null) return left.isEqual(t, compareNames, compareParams); - if (t.isParam && t.left != null) return isEqual(t.left, compareNames, compareParams); - return (isParam && t.isParam); + if (p.length > 2) { + t.setOperation('*', newValue(1), p.pop()); + traverseMultiplicationBack(t.left, p); } - - var is_equal:Bool = false; - - if (isValue && t.isValue) - is_equal = (value==t.value); - else if ( (isName && t.isName) || (isParam && t.isParam) || (isOperation && t.isOperation) ) - is_equal = (symbol==t.symbol); - - if (left != null) { - if (t.left != null) is_equal = is_equal && left.isEqual(t.left, compareNames, compareParams); - else is_equal = false; + else if (p.length == 2) { + t.setOperation('*', p[0].copy(), p[1].copy()); + p.pop(); + p.pop(); } - if (right != null) { - if (t.right != null) is_equal = is_equal && right.isEqual(t.right, compareNames, compareParams); - else is_equal = false; + else { + t.set(p.pop()); } - - return is_equal; } - - /* - * static Function Pointers (to stored in this.operation) - * - */ - static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else throw('Empty function "${t.symbol}".'); - static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else throw('Missing parameter "${t.symbol}".'); - static function opValue(t:TermNode):Float return t.value; - - static var MathOp:MapFloat> = [ - // two side operations - "+" => function(t) return t.left.result + t.right.result, - "-" => function(t) return t.left.result - t.right.result, - "*" => function(t) return t.left.result * t.right.result, - "/" => function(t) return t.left.result / t.right.result, - "^" => function(t) return Math.pow(t.left.result, t.right.result), - "%" => function(t) return t.left.result % t.right.result, - - // function without params (constants) - "e" => function(t) return Math.exp(1), - "pi" => function(t) return Math.PI, - - // function with one param - "abs" => function(t) return Math.abs(t.left.result), - "ln" => function(t) return Math.log(t.left.result), - "sin" => function(t) return Math.sin(t.left.result), - "cos" => function(t) return Math.cos(t.left.result), - "tan" => function(t) return Math.tan(t.left.result), - "cot" => function(t) return 1/Math.tan(t.left.result), - "asin" => function(t) return Math.asin(t.left.result), - "acos" => function(t) return Math.acos(t.left.result), - "atan" => function(t) return Math.atan(t.left.result), - - // function with two params - "atan2"=> function(t) return Math.atan2(t.left.result, t.right.result), - "log" => function(t) return Math.log(t.right.result) / Math.log(t.left.result), - "max" => function(t) return Math.max(t.left.result, t.right.result), - "min" => function(t) return Math.min(t.left.result, t.right.result), - ]; - - static var twoSideOp_ = "^,/,*,-,+,%"; // <- order here determines the operator precedence - static var constantOp_ = "e,pi"; // functions without parameters like "e() or pi()" - static var oneParamOp_ = "abs,ln,sin,cos,tan,cot,asin,acos,atan"; // functions with one parameter like "sin(2)" - static var twoParamOp_ = "atan2,log,max,min"; // functions with two parameters like "max(a,b)" - - static public var twoSideOp :Array = twoSideOp_.split(','); - static public var constantOp:Array = constantOp_.split(','); - static public var oneParamOp:Array = oneParamOp_.split(','); - static public var twoParamOp:Array = twoParamOp_.split(','); - - static var precedence:Map = [ for (i in 0...twoSideOp.length) twoSideOp[i] => i ]; - - /* - * Regular Expressions for parsing - * - */ - static var clearSpacesReg:EReg = ~/\s+/g; - static var trailingSpacesReg:EReg = ~/^(\s+)/; - - static var numberReg:EReg = ~/^([-+]?\d+\.?\d*)/; - static var paramReg:EReg = ~/^([a-z]+)/i; - - static var constantOpReg:EReg = new EReg("^(" + constantOp.join("|") + ")\\(\\)" , "i"); - static var oneParamOpReg:EReg = new EReg("^(" + oneParamOp.join("|") + ")\\(" , "i"); - static var twoParamOpReg:EReg = new EReg("^(" + twoParamOp.join("|") + ")\\(" , "i"); - static var twoSideOpReg: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")" , ""); - - static public var constantOpRegFull:EReg = new EReg("^(" + constantOp.join("|") + ")$" , "i"); - static public var oneParamOpRegFull:EReg = new EReg("^(" + oneParamOp.join("|") + ")$" , "i"); - static public var twoParamOpRegFull:EReg = new EReg("^(" + twoParamOp.join("|") + ")$" , "i"); - static public var twoSideOpRegFull: EReg = new EReg("^(" + "\\"+ twoSideOp.join("|\\") + ")$" , ""); - - static var nameReg:EReg = ~/^([a-z]+)(\s*[:=]\s*)/i; - static var nameRegFull:EReg = ~/^([a-z]+)$/i; - - static var signReg:EReg = ~/^([-+\s]+)/i; + * put all subterms separated by * into an array + * + */ + static function traverseAddition(t:TermNode, p:Array, ?negative:Bool=false) + { + if (t.symbol == "+" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p); + } + else if (t.symbol == "-" && negative == false) { + traverseAddition(t.left, p); + traverseAddition(t.right, p, true); + } + else if (t.symbol == "+" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p, true); + } + else if (t.symbol == "-" && negative == true) { + traverseAddition(t.left, p, true); + traverseAddition(t.right, p); + } + else if (negative == true && !t.isValue || negative == true && t.isValue && t.value != 0) { + p.push(newOperation('-', newValue(0), t)); + } + else if (!t.isValue || t.isValue && t.value != 0) { + p.push(t); + } + return(p); + } - public static inline function trailingSpaces(s:String):Int { - if (trailingSpacesReg.match(s)) return(trailingSpacesReg.matched(1).length); - else return 0; + /* + * build tree consisting of multiple - and + from array + * + */ + static function traverseAdditionBack(t:TermNode, p:Array) + { + if(p.length > 1) { + if (p[p.length-1].symbol == "-") { + t.set(p.pop()); + } + else { + t.setOperation("+", newValue(0), p.pop()); + } + traverseAdditionBack(t.left, p); + } + else if(p.length == 1){ + t.set(p.pop()); + } } + /* - * Build Tree up from String Math Expression + * reduce a fraction * - */ - public static inline function fromString(s:String, ?bindings:Map):TermNode { - var errPos:Int = 0; - errPos = trailingSpaces(s); s = s.substr(errPos); - //s = clearSpacesReg.replace(s, ''); // clear all whitespaces - if (nameReg.match(s)) { - var name:String = nameReg.matched(1); - s = s.substr(name.length + nameReg.matched(2).length); - errPos += name.length + nameReg.matched(2).length; - if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); - return newName(name, parseString(s, errPos, bindings)); + */ + static function simplifyfraction(t:TermNode) + { + var numerator:Array = new Array(); + traverseMultiplication(t.left, numerator); + var denominator:Array = new Array(); + traverseMultiplication(t.right, denominator); + for (n in numerator) { + for (d in denominator) { + if (isEqualAfterSimplify(n, d)) { + numerator.remove(n); + denominator.remove(d); + } + } + } + if (numerator.length > 1) { + traverseMultiplicationBack(t.left, numerator); + } + else if (numerator.length == 1) { + t.setOperation('/', numerator.pop(), newValue(1)); + } + else if (numerator.length == 0) { + t.left.setValue(1); + } + if (denominator.length > 1) { + traverseMultiplicationBack(t.right, denominator); + } + else if (denominator.length == 1) { + t.setOperation('/', t.left.copy(), denominator.pop()); + } + else if (denominator.length == 0) { + t.right.setValue(1); } - if (~/^\s*$/.match(s)) throw({"msg":"Can't parse Term from empty string.","pos":errPos}); - return parseString(s, errPos, bindings); } - static function parseString(s:String, errPos:Int, ?params:Map):TermNode { - var t:TermNode = null; - var operations:Array = new Array(); - var e, f:String; - var negate:Bool; - var spaces:Int = 0; + /* + * expands a mathmatical expression recursivly into a polynomial + * + */ + static public function expand(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _expand(tnew); + return tnew; + } + + static function _expand(t:TermNode):Void { - while (s.length != 0) // read in terms from left - { - negate = false; - - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - - if (numberReg.match(s)) { // float number - e = numberReg.matched(1); - t = newValue(Std.parseFloat(e)); + var len:Int = -1; + var len_old:Int = 0; + while(len != len_old) { + if (t.symbol == '*') { + expandStep(t); } - else if (constantOpReg.match(s)) { // like e() or pi() - e = constantOpReg.matched(1); - t = newOperation(e); - e+= "()"; + else { + if(t.left != null) { + _expand(t.left); + } + if(t.right != null) { + _expand(t.right); + + } } - else if (oneParamOpReg.match(s)) { // like sin(...) - f = oneParamOpReg.matched(1); errPos += f.length; - s = "("+oneParamOpReg.matchedRight(); - e = getBrackets(s, errPos); - t = newOperation(f, parseString(e.substring(1, e.length - 1), errPos+1, params) ); + len_old = len; + len = t.length(); + } + } + + /* + * expands a mathmatical expression into a polynomial -> use only if top symbol=* + * + */ + static function expandStep(t:TermNode):Void + { + var left:TermNode = t.left; + var right:TermNode = t.right; + + if (left.symbol == "+" || left.symbol == "-") { + if (right.symbol == "+" || right.symbol == "-") { + if (left.symbol == "+" && right.symbol == "+") { // (a+b)*(c+d) + t.setOperation('+', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "+" && right.symbol == "-") { // (a+b)*(c-d) + t.setOperation('+', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "+") { // (a-b)*(c+d) + t.setOperation('-', + newOperation('+', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('+', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } + else if (left.symbol == "-" && right.symbol == "-") { // (a-b)*(c-d) + t.setOperation('-', + newOperation('-', + newOperation('*', left.left.copy(), right.left.copy()), + newOperation('*', left.left.copy(), right.right.copy()) + ), + newOperation('-', + newOperation('*', left.right.copy(), right.left.copy()), + newOperation('*', left.right.copy(), right.right.copy()) + ) + ); + } } - else if (twoParamOpReg.match(s)) { // like atan2(... , ...) - f = twoParamOpReg.matched(1); errPos += f.length; - s = "("+twoParamOpReg.matchedRight(); - e = getBrackets(s, errPos); - var p1:String = e.substring(1, comataPos); - var p2:String = e.substring(comataPos + 1, e.length - 1); - if (comataPos == -1) throw({"msg":f+"() needs two parameter separated by comma.","pos":errPos}); - t = newOperation(f, parseString(p1, errPos+1, params), parseString(p2, errPos+1 + comataPos, params) ); + else + { + if (left.symbol == "+") { // (a+b)*c + t.setOperation('+', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } + else if (left.symbol == "-") { // (a-b)*c + t.setOperation('-', + newOperation('*', left.left.copy(), right.copy()), + newOperation('*', left.right.copy(), right.copy()) + ); + } } - else if (paramReg.match(s)) { // parameter - e = paramReg.matched(1); - t = newParam(e, (params==null) ? null : params.get(e)); + } + else if (right.symbol == "+" || right.symbol == "-") { + if (right.symbol == "+") { // a*(b+c) + t.setOperation('+', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); } - else if (signReg.match(s)) { // start with +- - e = signReg.matched(1); - s = s.substr(e.length); errPos += e.length; - e = ~/[\s+]/g.replace(e, ''); - if (e.length % 2 > 0) { - //s = "0-" + s; - if (numberReg.match(s)) { // followed by float number - e = numberReg.matched(1); - t = newValue(-Std.parseFloat(e)); - } else { // negative signed - t = newValue(0); - s = "-" + s; - e = ""; - negate = true; - } - } else continue; // positive signed + else if (right.symbol == "-") { // a*(b-c) + t.setOperation('-', + newOperation('*', left.copy(), right.left.copy()), + newOperation('*', left.copy(), right.right.copy()) + ); } - else if (twoSideOpReg.match(s)) { // start with other two side op - throw({"msg":"Missing left operand.","pos":errPos}); + } + } + + /* + * factorize a term: a*c+a*b -> a*(c+b) + * + */ + static public function factorize(t:TermNode):TermNode { + var tnew:TermNode = t.copy(); + _factorize(tnew); + return tnew; + } + + static function _factorize(t:TermNode):Void { + var mult_matrix:Array> = new Array(); + var add:Array = new Array(); + + // build matrix - addition in columns - multiplication in rows + traverseAddition(t, add); + var add_length_old:Int = 0; + for(i in add) { + if(i.symbol == "-") { + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1].right, mult_matrix[mult_matrix.length-1]); } else { - e = getBrackets(s, errPos); // term inside brackets - t = parseString(e.substring(1, e.length - 1), errPos+1, params); - } - - s = s.substr(e.length); errPos += e.length; - - if (operations.length > 0) operations[operations.length - 1].right = t; - - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - - if (twoSideOpReg.match(s)) { // two side operation symbol - e = twoSideOpReg.matched(1); errPos += e.length; - s = twoSideOpReg.matchedRight(); - spaces = trailingSpaces(s); s = s.substr(spaces); errPos += spaces; - operations.push( { symbol:e, left:t, right:null, leftOperation:null, rightOperation:null, precedence:((negate) ? -1 :precedence.get(e)) } ); - if (operations.length > 1) { - operations[operations.length - 2].rightOperation = operations[operations.length - 1]; - operations[operations.length - 1].leftOperation = operations[operations.length - 2]; - } - } else if (s.length > 0) { - if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.","pos":errPos}); - if (!(s.indexOf("(") == 0 || numberReg.match(s) || paramReg.match(s) || constantOpReg.match(s) || oneParamOpReg.match(s) || twoParamOpReg.match(s))) - throw({"msg":"Wrong char.","pos":errPos}); - else throw({"msg":"Missing operation.","pos":errPos}); + mult_matrix.push(new Array()); + traverseMultiplication(add[mult_matrix.length-1], mult_matrix[mult_matrix.length-1]); } } - if ( operations.length > 0 ) { - if ( operations[operations.length-1].right == null ) throw({"msg":"Missing right operand.","pos":errPos-spaces}); - else { - operations.sort(function(a:OperationNode, b:OperationNode):Int - { - if (a.precedence < b.precedence) return -1; - if (a.precedence > b.precedence) return 1; - return 0; - }); - for (op in operations) { - t = TermNode.newOperation(op.symbol, op.left, op.right); - if (op.leftOperation != null && op.rightOperation != null) { - op.rightOperation.leftOperation = op.leftOperation; - op.leftOperation.rightOperation = op.rightOperation; - } - if (op.leftOperation != null) op.leftOperation.right = t; - if (op.rightOperation != null) op.rightOperation.left = t; + // find and extract common factors + var part_of_all:Array = new Array(); + factorize_extract_common(mult_matrix, part_of_all); + if(part_of_all.length != 0) { + var new_add:Array = new Array(); + var helper:TermNode = new TermNode(); + for(i in mult_matrix) { + traverseMultiplicationBack(helper, i); + var v:TermNode = new TermNode(); + v.set(helper); + new_add.push(v); + } + for(i in 0...add.length) { + if(add[i].symbol == '-' && add[i].left.value == 0) { + new_add[i].setOperation('-', newValue(0), new_add[i].copy()); } - return t; } + + t.setOperation('*', new TermNode(), new TermNode()); + traverseMultiplicationBack(t.left, part_of_all); + traverseAdditionBack(t.right, new_add); } - else return t; } - static var comataPos:Int; - static function getBrackets(s:String, errPos:Int):String { - var pos:Int = 1; - if (s.indexOf("(") == 0) // check that s starts with opening bracket - { - if (~/^\(\s*\)/.match(s)) throw({"msg":"Empty brackets.", "pos":errPos}); - - var i,j,k:Int; - var openBrackets:Int = 1; - comataPos = -1; - while ( openBrackets > 0 ) - { - i = s.indexOf("(", pos); - j = s.indexOf(")", pos); - - // check for commata position - if (openBrackets == 1 && comataPos == -1) { - k = s.indexOf(",", pos); - if (k0) comataPos = k; + // delete common factors of mult_matrix and add them to part_of_all + static function factorize_extract_common(mult_matrix:Array>, part_of_all:Array):Void { + var bool:Bool = false; + var matrix_length_old:Int = -1; + var i:TermNode=new TermNode(); + var exponentiation_counter:Int = 0; + while(matrix_length_old != mult_matrix[0].length) { + matrix_length_old = mult_matrix[0].length; + for(p in mult_matrix[0]) { + if(p.symbol == '^') { + i.set(p.left); + exponentiation_counter++; } - - if ((i>0 && j>0 && i0 && j<0)) { // found open bracket - openBrackets++; pos = i + 1; + else if(p.symbol == '-' && p.left.isValue && p.left.value == 0) { + i.set(p.right); + } + else { + i.set(p); + } + for(j in 1...mult_matrix.length) { + bool = false; + for(h in mult_matrix[j]) { + if(isEqualAfterSimplify(h, i)) { + bool = true; + break; + } + else if(h.symbol == '^' && isEqualAfterSimplify(h.left , i)) { + bool=true; + exponentiation_counter++; + break; + + } + else if(h.symbol == '-' && h.left.isValue && h.left.value == 0 && isEqualAfterSimplify(h.right, i)) { + bool=true; + break; + } + } + if(bool == false) { + break; + } } - else if ((j>0 && i>0 && j0 && i<0)) { // found close bracket - openBrackets--; pos = j + 1; - } else { // no close or open found - throw({"msg":"Wrong bracket nesting.","pos":errPos}); + if(bool == true && exponentiation_counter < mult_matrix.length) { + part_of_all.push(new TermNode()); + part_of_all[part_of_all.length-1].set(i); + var helper:TermNode = new TermNode(); + helper.set(i); + delete_last_from_matrix(mult_matrix, helper); + break; } } - return s.substring(0, pos); - } - if (s.indexOf(")") == 0) throw({"msg":"No opening bracket.", "pos":errPos}); - else throw({"msg":"Wrong char.","pos":errPos}); - } - - - /* - * Puts out Math Expression as a String - * - */ - public function toString(?depth:Null = null, ?plOut:String = null):String { - var t:TermNode = this; - if (isName) t = left; - var options:Int; - switch (plOut) { - case 'glsl': options = noNeg|forceFloat|forcePow|forceMod|forceLog|forceAtan|forceConst; - default: options = 0; - } - return (left != null || !isName) ? t._toString(depth, options) : ''; - } - // options - public static inline var noNeg:Int = 1; - public static inline var forceFloat:Int = 2; - public static inline var forcePow:Int = 4; - public static inline var forceMod:Int = 8; - public static inline var forceLog:Int = 16; - public static inline var forceAtan:Int = 32; - public static inline var forceConst:Int = 64; - - inline function _toString(depth:Null, options:Int, ?isFirst:Bool=true):String { - if (depth == null) depth = -1; - return switch(symbol) { - case s if (isValue): floatToString(value, options); - //case s if (isName && isFirst): (left == null) ? symbol : left.toString(depth, false); - case s if (isName): (depth == 0 || left == null) ? symbol : left._toString(depth-1, options, false); - case s if (isParam): (depth == 0 || left == null) ? symbol : left._toString(depth-((left.isName)?0:1), options, false); - case s if (twoSideOpRegFull.match(s)) : - if (symbol == "-" && left.isValue && left.value == 0 && options&noNeg == 0) symbol + right._toString(depth, options, false); - else if (symbol == "^" && options&forcePow > 0) 'pow' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else if (symbol == "%" && options&forceMod > 0) 'mod' + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else ((isFirst)?"":"(") + left._toString(depth, options, false) + symbol + right._toString(depth, options, false) + ((isFirst)?'':")"); - case s if (twoParamOpRegFull.match(s)): - if (symbol == "log" && options&forceLog > 0) "(log(" + right._toString(depth, options) + ")/log(" + left._toString(depth, options) + "))"; - else if (symbol == "atan2" && options&forceAtan > 0) "atan(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - else symbol + "(" + left._toString(depth, options) + "," + right._toString(depth, options) + ")"; - case s if (constantOpRegFull.match(s)): - if (symbol == "pi" && options & forceConst > 0) Std.string(Math.PI); - else if (symbol == "e" && options&forceConst > 0) Std.string(Math.exp(1)); - else symbol + "()"; - default: - if (symbol == "ln" && options&forceLog > 0) 'log' + "(" + left._toString(depth, options) + ")"; - else symbol + "(" + left._toString(depth, options) + ")"; } } - inline function floatToString(value:Float, ?options:Int = 0):String { - var s:String = Std.string(value); - if (options&forceFloat > 0 && s.indexOf('.') == -1) s += ".0"; - return s; - } - - /* - * enrolls terms and subterms for debugging - * - */ - public function debug() { - //TODO - var out:String = "";// "(" + depth() + ")"; - for (i in 0 ... depth()+1) { - if (i == 0) out += ((name != null) ? name : "?") + " = "; else out += " -> "; - out += toString(i); + // deletes d from every row in mult_matrix once + static function delete_last_from_matrix(mult_matrix:Array>, d:TermNode):Void { + for(i in mult_matrix) { + if(i.length>1) { + for(j in 1...i.length+1) { + if(isEqualAfterSimplify(i[i.length-j], d)) { // a*x -> a + for(h in 0...j-1) { + i[i.length-j+h].set(i[i.length-j+h+1]); + } + i.pop(); + break; + } + else if(i[i.length-j].symbol == '^' && isEqualAfterSimplify(i[i.length-j].left, d)) { // x^n -> x^(n-1) + i[i.length-j].right.set(newOperation('-', i[i.length-j].right.copy(), newValue(1))); + break; + } + else if(i[i.length-j].symbol == '-' && i[i.length-j].left.isValue && i[i.length-j].left.value == 0 && isEqualAfterSimplify(i[i.length-j].right, d)) { + i[i.length-j].right.set(newValue(1)); + break; + } + } + } + else if(i[0].symbol == '^' && isEqualAfterSimplify(i[0].left, d)) { // x^n -> x^(n-1) + i[0].right.set(newOperation('-', i[0].right.copy(), newValue(1))); + } + else { + i[0].set(newValue(1)); + } } - trace(out); - } - - /* - * packs a TermNode and all sub-terms into Bytes - * - */ - public function toBytes():Bytes { - var b = new BytesOutput(); - _toBytes(b); - return b.getBytes(); } - inline function _toBytes(b:BytesOutput) { - // optimize (later to do): needs only 3 bit per TermNode type! - if (isValue) { - b.writeByte(0); - b.writeFloat(value); - } - else if (isName) { - b.writeByte((left!=null) ? 1:2); - _writeString(symbol, b); - if (left!=null) left._toBytes(b); + // compare function for Array.sort() + static function formsort_compare(t1:TermNode, t2:TermNode):Int + { + if (formsort_priority(t1) > formsort_priority(t2)) { + return -1; } - else if (isParam) { - b.writeByte((left!=null) ? 3:4); - _writeString(symbol, b); - if (left!=null) left._toBytes(b); + else if (formsort_priority(t1) < formsort_priority(t2)) { + return 1; } - else if (isOperation) { - b.writeByte(5); - var i:Int = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp))).indexOf(symbol); - if (i > -1) { - b.writeByte(i); - if (oneParamOpRegFull.match(symbol)) left._toBytes(b); - else if (twoSideOpRegFull.match(symbol) || twoParamOpRegFull.match(symbol) ) { - left._toBytes(b); - right._toBytes(b); + else{ + if (t1.isValue && t2.isValue) { + if (t1.value >= t2.value) { + return(-1); + } + else{ + return(1); + } + } + else if (t1.isOperation && t2.isOperation) { + if(t1.right != null && t2.right != null) { + return(formsort_compare(t1.right, t2.right)); + } + else { + return(formsort_compare(t1.left, t2.left)); } } - else throw("Error in _toBytes"); + else return 0; } - else throw("Error in _toBytes"); - } - - inline function _writeString(s:String, b:BytesOutput):Void { - b.writeByte((s.length<255) ? s.length: 255); - for (i in 0...((s.length<255) ? s.length: 255)) b.writeByte(symbol.charCodeAt(i)); - } - /* - * unserialize packed Bytes-Term to create a TermNode structure - * - */ - public static function fromBytes(b:Bytes):TermNode { - return _fromBytes(new BytesInput(b)); } - - static inline function _fromBytes(b:BytesInput):TermNode { - return switch (b.readByte()) { - case 0: TermNode.newValue(b.readFloat()); - case 1: TermNode.newName( _readString(b), _fromBytes(b) ); - case 2: TermNode.newName( _readString(b) ); - case 3: TermNode.newParam( _readString(b), _fromBytes(b) ); - case 4: TermNode.newParam( _readString(b) ); - case 5: - var op:String = twoSideOp.concat(constantOp.concat(oneParamOp.concat(twoParamOp)))[b.readByte()]; - if (oneParamOpRegFull.match(op)) TermNode.newOperation( op, _fromBytes(b) ); - else if (twoSideOpRegFull.match(op) || twoParamOpRegFull.match(op) ) TermNode.newOperation( op, _fromBytes(b), _fromBytes(b) ); - else TermNode.newOperation( op ); - default: throw("Error in _fromBytes"); + + // priority function for formsort_compare() + static function formsort_priority(t:TermNode):Float + { + return switch(t.symbol) + { + case s if (t.isParam): t.symbol.charCodeAt(0); + case s if (t.isName): t.symbol.charCodeAt(0); + case s if (t.isValue): 1+0.00001*t.value; + case s if (TermNode.twoSideOpRegFull.match(s)) : + if(t.symbol == '-' && t.left.value == 0) { + formsort_priority(t.right); + } + else { + formsort_priority(t.left)+formsort_priority(t.right)*0.001; + } + case s if (TermNode.oneParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.indexOf(s); + case s if (TermNode.twoParamOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.indexOf(s); + case s if (TermNode.constantOpRegFull.match(s)): -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.indexOf(s); + + default: -5 - TermNode.oneParamOp.length - TermNode.twoParamOp.length - TermNode.constantOp.length; } } - - static inline function _readString(b:BytesInput):String { - var len:Int = b.readByte(); - var s:String = ""; - for (i in 0...len) s += String.fromCharCode(b.readByte()); - return s; - } - - - /************************************************************************************** - * * - * various math operations transformation and more. * - * * - * * - **************************************************************************************/ - - /* - * creates a new term that is derivate of a given term - * - */ - public function derivate(paramName:String):TermNode return TermDerivate.derivate(this, paramName); - - /* - * Simplify: trims the length of a math expression - * - */ - public function simplify():TermNode return TermTransform.simplify(this); /* - * expands a mathmatical expression recursivly into a polynomial + * sort a Tree consisting of products * */ - public function expand():TermNode return TermTransform.expand(this); + static function arrangeMultiplication(t:TermNode):Void + { + var mult:Array = new Array(); + traverseMultiplication(t, mult); + mult.sort(formsort_compare); + traverseMultiplicationBack(t, mult); + } /* - * factorizes a mathmatical expression + * sort a Tree consisting of addition and subtraction * */ - public function factorize():TermNode return TermTransform.factorize(this); + static function arrangeAddition(t:TermNode):Void + { + var addlength_old:Int = -1; + var add:Array = new Array(); + traverseAddition(t, add); + add.sort(formsort_compare); + while(add.length != addlength_old) { + addlength_old = add.length; + for(i in 0...add.length-1) { + if(isEqualAfterSimplify(add[i], add[i+1])) { + add[i].setOperation('*', add[i].copy(), newValue(2)); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].symbol == '*' && add[i+1].symbol == '*' && add[i].right.isValue && add[i+1].right.isValue && isEqualAfterSimplify(add[i].left, add[i+1].left)) { + add[i].right.setValue(add[i].right.value+add[i+1].right.value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if(add[i].isValue && add[i+1].isValue) { + add[i].setValue(add[i].value+add[i+1].value); + for(j in 1...add.length-i-1) { + add[i+j] = add[i+j+1]; + } + add.pop(); + break; + } + if((add[i].symbol == '-' && add[i].left.isValue && add[i].left.value == 0 && isEqualAfterSimplify(add[i].right, add[i+1])) || (add[i+1].symbol == '-' && add[i+1].left.isValue && add[i+1].left.value == 0 && isEqualAfterSimplify(add[i+1].right, add[i]))) { + for(j in 0...add.length-i-2) { + add[i+j] = add[i+j+2]; + } + add.pop(); + add.pop(); + if(add.length == 0){ + add.push(newValue(0)); + } + break; + } + } + + if(add[0].symbol == '-' && add[0].left.value == 0) { + for(i in add) { + if(i.symbol == '-' && i.left.value == 0) { + i.set(i.right); + } + else { + i.setOperation('-', newValue(0), i.copy()); + } + } + t.setOperation('-', newValue(0), new TermNode()); + traverseAdditionBack(t.right, add); + return; + } + + } + + traverseAdditionBack(t, add); + } + } + /** - * abstract wrapper around TermNode + * solving one-dimensional, definite integrals RAM-efficiently using the trapezoidal rule + * by samusake * - * by Sylvio Sell, Rostock 2017 - */ + **/ +class Integrate { -@:forward( name, result, depth, params, hasParam, hasBinding, resolveAll, unbindAll, toBytes, debug, copy, derivate, simplify, expand, factorize) -abstract Formula(TermNode) from TermNode to TermNode -{ - /** - Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - @param formulaString the String that representing the math expression - **/ - inline public function new(formulaString:String) { - this = TermNode.fromString(formulaString); - } - - /** - Copy all from another Formula to this (keeps the own name if it is defined) - Keeps the bindings where this formula is linked into by a parameter. - @param formula the source formula from where the value is copyed - **/ - public inline function set(formula:Formula):Formula return this.set(formula); + static public inline function trapz(f:Formula, int_var:String, lower_bound:Float, higher_bound:Float, int_points:Int):Float { + var x:Formula, int:Float, x_low:Float, dx:Float, helper:Float; + x_low = lower_bound; + x=1.0;//x_low; + x.name = int_var; + f.bind(x); + int = 0; + dx = (higher_bound - lower_bound) / int_points; - /** - Link a variable inside of this formula to another formula - @param formula formula that will be linked into - @param paramName (optional) name of the variable to link with (e.g. if formula have no or different name) - **/ - public function bind(formula:Formula, ?paramName:String):Formula { - if (paramName != null) { - TermNode.checkValidName(paramName); - return this.bind( [paramName => formula] ); - } - else { - if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; - return this.bind( [formula.name => formula] ); - } - } - - /** - Link variables inside of this formula to another formulas - @param formulas array of formulas to link to variables - @param paramNames (optional) names of the variables to link with (e.g. if formulas have no or different names) - **/ - public function bindArray(formulas:Array, ?paramNames:Array):Formula { - var map = new Map(); - if (paramNames != null) { - if (paramNames.length != formulas.length) throw 'paramNames need to have the same length as formulas for bindArray().'; - for (i in 0...formulas.length) { - TermNode.checkValidName(paramNames[i]); - map.set(paramNames[i], formulas[i]); - } - } - else { - for (formula in formulas) { - if (formula.name == null) throw 'Can\'t bind unnamed formula:"${formula.toString()}" as parameter.'; - map.set(formula.name, formula); - } + for (i in 0...int_points) { + helper = f.result; + x_low += dx; + x.set(x_low); + int += (helper + f.result) / 2.0 * dx; } - return this.bind(map); - } - - /** - Link variables inside of this formula to another formulas - @param formulaMap map of formulas where the keys have same names as the variables to link with - **/ - public inline function bindMap(formulaMap:Map):Formula { - return this.bind(formulaMap); - } - - // ------------ unbind ------------- - - /** - Delete all connections of the linked formula - @param formula formula that has to be unlinked - **/ - public inline function unbind(formula:Formula):Formula { - return this.unbindTerm( [formula] ); - } - - /** - Delete all connections of the linked formulas - @param formulas array of formulas that has to be unlinked - **/ - public function unbindArray(formulas:Array):Formula { - return this.unbindTerm(formulas); - } - /** - Delete all connections to linked formulas for a given variable name - @param paramName name of the variable where the connected formula has to unlink from - **/ - public inline function unbindParam(paramName:String):Formula { - TermNode.checkValidName(paramName); - return this.unbind( [paramName] ); - } - - /** - Delete all connections to linked formulas for the given variable names - @param paramNames array of variablenames where the connected formula has to unlink from - **/ - public inline function unbindParamArray(paramNames:Array):Formula { - return this.unbind(paramNames); + return(int); } - // ----------------------------------- - - /** - Creates a new formula from a String, e.g. new("1+2") or new("f: 1+2") where "f" is the name of formula - @param depth (optional) how deep the variable-bindings should be resolved - @param plOut (optional) creates formula for a special language (only "glsl" at now) - **/ - inline public function toString(?depth:Null = null, ?plOut:String = null):String return this.toString(depth, plOut); +} - /** - Creates a formula from a packet Bytes representation - **/ - inline public static function fromBytes(b:Bytes):Formula return TermNode.fromBytes(b); - - @:to inline public function toStr():String return this.toString(0); - @:to inline public function toFloat():Float return this.result; - - @:from static public function fromString(a:String):Formula return TermNode.fromString(a); - @:from static public function fromFloat(a:Float):Formula return TermNode.newValue(a); - static inline function twoSideOp(op:String, a:Formula, b:Formula ):Formula { - return TermNode.newOperation( op, - (a.name != null ) ? TermNode.newParam(a.name, a) : a, - (b.name != null ) ? TermNode.newParam(b.name, b) : b - ); - } - @:op(A + B) static public function add (a:Formula, b:Formula):Formula return twoSideOp('+', a, b); - @:op(A - B) static public function subtract(a:Formula, b:Formula):Formula return twoSideOp('-', a, b); - @:op(A * B) static public function multiply(a:Formula, b:Formula):Formula return twoSideOp('*', a, b); - @:op(A / B) static public function divide (a:Formula, b:Formula):Formula return twoSideOp('/', a, b); - @:op(A ^ B) static public function potenz (a:Formula, b:Formula):Formula return twoSideOp('^', a, b); - @:op(A % B) static public function modulo (a:Formula, b:Formula):Formula return twoSideOp('%', a, b); +/** + * expetions for try/catch errorhandling + * + * by Sylvio Sell, Rostock 2022 + */ - public static inline function atan2(a:Formula, b:Formula):Formula return twoSideOp('atan2', a, b); - public static inline function log (a:Formula, b:Formula):Formula return twoSideOp('log', a, b); - public static inline function max (a:Formula, b:Formula):Formula return twoSideOp('max', a, b); - public static inline function min (a:Formula, b:Formula):Formula return twoSideOp('min', a, b); +#if (haxe_ver >= "4.1.0") - static inline function oneParamOp(op:String, a:Formula):Formula { - return TermNode.newOperation( op, - (a.name != null ) ? TermNode.newParam(a.name, a) : a - ); +class FormulaException extends haxe.Exception +{ + public function new(msg:String, pos:Int) { + super(msg); + this.pos = pos; } - public static inline function abs (a:Formula):Formula return oneParamOp('abs', a); - public static inline function ln (a:Formula):Formula return oneParamOp('ln', a); - public static inline function sin (a:Formula):Formula return oneParamOp('sin', a); - public static inline function cos (a:Formula):Formula return oneParamOp('cos', a); - public static inline function tan (a:Formula):Formula return oneParamOp('tan', a); - public static inline function cot (a:Formula):Formula return oneParamOp('cot', a); - public static inline function asin(a:Formula):Formula return oneParamOp('asin', a); - public static inline function acos(a:Formula):Formula return oneParamOp('acos', a); - public static inline function atan(a:Formula):Formula return oneParamOp('atan', a); + public var msg(get, never):String; + inline function get_msg() return message; + + public var pos:Int; } -class MathExpressionNode extends LogicNode { +#else - public var property0: String; // Expression - public var property1: Bool; // Clamp +typedef FormulaException = Dynamic; - public function new(tree: LogicTree) { - super(tree); - } +#end - override function get(from: Int): Dynamic { - var r: Float = 0.0; - var exp: String = property0; - // Variable - var a: Float = inputs[0].get(); - var b: Float = inputs[1].get(); - exp = StringTools.replace(exp, 'a', Std.string(a)); - exp = StringTools.replace(exp, 'b', Std.string(b)); - var c: Float = 0.0; - var d: Float = 0.0; - var e: Float = 0.0; - var x: Float = 0.0; - var y: Float = 0.0; - var h: Float = 0.0; - var i: Float = 0.0; - var k: Float = 0.0; - var i = 2; - while (i < inputs.length) { - switch (i) { - case 2: - c = inputs[i].get(); - exp = StringTools.replace(exp, 'c', Std.string(c)); - case 3: - d = inputs[i].get(); - exp = StringTools.replace(exp, 'd', Std.string(d)); - case 4: - e = inputs[i].get(); - exp = StringTools.replace(exp, 'e', Std.string(e)); - case 5: - x = inputs[i].get(); - exp = StringTools.replace(exp, 'x', Std.string(x)); - case 6: - y = inputs[i].get(); - exp = StringTools.replace(exp, 'y', Std.string(y)); - case 7: - h = inputs[i].get(); - exp = StringTools.replace(exp, 'h', Std.string(h)); - case 8: - i = inputs[i].get(); - exp = StringTools.replace(exp, 'i', Std.string(i)); - case 9: - k = inputs[i].get(); - exp = StringTools.replace(exp, 'k', Std.string(k)); - } - i++; - } - // Expression - try { - var f: Formula = new Formula(exp); - r = f.result; - } catch(msg: String) { - #if arm_debug - trace(msg); - #end - } - // Clamp - if (property1) r = r < 0.0 ? 0.0 : (r > 1.0 ? 1.0 : r); - return r; +/** + * collect all error messages + * + * by Sylvio Sell, Rostock 2022 + */ + +class ErrorMsg +{ + static inline function error(msg:String, pos:Int = 0) { + #if (haxe_ver >= "4.1.0") + throw new FormulaException(msg, pos); + #else + throw({msg:msg, pos:pos}); + #end } + + + // --------------- Formula ---------------- + + public static inline function cantBindUnnamed(formula:Formula, bindFormula:Formula) + error('Can\'t bind unnamed formula: \'${bindFormula.toString()}\'. Specify the available parameter: \'${formula.params().join("\' ,\'")}\' or name the formula to one.'); + + public static inline function bindArrayWrongLengths(nf:Int, np:Int) + error('The array-length of formulas ($nf) have to be the same as paramNames ($np) for bindArray().'); + + + + // --------------- TermNode --------------- + + public static inline function noValidOperation(s:String) + error('"$s" is no valid operation.'); + + public static inline function wrongCharInsideName(name:String) + error('Wrong character inside name "$name".'); + + public static inline function emptyFunction(s:String) + error('Empty function "$s".'); + + public static inline function missingParameter(s:String) + error('Missing parameter "$s".'); + + + // formula parsing + public static inline function cantParseFromEmptyString(pos:Int) + error("Can't parse Term from empty string.", pos); + + public static inline function operatorNeedTwoArgs(op:String, pos:Int) + error('Operation "$op()" needs two arguments separated by comma.', pos); + + public static inline function missingLeftOperand(pos:Int) + error("Missing left operand.", pos); + + public static inline function noOpeningBracket(pos:Int) + error("No opening bracket.", pos); + + public static inline function wrongChar(pos:Int) + error("Wrong char.", pos); + + public static inline function missingOperation(pos:Int) + error("Missing operation.", pos); + + public static inline function missingRightOperand(pos:Int) + error("Missing right operand.", pos); + + public static inline function emptyBrackets(pos:Int) + error("Empty brackets.", pos); + + public static inline function wrongBracketNesting(pos:Int) + error("Wrong bracket nesting.", pos); + + + // by en/decoding to/from Bytes + public static inline function intoBytes() + error("Can't encode into Bytes."); + + public static inline function fromBytes() + error("Can't decode from Bytes."); + + + + // --------------- TermDerivate --------------- + + public static inline function notImplementedFor(s:String) + error('Derivation of "$s" is not implemented.'); + } \ No newline at end of file diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx index 695285acab..9ebf7a7df6 100644 --- a/Sources/armory/logicnode/NetworkHttpRequestNode.hx +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -27,9 +27,9 @@ class NetworkHttpRequestNode extends LogicNode { } var request = new haxe.Http(url); - + #if js request.async = true; - + #end if(headers != null){ for (k in headers.keys()) { request.addHeader( k, headers[k]); diff --git a/Sources/armory/logicnode/RotationMathNode.hx b/Sources/armory/logicnode/RotationMathNode.hx index 2af24da5cd..4f9b3f763f 100644 --- a/Sources/armory/logicnode/RotationMathNode.hx +++ b/Sources/armory/logicnode/RotationMathNode.hx @@ -54,7 +54,7 @@ class RotationMathNode extends LogicNode { } case "Amplify": { var v1: Quat = inputs[0].get(); - var v2: Float = inputs[1].get(); + var v2: Null = inputs[1].get(); if ((v1 == null) || (v2 == null)) return null; res_q.setFrom(v1); var fac2 = Math.sqrt(1- res_q.w*res_q.w); @@ -78,7 +78,11 @@ class RotationMathNode extends LogicNode { //var from = q; var from: Quat = inputs[0].get(); var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); + #if js + var f: Null = inputs[2].get(); + #else + var f: Null = inputs[2].get(); + #end if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.lerp(from, to, f); } @@ -86,11 +90,15 @@ class RotationMathNode extends LogicNode { //var from = q; var from:Quat = inputs[0].get(); var to: Quat = inputs[1].get(); - var f: Float = inputs[2].get(); + #if js + var f: Null = inputs[2].get(); + #else + var f: Null = inputs[2].get(); + #end if ((from == null) || (f == null) || (to == null)) return null; res_q = res_q.slerp(from, to, f); } } return res_q; } -} \ No newline at end of file +} diff --git a/Sources/armory/logicnode/SetVisibleNode.hx b/Sources/armory/logicnode/SetVisibleNode.hx index 2fe99d2493..2abd6b462b 100644 --- a/Sources/armory/logicnode/SetVisibleNode.hx +++ b/Sources/armory/logicnode/SetVisibleNode.hx @@ -14,31 +14,44 @@ class SetVisibleNode extends LogicNode { var object: Object = inputs[1].get(); var visible: Bool = inputs[2].get(); var children: Bool = inputs[3].get(); + var recursive: Bool = inputs[4].get(); if (object == null) return; - var objectChildren: Array = object.children; - switch (property0) { - case "object": - object.visible = visible; - if (children == true) for (child in objectChildren) { - child.visible = visible; - } - - case "mesh": - object.visibleMesh = visible; - if (children == true) for (child in objectChildren) { - child.visibleMesh = visible; - } - - case "shadow": - object.visibleShadow = visible; - if (children == true) for (child in objectChildren) { - child.visibleShadow = visible; + case "object": + object.visible = visible; + case "mesh": + object.visibleMesh = visible; + case "shadow": + object.visibleShadow = visible; } - } + + if (children) setVisisbleRecursive(property0, object, visible, recursive); runOutput(0); } + + function setVisisbleRecursive(property: String, object: Object, visible: Bool, recursive: Bool) { + var objectChildren: Array = object.children; + switch (property) { + case "object": + for (child in objectChildren) { + child.visible = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + + case "mesh": + for (child in objectChildren) { + child.visibleMesh = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + + case "shadow": + for (child in objectChildren) { + child.visibleShadow = visible; + if (recursive) setVisisbleRecursive(property, child, visible, recursive); + } + } + } } diff --git a/Sources/armory/logicnode/SpawnObjectByNameNode.hx b/Sources/armory/logicnode/SpawnObjectByNameNode.hx index 5ecb1bc71a..8a20ece8d2 100644 --- a/Sources/armory/logicnode/SpawnObjectByNameNode.hx +++ b/Sources/armory/logicnode/SpawnObjectByNameNode.hx @@ -59,7 +59,6 @@ class SpawnObjectByNameNode extends LogicNode { } #end } - object.visible = true; runOutput(0); }, spawnChildren, rawScene); diff --git a/Sources/armory/logicnode/SpawnObjectNode.hx b/Sources/armory/logicnode/SpawnObjectNode.hx index e6f16aac7f..5529bc8cea 100644 --- a/Sources/armory/logicnode/SpawnObjectNode.hx +++ b/Sources/armory/logicnode/SpawnObjectNode.hx @@ -37,7 +37,6 @@ class SpawnObjectNode extends LogicNode { } #end } - object.visible = true; runOutput(0); }, spawnChildren); } diff --git a/Sources/armory/renderpath/Inc.hx b/Sources/armory/renderpath/Inc.hx index 90814795c8..01459acd2c 100644 --- a/Sources/armory/renderpath/Inc.hx +++ b/Sources/armory/renderpath/Inc.hx @@ -395,7 +395,6 @@ class Inc { path.setTarget("revealage"); path.clearTarget(0xffffffff); path.setTarget("accum", ["revealage"]); - #if rp_shadowmap { #if arm_shadowmap_atlas @@ -405,7 +404,6 @@ class Inc { #end } #end - path.drawMeshes("translucent"); #if rp_render_to_texture { @@ -416,7 +414,6 @@ class Inc { path.setTarget(""); } #end - path.bindTarget("accum", "gbuffer0"); path.bindTarget("revealage", "gbuffer1"); path.drawShader("shader_datas/translucent_resolve/translucent_resolve"); diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index b9a21aa287..48b65d0e2e 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -334,7 +334,7 @@ class RenderPathDeferred { path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.loadShader("shader_datas/copy_pass/copy_pass"); - //holds rior and opacity + //holds rior and opacity var t = new RenderTargetRaw(); t.name = "gbuffer_refraction"; t.width = 0; diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index dbe02c0273..4d6c6430aa 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -22,11 +22,10 @@ class RenderPathForward { public static function setTargetMeshes() { #if rp_render_to_texture { - #if rp_ssr - path.setTarget("lbuffer0", ["lbuffer1"]); - #else - path.setTarget("lbuffer0"); - #end + path.setTarget("lbuffer0", [ + #if rp_ssr "lbuffer1", #end + #if rp_ssrefr "gbuffer_refraction" #end] + ); } #else { @@ -119,6 +118,41 @@ class RenderPathForward { } #end + #if rp_ssrefr + { + //holds rior and opacity + var t = new RenderTargetRaw(); + t.name = "gbuffer_refraction"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + //holds colors before refractive meshes are drawn + var t = new RenderTargetRaw(); + t.name = "refr"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "RGBA64"; + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + //holds background depth + var t = new RenderTargetRaw(); + t.name = "gbufferD1"; + t.width = 0; + t.height = 0; + t.displayp = Inc.getDisplayp(); + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + } + #end + #if rp_compositornodes { path.loadShader("shader_datas/compositor_pass/compositor_pass"); @@ -265,27 +299,6 @@ class RenderPathForward { { path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.loadShader("shader_datas/copy_pass/copy_pass"); - - //holds colors before refractive meshes are drawn - var t = new RenderTargetRaw(); - t.name = "refr"; - t.width = 0; - t.height = 0; - t.displayp = Inc.getDisplayp(); - t.format = "RGBA64"; - t.scale = Inc.getSuperSampling(); - t.depth_buffer = "main"; - path.createRenderTarget(t); - - //holds background depth - var t = new RenderTargetRaw(); - t.name = "gbufferD1"; - t.width = 0; - t.height = 0; - t.displayp = Inc.getDisplayp(); - t.format = "R32"; - t.scale = Inc.getSuperSampling(); - path.createRenderTarget(t); } #end @@ -424,7 +437,8 @@ class RenderPathForward { #if rp_ssrefr { - if (armory.data.Config.raw.rp_ssrefr != false) { + if (armory.data.Config.raw.rp_ssrefr != false) + { path.setTarget("gbufferD1"); path.bindTarget("_main", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); @@ -442,6 +456,7 @@ class RenderPathForward { path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("lbuffer1", "gbuffer0"); + path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); } } diff --git a/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx b/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx new file mode 100644 index 0000000000..a528839df8 --- /dev/null +++ b/Sources/armory/trait/physics/bullet/DebugDrawHelper.hx @@ -0,0 +1,224 @@ +package armory.trait.physics.bullet; + +import bullet.Bt.Vector3; + +import kha.FastFloat; +import kha.System; + +import iron.math.Vec4; + +#if arm_ui +import armory.ui.Canvas; +#end + +using StringTools; + +class DebugDrawHelper { + static inline var contactPointSizePx = 4; + static inline var contactPointNormalColor = 0xffffffff; + static inline var contactPointDrawLifetime = true; + + final physicsWorld: PhysicsWorld; + final lines: Array = []; + final texts: Array = []; + var font: kha.Font = null; + + var debugMode: PhysicsWorld.DebugDrawMode = NoDebug; + + public function new(physicsWorld: PhysicsWorld) { + this.physicsWorld = physicsWorld; + + #if arm_ui + iron.data.Data.getFont(Canvas.defaultFontName, function(defaultFont: kha.Font) { + font = defaultFont; + }); + #end + + iron.App.notifyOnRender2D(onRender); + } + + public function drawLine(from: bullet.Bt.Vector3, to: bullet.Bt.Vector3, color: bullet.Bt.Vector3) { + #if js + // https://github.com/InfiniteLee/ammo-debug-drawer/pull/1/files + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html#pointers-and-comparisons + from = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", from); + to = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", to); + color = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", color); + #end + + final fromScreenSpace = worldToScreenFast(new Vec4(from.x(), from.y(), from.z(), 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(to.x(), to.y(), to.z(), 1.0)); + + // For now don't draw lines if any point is outside of clip space z, + // investigate how to clamp lines to clip space borders + if (fromScreenSpace.w == 1 && toScreenSpace.w == 1) { + lines.push({ + fromX: fromScreenSpace.x, + fromY: fromScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: kha.Color.fromFloats(color.x(), color.y(), color.z(), 1.0) + }); + } + } + + public function drawContactPoint(pointOnB: Vector3, normalOnB: Vector3, distance: kha.FastFloat, lifeTime: Int, color: Vector3) { + #if js + pointOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", pointOnB); + normalOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", normalOnB); + color = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", color); + #end + + final contactPointScreenSpace = worldToScreenFast(new Vec4(pointOnB.x(), pointOnB.y(), pointOnB.z(), 1.0)); + final toScreenSpace = worldToScreenFast(new Vec4(pointOnB.x() + normalOnB.x() * distance, pointOnB.y() + normalOnB.y() * distance, pointOnB.z() + normalOnB.z() * distance, 1.0)); + + if (contactPointScreenSpace.w == 1) { + final color = kha.Color.fromFloats(color.x(), color.y(), color.z(), 1.0); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y - contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y + contactPointSizePx, + color: color + }); + + lines.push({ + fromX: contactPointScreenSpace.x - contactPointSizePx, + fromY: contactPointScreenSpace.y + contactPointSizePx, + toX: contactPointScreenSpace.x + contactPointSizePx, + toY: contactPointScreenSpace.y - contactPointSizePx, + color: color + }); + + if (toScreenSpace.w == 1) { + lines.push({ + fromX: contactPointScreenSpace.x, + fromY: contactPointScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: contactPointNormalColor + }); + } + + if (contactPointDrawLifetime && font != null) { + texts.push({ + x: contactPointScreenSpace.x, + y: contactPointScreenSpace.y, + color: color, + text: Std.string(lifeTime), // lifeTime: number of frames the contact point existed + }); + } + } + } + + public function reportErrorWarning(warningString: bullet.Bt.BulletString) { + trace(warningString.toHaxeString().trim()); + } + + public function draw3dText(location: Vector3, textString: bullet.Bt.BulletString) { + if (font == null) { + return; + } + + #if js + location = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", location); + #end + + final locationScreenSpace = worldToScreenFast(new Vec4(location.x(), location.y(), location.z(), 1.0)); + + texts.push({ + x: locationScreenSpace.x, + y: locationScreenSpace.y, + color: kha.Color.fromFloats(0.0, 0.0, 0.0, 1.0), + text: textString.toHaxeString() + }); + } + + public function setDebugMode(debugMode: PhysicsWorld.DebugDrawMode) { + this.debugMode = debugMode; + } + + public function getDebugMode(): PhysicsWorld.DebugDrawMode { + #if js + return debugMode; + #elseif hl + return physicsWorld.getDebugDrawMode(); + #else + return NoDebug; + #end + } + + function onRender(g: kha.graphics2.Graphics) { + if (getDebugMode() == NoDebug) { + return; + } + + // It might be a bit unusual to call this method in a render callback + // instead of the update loop (after all it doesn't draw anything but + // will cause Bullet to call the btIDebugDraw callbacks), but this way + // we can ensure that--within a frame--the function will not be called + // before some user-specific physics update, which would result in a + // one-frame drawing delay... Ideally we would ensure that debugDrawWorld() + // is called when all other (late) update callbacks are already executed... + physicsWorld.world.debugDrawWorld(); + + g.opacity = 1.0; + + for (line in lines) { + g.color = line.color; + g.drawLine(line.fromX, line.fromY, line.toX, line.toY, 1.0); + } + lines.resize(0); + + if (font != null) { + g.font = font; + g.fontSize = 12; + for (text in texts) { + g.color = text.color; + g.drawString(text.text, text.x, text.y); + } + texts.resize(0); + } + } + + /** + Transform a world coordinate vector into screen space and store the + result in the input vector's x and y coordinates. The w coordinate is + set to 0 if the input vector is outside the active camera's far and near + planes, and 1 otherwise. + **/ + inline function worldToScreenFast(loc: Vec4): Vec4 { + final cam = iron.Scene.active.camera; + loc.w = 1.0; + loc.applyproj(cam.VP); + + if (loc.z < -1 || loc.z > 1) { + loc.w = 0.0; + } + else { + loc.x = (loc.x + 1) * 0.5 * System.windowWidth(); + loc.y = (1 - loc.y) * 0.5 * System.windowHeight(); + loc.w = 1.0; + } + + return loc; + } +} + +@:structInit +class LineData { + public var fromX: FastFloat; + public var fromY: FastFloat; + public var toX: FastFloat; + public var toY: FastFloat; + public var color: kha.Color; +} + +@:structInit +class TextData { + public var x: FastFloat; + public var y: FastFloat; + public var color: kha.Color; + public var text: String; +} diff --git a/Sources/armory/trait/physics/bullet/PhysicsHook.hx b/Sources/armory/trait/physics/bullet/PhysicsHook.hx index e0f8e45008..5501735fee 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsHook.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsHook.hx @@ -92,17 +92,15 @@ class PhysicsHook extends Trait { #if js var nodes = sb.body.get_m_nodes(); - #elseif cpp + #else var nodes = sb.body.m_nodes; #end - var geom = cast(object, MeshObject).data.geom; - var numNodes = Std.int(geom.positions.values.length / 4); - for (i in 0...numNodes) { + for (i in 0...nodes.size()) { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); - #elseif cpp + #else var nodePos = node.m_x; #end diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx index 6af4cd2761..97f40499a3 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -79,11 +79,13 @@ class PhysicsWorld extends Trait { static var transform1: bullet.Bt.Transform = null; static var transform2: bullet.Bt.Transform = null; + var debugDrawHelper: DebugDrawHelper = null; + #if arm_debug public static var physTime = 0.0; #end - public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10) { + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, debugDrawMode: DebugDrawMode = NoDebug) { super(); if (nullvec) { @@ -122,6 +124,8 @@ class PhysicsWorld extends Trait { _lateUpdate = [lateUpdate]; @:privateAccess iron.App.traitLateUpdates.insert(0, lateUpdate); + setDebugDrawMode(debugDrawMode); + iron.Scene.active.notifyOnRemove(function() { sceneRemoved = true; }); @@ -445,7 +449,7 @@ class PhysicsWorld extends Trait { var worldCol: bullet.Bt.CollisionWorld = worldDyn; var bodyColl: bullet.Bt.ConvexShape = cast rb.body.getCollisionShape(); worldCol.convexSweepTest(bodyColl, transformFrom, transformTo, convexCallback, 0.0); - + var hitInfo: ConvexHit = null; var cc: bullet.Bt.ClosestConvexResultCallback = convexCallback; @@ -483,6 +487,187 @@ class PhysicsWorld extends Trait { public function removePreUpdate(f: Void->Void) { preUpdates.remove(f); } + + public function setDebugDrawMode(debugDrawMode: DebugDrawMode) { + if (debugDrawHelper == null) { + if (debugDrawMode == NoDebug) { + return; + } + initDebugDrawing(); + } + + #if js + world.getDebugDrawer().setDebugMode(debugDrawMode); + #elseif hl + hlDebugDrawer_setDebugMode(debugDrawMode); + #end + } + + public inline function getDebugDrawMode(): DebugDrawMode { + if (debugDrawHelper == null) { + return NoDebug; + } + + #if js + return world.getDebugDrawer().getDebugMode(); + #elseif hl + return hlDebugDrawer_getDebugMode(); + #else + return NoDebug; + #end + } + + function initDebugDrawing() { + debugDrawHelper = new DebugDrawHelper(this); + + #if js + final drawer = new bullet.Bt.DebugDrawer(); + + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html?highlight=jsimplementation#sub-classing-c-base-classes-in-javascript-jsimplementation + drawer.drawLine = debugDrawHelper.drawLine; + drawer.drawContactPoint = debugDrawHelper.drawContactPoint; + drawer.reportErrorWarning = debugDrawHelper.reportErrorWarning; + drawer.draw3dText = debugDrawHelper.draw3dText; + + // From the Armory API perspective this is not required, + // but Ammo requires it and will result in a black screen if not set + drawer.setDebugMode = debugDrawHelper.setDebugMode; + drawer.getDebugMode = debugDrawHelper.getDebugMode; + + world.setDebugDrawer(drawer); + #elseif hl + hlDebugDrawer_setDrawLine(debugDrawHelper.drawLine); + hlDebugDrawer_setDrawContactPoint(debugDrawHelper.drawContactPoint); + hlDebugDrawer_setReportErrorWarning(debugDrawHelper.reportErrorWarning); + hlDebugDrawer_setDraw3dText(debugDrawHelper.draw3dText); + + hlDebugDrawer_worldSetGlobalDebugDrawer(world); + #end + } + + #if hl + @:hlNative("bullet", "debugDrawer_worldSetGlobalDebugDrawer") + public static function hlDebugDrawer_worldSetGlobalDebugDrawer(world: #if arm_physics_soft bullet.Bt.SoftRigidDynamicsWorld #else bullet.Bt.DiscreteDynamicsWorld #end) {} + + @:hlNative("bullet", "debugDrawer_setDebugMode") + public static function hlDebugDrawer_setDebugMode(debugMode: Int) {} + + @:hlNative("bullet", "debugDrawer_getDebugMode") + public static function hlDebugDrawer_getDebugMode(): Int { return 0; } + + @:hlNative("bullet", "debugDrawer_setDrawLine") + public static function hlDebugDrawer_setDrawLine(func: bullet.Bt.Vector3->bullet.Bt.Vector3->bullet.Bt.Vector3->Void) {} + + @:hlNative("bullet", "debugDrawer_setDrawContactPoint") + public static function hlDebugDrawer_setDrawContactPoint(func: bullet.Bt.Vector3->bullet.Bt.Vector3->kha.FastFloat->Int->bullet.Bt.Vector3->Void) {} + + @:hlNative("bullet", "debugDrawer_setReportErrorWarning") + public static function hlDebugDrawer_setReportErrorWarning(func: hl.Bytes->Void) {} + + @:hlNative("bullet", "debugDrawer_setDraw3dText") + public static function hlDebugDrawer_setDraw3dText(func: bullet.Bt.Vector3->hl.Bytes->Void) {} + #end +} + +/** + Debug flags for Bullet physics, despite the name not solely related to debug drawing. + You can combine multiple flags with bitwise operations. + + Taken from Bullet's `btIDebugDraw::DebugDrawModes` enum. + Please note that the descriptions of the individual flags are a result of inspecting the Bullet sources and thus might contain inaccuracies. + + @see `armory.trait.physics.PhysicsWorld.getDebugDrawMode()` + @see `armory.trait.physics.PhysicsWorld.setDebugDrawMode()` +**/ +// Not all of the flags below are actually used in the library core, some of them are only used +// in individual Bullet example projects. The intention of the original authors is unknown, +// so whether those flags are actually meant to get their purpose from the implementing application +// and not from the library remains a mystery... +enum abstract DebugDrawMode(Int) from Int to Int { + /** All debug flags off. **/ + var NoDebug = 0; + + /** Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations. **/ + var DrawWireframe = 1; + + /** Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes. **/ + var DrawAABB = 1 << 1; + + /** Not used in Armory. **/ + // Only used in a Bullet physics example at the moment: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ExampleBrowser/GL_ShapeDrawer.cpp#L616-L644 + var DrawFeaturesText = 1 << 2; + + /** Visualize contact points of multiple colliders. **/ + var DrawContactPoints = 1 << 3; + + /** + Globally disable sleeping/deactivation of dynamic colliders. + **/ + var NoDeactivation = 1 << 4; + + /** Not used in Armory. **/ + // Not used in the library core, in some Bullet examples this flag is used to print application-specific help text (e.g. keyboard shortcuts) to the screen, e.g.: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ForkLift/ForkLiftDemo.cpp#L586 + var NoHelpText = 1 << 5; + + /** Not used in Armory. **/ + // Not used in the library core, appears to be the opposite of NoHelpText (not sure why there are two flags required for this...) + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/FractureDemo/FractureDemo.cpp#L189 + var DrawText = 1 << 6; + + /** Not used in Armory. **/ + // Not even used in official Bullet examples, probably obsolete. + // Related to btQuickprof.h: https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=1285#p4743 + // Probably replaced by define: https://github.com/bulletphysics/bullet3/commit/d051e2eacb01a948c7b53e24fd3d9942ce64bdcc + var ProfileTimings = 1 << 7; + + /** Not used in Armory. **/ + // Not even used in official Bullet examples, might be obsolete. + var EnableSatComparison = 1 << 8; + + /** Not used in Armory. **/ + var DisableBulletLCP = 1 << 9; + + /** Not used in Armory. **/ + var EnableCCD = 1 << 10; + + /** Draw axis gizmos for important constraint points. **/ + var DrawConstraints = 1 << 11; + + /** Draw additional constraint information such as distance or angle limits. **/ + var DrawConstraintLimits = 1 << 12; + + /** Not used in Armory. **/ + // Only used in a Bullet physics example at the moment: + // https://github.com/bulletphysics/bullet3/blob/39b8de74df93721add193e5b3d9ebee579faebf8/examples/ExampleBrowser/GL_ShapeDrawer.cpp#L258 + // We could use it in the future to toggle depth testing for lines, i.e. draw actual 3D lines if not set and Kha's g2 lines if set. + var FastWireframe = 1 << 13; + + /** + Draw the normal vectors of the triangles of the physics collider meshes. + This only works for `Mesh` collision shapes. + **/ + // Outside of Armory this works for a few more collision shapes + var DrawNormals = 1 << 14; + + /** + Draw a small axis gizmo at the origin of the collision shape. + Only works if `DrawWireframe` is enabled as well. + **/ + var DrawFrames = 1 << 15; + + @:op(~A) public inline function bitwiseNegate(): DebugDrawMode { + return ~this; + } + + @:op(A & B) public inline function bitwiseAND(other: DebugDrawMode): DebugDrawMode { + return this & other; + } + + @:op(A | B) public inline function bitwiseOR(other: DebugDrawMode): DebugDrawMode { + return this | other; + } } #end diff --git a/Sources/armory/trait/physics/bullet/SoftBody.hx b/Sources/armory/trait/physics/bullet/SoftBody.hx index e45344e16b..9e44f12267 100644 --- a/Sources/armory/trait/physics/bullet/SoftBody.hx +++ b/Sources/armory/trait/physics/bullet/SoftBody.hx @@ -1,7 +1,11 @@ package armory.trait.physics.bullet; #if arm_bullet - +import iron.Scene; +import haxe.ds.Vector; +import kha.arrays.ByteArray; +import bullet.Bt.Vector3; +import bullet.Bt.CollisionObjectActivationState; import iron.math.Vec4; import iron.math.Mat4; import iron.Trait; @@ -9,6 +13,7 @@ import iron.object.MeshObject; import iron.data.Geometry; import iron.data.MeshData; import iron.data.SceneFormat; +import kha.arrays.Uint32Array; #if arm_physics_soft import armory.trait.physics.RigidBody; import armory.trait.physics.PhysicsWorld; @@ -37,6 +42,8 @@ class SoftBody extends Trait { static var helpersCreated = false; static var worldInfo: bullet.Bt.SoftBodyWorldInfo; + var vertexIndexMap: Map>; + public function new(shape = SoftShape.Cloth, bend = 0.5, mass = 1.0, margin = 0.04) { super(); this.shape = shape; @@ -44,13 +51,14 @@ class SoftBody extends Trait { this.mass = mass; this.margin = margin; - iron.Scene.active.notifyOnInit(function() { - notifyOnInit(init); - }); + //notifyOnAdd(init); + //The above line works as well, but the object transforms are not set + //properly, so the positions are not accurate + notifyOnInit(init); } - function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): kha.arrays.Float32Array { - var vals = new kha.arrays.Float32Array(Std.int(ar.length / 4) * 3); + function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): haxe.ds.Vector { + var vals = new haxe.ds.Vector(Std.int(ar.length / 4) * 3); for (i in 0...Std.int(vals.length / 3)) { vals[i * 3 ] = (ar[i * 4 ] / 32767) * scalePos; vals[i * 3 + 1] = (ar[i * 4 + 1] / 32767) * scalePos; @@ -59,10 +67,10 @@ class SoftBody extends Trait { return vals; } - function fromU32(ars: Array): kha.arrays.Uint32Array { + function fromU32(ars: Array): haxe.ds.Vector { var len = 0; for (ar in ars) len += ar.length; - var vals = new kha.arrays.Uint32Array(len); + var vals = new haxe.ds.Vector(len); var i = 0; for (ar in ars) { for (j in 0...ar.length) { @@ -73,8 +81,36 @@ class SoftBody extends Trait { return vals; } - var v = new Vec4(); + function generateVertexIndexMap(ind: haxe.ds.Vector, vert: haxe.ds.Vector) { + if (vertexIndexMap == null) vertexIndexMap = new Map(); + for (i in 0...ind.length) { + var currentVertex = vert[i]; + var currentIndex = ind[i]; + + var mapping = vertexIndexMap.get(currentVertex); + if (mapping == null) { + vertexIndexMap.set(currentVertex, [currentIndex]); + } + else { + if(! mapping.contains(currentIndex)) mapping.push(currentIndex); + } + } + } + function init() { + var mo = cast(object, MeshObject); + //Set new mesh data for this object + new MeshData(mo.data.raw, function (data) { + mo.setData(data); + //Init soft body after setting new data + initSoftBody(); + //If the above line is commented out, the program becomes unresponsive with white screen + //and no errors. + }); + } + + var v = new Vec4(); + function initSoftBody() { if (ready) return; ready = true; @@ -83,6 +119,17 @@ class SoftBody extends Trait { var mo = cast(object, MeshObject); mo.frustumCulling = false; var geom = mo.data.geom; + var rawData = mo.data.raw; + var vertexMap: Array = []; + for (ind in rawData.index_arrays) { + if (ind.vertex_map == null) return; + vertexMap.push(ind.vertex_map); + } + + var vecind = fromU32(geom.indices); + var vertexMapArray = fromU32(vertexMap); + + generateVertexIndexMap(vecind, vertexMapArray); // Parented soft body - clear parent location if (object.parent != null && object.parent.name != "") { @@ -105,16 +152,7 @@ class SoftBody extends Trait { positions[i * 3 + 1] = v.y; positions[i * 3 + 2] = v.z; } - vertOffsetX = object.transform.worldx(); - vertOffsetY = object.transform.worldy(); - vertOffsetZ = object.transform.worldz(); - - object.transform.scale.set(1, 1, 1); - object.transform.loc.set(0, 0, 0); - object.transform.rot.set(0, 0, 0, 1); - object.transform.buildMatrix(); - var vecind = fromU32(geom.indices); var numtri = 0; for (ar in geom.indices) numtri += Std.int(ar.length / 3); @@ -122,14 +160,49 @@ class SoftBody extends Trait { helpers = new bullet.Bt.SoftBodyHelpers(); worldInfo = physics.world.getWorldInfo(); helpersCreated = true; + #if hl + //world info is passed as value and not as a reference, need to set gravity again in HL. + worldInfo.m_gravity = physics.world.getGravity(); + #end + } + + var vertsLength = 0; + for (key in vertexIndexMap.keys()) vertsLength++; + var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(vertsLength * 3); + for (key in 0...vertsLength){ + var i = vertexIndexMap.get(key)[0]; + positionsVector.set(key * 3 , positions[i * 3 ]); + positionsVector.set(key * 3 + 1, positions[i * 3 + 1]); + positionsVector.set(key * 3 + 2, positions[i * 3 + 2]); + } + + var indexMax: Int = 0; + var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vertexMapArray.length); + for (i in 0...vecindVector.length){ + var idx = vertexMapArray.get(i); + vecindVector.set(i, idx); + indexMax = indexMax > idx ? indexMax : idx; } #if js - body = helpers.CreateFromTriMesh(worldInfo, cast positions, cast vecind, numtri); - #elseif cpp - untyped __cpp__("body = helpers.CreateFromTriMesh(worldInfo, positions->self.data, (int*)vecind->self.data, numtri);"); - #end + body = helpers.CreateFromTriMesh(worldInfo, positionsVector, vecindVector, numtri); + #else + //Create helper float array + var floatArray = new bullet.Bt.FloatArray(positionsVector.length); + for (i in 0...positionsVector.length){ + floatArray.set(i, positionsVector[i]); + } + //Create helper int array + var intArray = new bullet.Bt.IntArray(vecindVector.length); + for (i in 0...vecindVector.length){ + intArray.set(i, vecindVector[i]); + } + //Create soft body + body = helpers.CreateFromTriMesh(worldInfo, floatArray.raw, intArray.raw, numtri, false); + floatArray.delete(); + intArray.delete(); + #end // body.generateClusters(4); #if js @@ -137,30 +210,39 @@ class SoftBody extends Trait { cfg.set_viterations(physics.solverIterations); cfg.set_piterations(physics.solverIterations); // cfg.set_collisions(0x0001 + 0x0020 + 0x0040); // self collision - // cfg.set_collisions(0x11); // Soft-rigid, soft-soft + cfg.set_collisions(0x11); // Soft-rigid, soft-soft if (shape == SoftShape.Volume) { cfg.set_kDF(0.1); cfg.set_kDP(0.01); cfg.set_kPR(bend); } - - #elseif cpp - body.m_cfg.viterations = physics.solverIterations; - body.m_cfg.piterations = physics.solverIterations; + #else + //Not passed as refernece + var cfg = body.m_cfg; + cfg.viterations = physics.solverIterations; + cfg.piterations = physics.solverIterations; // body.m_cfg.collisions = 0x0001 + 0x0020 + 0x0040; + cfg.collisions = 0x11; // Soft-rigid, soft-soft if (shape == SoftShape.Volume) { - body.m_cfg.kDF = 0.1; - body.m_cfg.kDP = 0.01; - body.m_cfg.kPR = bend; + cfg.kDF = 0.1; + cfg.kDP = 0.01; + cfg.kPR = bend; } + //Set config again in HL + body.m_cfg = cfg; #end body.setTotalMass(mass, false); body.getCollisionShape().setMargin(margin); physics.world.addSoftBody(body, 1, -1); - body.setActivationState(bullet.Bt.CollisionObject.DISABLE_DEACTIVATION); + body.setActivationState(CollisionObjectActivationState.DISABLE_DEACTIVATION); + + #if hl + cfg.delete(); + #end + notifyOnRemove(removeFromWorld); notifyOnUpdate(update); } @@ -172,68 +254,128 @@ class SoftBody extends Trait { function update() { var mo = cast(object, MeshObject); var geom = mo.data.geom; - #if arm_deinterleaved - var v = geom.vertexBuffers[0].lock(); - var n = geom.vertexBuffers[1].lock(); + var v: ByteArray = geom.vertexBuffers[0].buffer.lock(); + var n: ByteArray = geom.vertexBuffers[1].buffer.lock(); #else - var v = geom.vertexBuffer.lock(); + var v:ByteArray = geom.vertexBuffer.lock(); var vbPos = geom.vertexBufferMap.get("pos"); var v2 = vbPos != null ? vbPos.lock() : null; // For shadows var l = geom.structLength; #end - var numVerts = Std.int(v.length / l); #if js var nodes = body.get_m_nodes(); - #elseif cpp + #else var nodes = body.m_nodes; #end + var numNodes = nodes.size(); + //Finding the mean position of vertices in world space + vertOffsetX = 0.0; + vertOffsetY = 0.0; + vertOffsetZ = 0.0; + for (i in 0...numNodes) { + var node = nodes.at(i); + #if js + var nodePos = node.get_m_x(); + #else + var nodePos = node.m_x; + #end + var mx = nodePos.x(); + var my = nodePos.y(); + var mz = nodePos.z(); + vertOffsetX += mx; + vertOffsetY += my; + vertOffsetZ += mz; + + #if hl + node.delete(); + nodePos.delete(); + #end + } + vertOffsetX /= numNodes; + vertOffsetY /= numNodes; + vertOffsetZ /= numNodes; + + //Setting the mean position as object local location + mo.transform.scale.set(1, 1, 1); + mo.transform.loc.set(vertOffsetX, vertOffsetY, vertOffsetZ); + mo.transform.rot.set(0, 0, 0, 1); + + //Checking maximum dimension for scalePos var scalePos = 1.0; - for (i in 0...numVerts) { + for (i in 0...numNodes) { var node = nodes.at(i); #if js var nodePos = node.get_m_x(); - #elseif cpp + #else var nodePos = node.m_x; #end - if (Math.abs(nodePos.x()) > scalePos) scalePos = Math.abs(nodePos.x()); - if (Math.abs(nodePos.y()) > scalePos) scalePos = Math.abs(nodePos.y()); - if (Math.abs(nodePos.z()) > scalePos) scalePos = Math.abs(nodePos.z()); + var mx = nodePos.x() - vertOffsetX; + var my = nodePos.y() - vertOffsetY; + var mz = nodePos.z() - vertOffsetZ; + if (Math.abs(mx * 2) > scalePos) scalePos = Math.abs(mx * 2); + if (Math.abs(my * 2) > scalePos) scalePos = Math.abs(my * 2); + if (Math.abs(mz * 2) > scalePos) scalePos = Math.abs(mz * 2); + + #if hl + node.delete(); + nodePos.delete(); + #end } + //Set scalePos and buildMatrix mo.data.scalePos = scalePos; mo.transform.scaleWorld = scalePos; mo.transform.buildMatrix(); - for (i in 0...numVerts) { + //Set vertices with location offset + for (i in 0...nodes.size()) { var node = nodes.at(i); + var indices = vertexIndexMap.get(i); #if js var nodePos = node.get_m_x(); var nodeNor = node.get_m_n(); - #elseif cpp + #else var nodePos = node.m_x; var nodeNor = node.m_n; #end - #if arm_deinterleaved - v.set(i * 4 , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.set(i * 4 + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.set(i * 4 + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - n.set(i * 2 , Std.int(nodeNor.x() * 32767)); - n.set(i * 2 + 1, Std.int(nodeNor.y() * 32767)); - v.set(i * 4 + 3, Std.int(nodeNor.z() * 32767)); - #else - v.set(i * l , Std.int(nodePos.x() * 32767 * (1 / scalePos))); - v.set(i * l + 1, Std.int(nodePos.y() * 32767 * (1 / scalePos))); - v.set(i * l + 2, Std.int(nodePos.z() * 32767 * (1 / scalePos))); - if (vbPos != null) { - v2.set(i * 4 , v.get(i * l )); - v2.set(i * 4 + 1, v.get(i * l + 1)); - v2.set(i * 4 + 2, v.get(i * l + 2)); + var mx = nodePos.x() - vertOffsetX; + var my = nodePos.y() - vertOffsetY; + var mz = nodePos.z() - vertOffsetZ; + + var nx = nodeNor.x(); + var ny = nodeNor.y(); + var nz = nodeNor.z(); + + for (idx in indices){ + #if arm_deinterleaved + v.setInt16(idx * 8 , Std.int(mx * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 2, Std.int(my * 32767 * (1 / scalePos))); + v.setInt16(idx * 8 + 4, Std.int(mz * 32767 * (1 / scalePos))); + n.setInt16(idx * 4 , Std.int(nx * 32767)); + n.setInt16(idx * 4 + 2, Std.int(ny * 32767)); + v.setInt16(idx * 8 + 6, Std.int(nz * 32767)); + #else + var vertIndex = idx * l * 2; + v.setInt16(vertIndex , Std.int(mx * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 2, Std.int(my * 32767 * (1 / scalePos))); + v.setInt16(vertIndex + 4, Std.int(mz * 32767 * (1 / scalePos))); + if (vbPos != null) { + v2.setInt16(idx * 8 , v.getInt16(vertIndex )); + v2.setInt16(idx * 8 + 2, v.getInt16(vertIndex + 2)); + v2.setInt16(idx * 8 + 4, v.getInt16(vertIndex + 4)); + } + v.setInt16(vertIndex + 6, Std.int(nx * 32767)); + v.setInt16(vertIndex + 8, Std.int(ny * 32767)); + v.setInt16(vertIndex + 10, Std.int(nz * 32767)); + #end } - v.set(i * l + 3, Std.int(nodeNor.z() * 32767)); - v.set(i * l + 4, Std.int(nodeNor.x() * 32767)); - v.set(i * l + 5, Std.int(nodeNor.y() * 32767)); + + #if hl + node.delete(); + nodePos.delete(); + nodeNor.delete(); #end } // for (i in 0...Std.int(geom.indices[0].length / 3)) { @@ -258,12 +400,24 @@ class SoftBody extends Trait { // v.set(c * l + 5, cb.z); // } #if arm_deinterleaved - geom.vertexBuffers[0].unlock(); - geom.vertexBuffers[1].unlock(); + geom.vertexBuffers[0].buffer.unlock(); + geom.vertexBuffers[1].buffer.unlock(); #else geom.vertexBuffer.unlock(); if (vbPos != null) vbPos.unlock(); #end + #if hl + nodes.delete(); + #end + } + + function removeFromWorld() { + physics.world.removeSoftBody(body); + #if js + bullet.Bt.Ammo.destroy(body); + #else + body.delete(); + #end } #end diff --git a/Sources/armory/ui/Canvas.hx b/Sources/armory/ui/Canvas.hx index c9950e96c2..6d9d0be022 100644 --- a/Sources/armory/ui/Canvas.hx +++ b/Sources/armory/ui/Canvas.hx @@ -16,6 +16,7 @@ class Canvas { public static var screenW = -1; public static var screenH = -1; public static var locale = "en"; + public static var imageScaleQuality = kha.graphics2.ImageScaleQuality.Low; static var _ui: Zui; static var h = new zui.Zui.Handle(); // TODO: needs one handle per canvas @@ -29,6 +30,7 @@ class Canvas { _ui = ui; g.end(); + g.imageScaleQuality = Canvas.imageScaleQuality; ui.begin(g); // Bake elements g.begin(false); diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 2035de0df3..b81b1afc1e 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -138,6 +138,9 @@ def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.S self.world_array = [] self.particle_system_array = {} + self.referenced_collections: list[bpy.types.Collection] = [] + """Collections referenced by collection instances""" + self.has_spawning_camera = False """Whether there is at least one camera in the scene that spawns by default""" @@ -588,6 +591,8 @@ def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Mat # Tilesheets elif bobject.arm_tilesheet != '': variant_suffix = '_armtile' + elif arm.utils.export_morph_targets(bobject): + variant_suffix = '_armskey' if variant_suffix == '': continue @@ -769,6 +774,7 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: out_object['group_ref'] = bobject.instance_collection.name + self.referenced_collections.append(bobject.instance_collection) if bobject.arm_tilesheet != '': out_object['tilesheet_ref'] = bobject.arm_tilesheet @@ -1577,6 +1583,7 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec if tris == 0: # No face assigned continue prim = np.empty(tris * 3, dtype=' 1: for i in range(len(mats)): # Multi-mat mesh if mats[i] == mats[index]: # Default material for empty slots @@ -2422,7 +2437,7 @@ def execute(self): if collection.name.startswith(('RigidBodyWorld', 'Trait|')): continue - if self.scene.user_of_id(collection) or collection.library: + if self.scene.user_of_id(collection) or collection.library or collection in self.referenced_collections: self.export_collection(collection) if not ArmoryExporter.option_mesh_only: @@ -2949,6 +2964,16 @@ def export_scene_traits(self) -> None: if rbw is not None and rbw.enabled: out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] + if phys_pkg == 'bullet': + debug_draw_mode = 1 if wrd.arm_bullet_dbg_draw_wireframe else 0 + debug_draw_mode |= 2 if wrd.arm_bullet_dbg_draw_aabb else 0 + debug_draw_mode |= 8 if wrd.arm_bullet_dbg_draw_contact_points else 0 + debug_draw_mode |= 2048 if wrd.arm_bullet_dbg_draw_constraints else 0 + debug_draw_mode |= 4096 if wrd.arm_bullet_dbg_draw_constraint_limits else 0 + debug_draw_mode |= 16384 if wrd.arm_bullet_dbg_draw_normals else 0 + debug_draw_mode |= 32768 if wrd.arm_bullet_dbg_draw_axis_gizmo else 0 + out_trait['parameters'].append(str(debug_draw_mode)) + self.output['traits'].append(out_trait) if wrd.arm_navigation != 'Disabled' and ArmoryExporter.export_navigation: diff --git a/blender/arm/exporter_opt.py b/blender/arm/exporter_opt.py index 3e762062fe..88f4ebd7a0 100644 --- a/blender/arm/exporter_opt.py +++ b/blender/arm/exporter_opt.py @@ -248,35 +248,57 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec cdata[i3 + 2] = v.col[2] # Indices + # Create dict for every material slot prims = {ma.name if ma else '': [] for ma in export_mesh.materials} + v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} if not prims: + # No materials prims = {'': []} + v_maps = {'': []} + # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list vert_dict = {i : v for v in vert_list for i in v.loop_indices} + # For each polygon in a mesh for poly in export_mesh.polygons: + # Index of the first loop of this polygon first = poly.loop_start + # No materials assigned if len(export_mesh.materials) == 0: + # Get prim prim = prims[''] + v_map = v_maps[''] else: + # First material mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] + # Get prim for this material prim = prims[mat.name if mat else ''] + v_map = v_maps[mat.name if mat else ''] + # List of indices for each loop_index belonging to this polygon indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] + # If 3 loops per polygon (Triangle?) if poly.loop_total == 3: prim += indices + v_map += v_indices + # If > 3 loops per polygon (Non-Triangular?) elif poly.loop_total > 3: for i in range(poly.loop_total-2): prim += (indices[-1], indices[i], indices[i + 1]) + v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) # Write indices o['index_arrays'] = [] for mat, prim in prims.items(): idata = [0] * len(prim) + v_map_data = [0] * len(prim) + v_map_sub = v_maps[mat] for i, v in enumerate(prim): idata[i] = v + v_map_data[i] = v_map_sub[i] if len(idata) == 0: # No face assigned continue - ia = {'values': idata, 'material': 0} + ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} # Find material index for multi-mat mesh if len(export_mesh.materials) > 1: for i in range(0, len(export_mesh.materials)): diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py index 056a526705..8743a9e770 100644 --- a/blender/arm/handlers.py +++ b/blender/arm/handlers.py @@ -89,6 +89,18 @@ def on_operator_post(operator_id: str) -> None: target_obj.arm_rb_ccd = source_obj.arm_rb_ccd target_obj.arm_rb_collision_filter_mask = source_obj.arm_rb_collision_filter_mask + elif operator_id == "NODE_OT_new_node_tree": + if bpy.context.space_data.tree_type == arm.nodes_logic.ArmLogicTree.bl_idname: + # In Blender 3.5+, new node trees are no longer called "NodeTree" + # but follow the bl_label attribute by default. New logic trees + # are thus called "Armory Logic Editor" which conflicts with Haxe's + # class naming convention. To avoid this, we listen for the + # creation of a node tree and then rename it. + # Unfortunately, manually naming the tree has the unfortunate + # side effect of not basing the new name on the name of the + # previously opened node tree, as it is the case for Blender trees... + bpy.context.space_data.edit_tree.name = "LogicTree" + def send_operator(op): if hasattr(bpy.context, 'object') and bpy.context.object is not None: @@ -162,6 +174,7 @@ def on_save_pre(context): @persistent def on_load_pre(context): unload_py_libraries() + log.clear(clear_warnings=True, clear_errors=True) @persistent diff --git a/blender/arm/logicnode/arm_node_group.py b/blender/arm/logicnode/arm_node_group.py index 0e8e27ddc2..51728ad7c1 100644 --- a/blender/arm/logicnode/arm_node_group.py +++ b/blender/arm/logicnode/arm_node_group.py @@ -549,7 +549,7 @@ def draw(self, context): row.operator('arm.edit_group_tree', icon='FULLSCREEN_ENTER', text='Edit Tree') -REG_CLASSES = ( +__REG_CLASSES = ( ArmGroupTree, ArmEditGroupTree, ArmCopyGroupTree, @@ -562,4 +562,4 @@ def draw(self, context): ArmAddCallGroupNode, ARM_PT_LogicGroupPanel ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 827694fc5f..0dbebaa918 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -1014,7 +1014,7 @@ def reset_globals(): category_items = OrderedDict() -REG_CLASSES = ( +__REG_CLASSES = ( ArmNodeSearch, ArmNodeAddInputButton, ArmNodeAddInputValueButton, @@ -1026,4 +1026,4 @@ def reset_globals(): ArmNodeRemoveInputOutputButton, ArmNodeCallFuncButton ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index a07cad871d..4604d11fb7 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -633,7 +633,7 @@ def draw_color(self, context): ArmAnySocketInterface = _make_socket_interface('ArmAnySocketInterface', 'ArmAnySocket') -REG_CLASSES = ( +__REG_CLASSES = ( ArmActionSocketInterface, ArmAnimSocketInterface, ArmRotationSocketInterface, @@ -662,4 +662,4 @@ def draw_color(self, context): ArmVectorSocket, ArmAnySocket, ) -register, unregister = bpy.utils.register_classes_factory(REG_CLASSES) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/logicnode/math/LN_math_expression.py b/blender/arm/logicnode/math/LN_math_expression.py index 0795d6a810..a301599912 100644 --- a/blender/arm/logicnode/math/LN_math_expression.py +++ b/blender/arm/logicnode/math/LN_math_expression.py @@ -5,185 +5,76 @@ class MathExpressionNode(ArmLogicTreeNode): """Mathematical operations on values.""" bl_idname = 'LNMathExpressionNode' bl_label = 'Math Expression' - arm_version = 1 - min_inputs = 2 - max_inputs = 10 + arm_version = 2 - @staticmethod - def get_variable_name(index): - return { - 0: 'a', - 1: 'b', - 2: 'c', - 3: 'd', - 4: 'e', - 5: 'x', - 6: 'y', - 7: 'h', - 8: 'i', - 9: 'k' - }.get(index, 'a') - - @staticmethod - def get_clear_exp(value): - return re.sub(r'[\-\+\*\/\(\)\^\%abcdexyhik0123456789. ]', '', value).strip() - - @staticmethod - def get_invalid_characters(value): - value = value.replace(' ', '') - len_v = len(value) - arg = ['a', 'b', 'c', 'd', 'e', 'x', 'y', 'h', 'i', 'k'] - for i in range(len_v): - s = value[i] - if s == '.': - if ((i - 1) < 0) or ((i + 1) >= len_v) or (not value[i - 1].isnumeric()) or (not value[i + 1].isnumeric()): - return False - oper = ['+', '-', '*', '/', '%', '^'] - if s == '(': - if (i > 0) and ((value[i - 1] not in oper) and (value[i - 1] != '(')): - return False - if (i < (len_v - 1)) and ((value[i + 1] not in arg) and (not value[i + 1].isnumeric()) and (value[i + 1] != '(')): - return False - if s == ')': - if (i > 0) and ((value[i - 1] not in arg) and (not value[i - 1].isnumeric()) and (value[i - 1] != ')')): - return False - if (i < (len_v - 1)) and (not value[i + 1].isnumeric()) and ((value[i + 1] not in oper) and (value[i + 1] != ')')): - return False - if s in oper: - if ((i > 0) and (value[i - 1] in oper)) or ((i < (len_v - 1)) and (value[i + 1] in oper)): - return False - last_sym = value[len_v - 1] - if (not last_sym.isnumeric()) and (last_sym not in arg) and (last_sym != ')'): - return False - return True - - @staticmethod - def check_variable(self, value): - variables = re.sub(r'[\-\+\*\/\(\)\^\%0123456789. ]', '', value).strip() - for vr in variables: - check = False - for inp_key in self.inputs.keys(): - if (vr == inp_key): - check = True - break - if not check: - return False - return True - - @staticmethod - def matches(line, opendelim='(', closedelim=')'): - stack = [] - for m in re.finditer(r'[{}{}]'.format(opendelim, closedelim), line): - pos = m.start() - if line[pos-1] == '\\': - # Skip escape sequence - continue - c = line[pos] - if c == opendelim: - stack.append(pos+1) - elif c == closedelim: - if len(stack) > 0: - prevpos = stack.pop() - yield (True, prevpos, pos, len(stack)) - else: - # Error - yield (False, 0, 0, 0) - pass - if len(stack) > 0: - for pos in stack: - yield (False, 0, 0, 0) - - @staticmethod - def isPartCorrect(s): - if len(s.replace('p', '').replace(' ', '').split()) == 0: - return True - REGEX = re.compile(r"(([abcdexyhikp]|\d+)[\+\-\/\*\^\%]){1,}([abcdexyhikp]|\d+)(=([abcdexyhikp]|\d+))?") - result = False - if REGEX.match(s): - result = True - return result + num_params: IntProperty(default=2, min=0) @staticmethod - def isCorrect(self, s): - result = True - if s.find("(") >=0 or s.find(")") >= 0: - for correct, openpos, closepos, level in self.matches(s): - if correct: - part = s[openpos:closepos] - if part.find("(") == -1 and part.find(")") == -1: - if not self.isPartCorrect(part): - result = False - break - part = s[openpos-1:closepos+1] - replaced = s.replace(part, "p") - if replaced.find("(") >=0 or replaced.find(")") >= 0: - if not self.isCorrect(self, replaced): - result = False - break - else: - if not self.isPartCorrect(replaced): - result = False - break - else: - result = False - break - else: - result = self.isPartCorrect(s) - return result - - def set_exp_error(self, value): - self['exp_error'] = value - - def get_exp_error(self): - return self.get('exp_error', False) + def get_variable_name(index): + return chr( range(ord('a'), ord('z')+1)[index] ) def set_exp(self, value): - value = value.lower() - self['property0'] = value - # Check errors - val_error = False - if len(self.get_clear_exp(value)) > 0: - val_error = True - elif not self.get_invalid_characters(value): - val_error = True - elif not self.check_variable(self, value): - val_error = True - elif not self.isCorrect(self, value.replace(' ', '')): - val_error = True - self.set_exp_error(val_error) + self['property2'] = value + # TODO: Check expression for errors + self['exp_error'] = False def get_exp(self): - return self.get('property0', 'a + b') + return self.get('property2', 'a + b') - property0: HaxeStringProperty('property0', name='', description='Expression (operation: +, -, *, /, ^, (, ), %)', set=set_exp, get=get_exp) - property1: HaxeBoolProperty('property1', name='Clamp', default=False) + property0: HaxeBoolProperty('property0', name='Clamp Result', default=False) + property1: HaxeIntProperty('property1', name='Number of Params', default=2) + property2: HaxeStringProperty('property2', name='', description='Math Expression: +, -, *, /, ^, %, (, ), log(a, b), ln(a), abs(a), max(a,b), min(a,b), sin(a), cos(a), tan(a), cot(a), asin(a), acos(a), atan(a), atan2(a,b), pi(), e()', set=set_exp, get=get_exp) def __init__(self): - array_nodes[str(id(self))] = self + super(MathExpressionNode, self).__init__() + self.register_id() + def arm_init(self, context): + + # OUTPUTS: + self.add_output('ArmFloatSocket', 'Result') + + # two default parameters at start self.add_input('ArmFloatSocket', self.get_variable_name(0), default_value=0.0) self.add_input('ArmFloatSocket', self.get_variable_name(1), default_value=0.0) - self.add_output('ArmFloatSocket', 'Result') + + def add_sockets(self): + if self.num_params < 26: + self.add_input('ArmFloatSocket', self.get_variable_name(self.num_params), default_value=0.0) + self.num_params += 1 + self['property1'] = self.num_params + + def remove_sockets(self): + if self.num_params > 0: + self.inputs.remove(self.inputs.values()[-1]) + self.num_params -= 1 + self['property1'] = self.num_params def draw_buttons(self, context, layout): - layout.prop(self, 'property1') - # Expression + # Clamp Property + layout.prop(self, 'property0') + + # Expression Property row = layout.row(align=True) column = row.column(align=True) - column.alert = self.get_exp_error() - column.prop(self, 'property0', icon='FORCE_HARMONIC') - # Buttons + # TODO: + #column.alert = self['exp_error'] + column.prop(self, 'property2', icon='FORCE_HARMONIC') + + # Button ADD parameter row = layout.row(align=True) column = row.column(align=True) - op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) - if len(self.inputs) == 10: + op = column.operator('arm.node_call_func', text='Add Param', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_sockets' + if self.num_params == 26: column.enabled = False - op.node_index = str(id(self)) - op.socket_type = 'ArmFloatSocket' - op.name_format = self.get_variable_name(len(self.inputs)) + + # Button REMOVE parameter column = row.column(align=True) - op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) - op.node_index = str(id(self)) - if len(self.inputs) == 2: + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'remove_sockets' + if self.num_params == 0: column.enabled = False diff --git a/blender/arm/logicnode/miscellaneous/LN_group_input.py b/blender/arm/logicnode/miscellaneous/LN_group_input.py index 018c3b35e6..89fec6b476 100644 --- a/blender/arm/logicnode/miscellaneous/LN_group_input.py +++ b/blender/arm/logicnode/miscellaneous/LN_group_input.py @@ -189,7 +189,7 @@ def draw_buttons_ext(self, context, layout): layout.enabled = False node = context.active_node split = layout.row() - split.template_list('ARM_UL_interface_sockets', 'OUT', node, 'outputs', node, 'active_output') + split.template_list('ARM_UL_InterfaceSockets', 'OUT', node, 'outputs', node, 'active_output') ops_col = split.column() add_remove_col = ops_col.column(align=True) props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") @@ -214,4 +214,4 @@ def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.arm_version not in (0, 1, 2): raise LookupError() - return node_tree.nodes.new('LNGroupInputsNode') \ No newline at end of file + return node_tree.nodes.new('LNGroupInputsNode') diff --git a/blender/arm/logicnode/miscellaneous/LN_group_output.py b/blender/arm/logicnode/miscellaneous/LN_group_output.py index a4fa9636d1..79b0a1720b 100644 --- a/blender/arm/logicnode/miscellaneous/LN_group_output.py +++ b/blender/arm/logicnode/miscellaneous/LN_group_output.py @@ -189,7 +189,7 @@ def draw_buttons_ext(self, context, layout): layout.enabled = False node = context.active_node split = layout.row() - split.template_list('ARM_UL_interface_sockets', 'IN', node, 'inputs', node, 'active_input') + split.template_list('ARM_UL_InterfaceSockets', 'IN', node, 'inputs', node, 'active_input') ops_col = split.column() add_remove_col = ops_col.column(align=True) props = add_remove_col.operator('arm.node_call_func', icon='ADD', text="") @@ -214,4 +214,4 @@ def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.arm_version not in (0, 1, 2): raise LookupError() - return node_tree.nodes.new('LNGroupOutputsNode') \ No newline at end of file + return node_tree.nodes.new('LNGroupOutputsNode') diff --git a/blender/arm/logicnode/object/LN_set_object_visible.py b/blender/arm/logicnode/object/LN_set_object_visible.py index 09107e9ec9..0bb93e04d0 100644 --- a/blender/arm/logicnode/object/LN_set_object_visible.py +++ b/blender/arm/logicnode/object/LN_set_object_visible.py @@ -3,11 +3,19 @@ class SetVisibleNode(ArmLogicTreeNode): """Sets whether the given object is visible. + @input Object: Object whose property to be set. + + @input Visible: Visibility. + + @input Children: Set the visibility of the children too. Visibility is set only to the immediate children. + + @input Recursive: If enabled, visibility of all the children in the tree is set. Ignored if `Children` is disabled. + @seeNode Get Object Visible""" bl_idname = 'LNSetVisibleNode' bl_label = 'Set Object Visible' arm_section = 'props' - arm_version = 1 + arm_version = 2 property0: HaxeEnumProperty( 'property0', @@ -22,8 +30,15 @@ def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') self.add_input('ArmBoolSocket', 'Visible') self.add_input('ArmBoolSocket', 'Children', default_value=True) + self.add_input('ArmBoolSocket', 'Recursive', default_value=False) self.add_output('ArmNodeSocketAction', 'Out') def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + 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/tree_variables.py b/blender/arm/logicnode/tree_variables.py index 42cab1ab84..8faaaca611 100644 --- a/blender/arm/logicnode/tree_variables.py +++ b/blender/arm/logicnode/tree_variables.py @@ -525,7 +525,7 @@ def node_compat_sdk2209(): arm.logicnode.arm_nodes.ArmLogicVariableNodeMixin.synchronize(tree, item.name) -REG_CLASSES = ( +__REG_CLASSES = ( ARM_PT_Variables, ARM_OT_TreeVariableListMoveItem, ARM_OT_TreeVariableMakeLocalNode, @@ -536,11 +536,11 @@ def node_compat_sdk2209(): ARM_UL_TreeVarList, ARM_PG_TreeVarListItem, ) -register_classes, unregister_classes = bpy.utils.register_classes_factory(REG_CLASSES) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): - register_classes() + __reg_classes() bpy.types.Node.arm_logic_id = StringProperty( name='ID', @@ -554,4 +554,4 @@ def register(): def unregister(): - unregister_classes() + __unreg_classes() diff --git a/blender/arm/make.py b/blender/arm/make.py index 626b6774d9..ad7efe1dba 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -208,8 +208,11 @@ def export_data(fp, sdk_path): if network_found == False: export_network = False - if wrd.arm_ui == 'Enabled': - export_ui = True + # Ugly workaround: some logic nodes require Zui code even if no UI is used, + # for now enable UI export unless explicitly disabled. + export_ui = True + if wrd.arm_ui == 'Disabled': + export_ui = False if wrd.arm_network == 'Enabled': export_network = True diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index 9010f120d1..6654cfd3f4 100644 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -308,6 +308,8 @@ def build(): assets.add_khafile_def('rp_ssr') assets.add_shader_pass('ssr_pass') assets.add_shader_pass('blur_adaptive_pass') + if rpdat.arm_ssr_half_res: + assets.add_khafile_def('rp_ssr_half') if rpdat.rp_ss_refraction: wrd.world_defs += '_SSRefraction' @@ -436,7 +438,7 @@ def build(): callback() -def get_num_gbuffer_rts_deferred() -> int: +def get_num_gbuffer_rts()-> int: """Return the number of render targets required for the G-Buffer.""" wrd = bpy.data.worlds['Arm'] diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 50e0807cb0..3751ce72cf 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -338,7 +338,7 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: 'GAMMA', 'HUE_SAT', 'INVERT', - 'MIX_RGB', + 'MIX', 'BLACKBODY', 'VALTORGB', 'CURVE_VEC', @@ -465,6 +465,7 @@ def parse_value(node, socket): 'CLAMP', 'VALTORGB', 'MATH', + 'MIX', 'RGBTOBW', 'SEPARATE_COLOR', 'SEPHSV', @@ -538,7 +539,10 @@ def is_parsed(node_store_name: str): def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: """Return the name of the variable that stores the parsed result from the given node and socket.""" - return node_name(node.name) + '_' + safesrc(socket.name) + '_res' + name = node_name(node.name) + '_' + safesrc(socket.name) + '_res' + if '__' in name: # Consecutive _ are reserved + name = name.replace('_', '_x') + return name def write_result(link: bpy.types.NodeLink) -> Optional[str]: @@ -600,8 +604,12 @@ def to_uniform(inp: bpy.types.NodeSocket): state.curshader.add_uniform(glsl_type(inp.type) + ' ' + uname) return uname -def store_var_name(node: bpy.types.Node): - return node_name(node.name) + '_store' + +def store_var_name(node: bpy.types.Node) -> str: + name = node_name(node.name) + if name[-1] == "_": + return name + '_x_store' # Prevent consecutive __ + return name + '_store' def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_arm_mat_param=None): @@ -772,7 +780,7 @@ def node_name(s: str) -> str: if state.curshader.write_textures > 0: s += '_texread' s = safesrc(s) - if '__' in s: # Consecutive _ are reserved + if '__' in s: # Consecutive _ are reserved s = s.replace('_', '_x') return s diff --git a/blender/arm/material/cycles_nodes/nodes_color.py b/blender/arm/material/cycles_nodes/nodes_color.py index bb29506cb1..35c5ecd95b 100644 --- a/blender/arm/material/cycles_nodes/nodes_color.py +++ b/blender/arm/material/cycles_nodes/nodes_color.py @@ -54,9 +54,47 @@ def parse_invert(node: bpy.types.ShaderNodeInvert, out_socket: bpy.types.NodeSoc return f'mix({out_col}, vec3(1.0) - ({out_col}), {fac})' -def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: - col1 = c.parse_vector_input(node.inputs[1]) - col2 = c.parse_vector_input(node.inputs[2]) +def parse_mix(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> str: + if node.data_type == 'FLOAT': + return _parse_mixfloat(node, out_socket, state) + elif node.data_type == 'VECTOR': + return _parse_mixvec(node, out_socket, state) + elif node.data_type == 'RGBA': + return _parse_mixrgb(node, out_socket, state) + else: + log.warn(f'Mix node: unsupported data type {node.data_type}.') + return '0.0' + + +def _parse_mixfloat(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: + fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + + return f'mix({c.parse_value_input(node.inputs[2])}, {c.parse_value_input(node.inputs[3])}, {fac})' + + +def _parse_mixvec(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + if node.factor_mode == 'UNIFORM': + fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + + elif node.factor_mode == 'NON_UNIFORM': + fac = c.parse_vector_input(node.inputs[1]) + if node.clamp_factor: + fac = f'clamp({fac}, vec3(0.0), vec3(1.0))' + + else: + log.warn(f'Mix node: unsupported factor mode {node.factor_mode}.') + return 'vec3(0.0, 0.0, 0.0)' + + return f'mix({c.parse_vector_input(node.inputs[4])}, {c.parse_vector_input(node.inputs[5])}, {fac})' + + +def _parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + col1 = c.parse_vector_input(node.inputs[6]) + col2 = c.parse_vector_input(node.inputs[7]) # Store factor in variable for linked factor input if node.inputs[0].is_linked: @@ -65,6 +103,9 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc else: fac = c.parse_value_input(node.inputs[0]) + if node.clamp_factor: + fac = f'clamp({fac}, 0.0, 1.0)' + # TODO: Do not mix if factor is constant 0.0 or 1.0? blend = node.blend_type @@ -101,7 +142,7 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc elif blend == 'COLOR': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'SOFT_LIGHT': - out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))));'.format(col1, col2, fac) + out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))))'.format(col1, col2, fac) elif blend == 'LINEAR_LIGHT': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix # out_col = '({0} + {2} * (2.0 * ({1} - vec3(0.5))))'.format(col1, col2, fac_var) @@ -109,7 +150,7 @@ def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSoc log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') return col1 - if node.use_clamp: + if node.clamp_result: return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col) return out_col diff --git a/blender/arm/material/cycles_nodes/nodes_converter.py b/blender/arm/material/cycles_nodes/nodes_converter.py index 044834ef5f..8c350c5caa 100644 --- a/blender/arm/material/cycles_nodes/nodes_converter.py +++ b/blender/arm/material/cycles_nodes/nodes_converter.py @@ -32,25 +32,30 @@ def parse_maprange(node: bpy.types.ShaderNodeMapRange, out_socket: bpy.types.Nod toMax = c.parse_value_input(node.inputs[4]) if interp == "LINEAR": - state.curshader.add_function(c_functions.str_map_range_linear) - return f'map_range_linear({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_linear({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' elif interp == "STEPPED": - steps = float(c.parse_value_input(node.inputs[5])) state.curshader.add_function(c_functions.str_map_range_stepped) - return f'map_range_stepped({value}, {fromMin}, {fromMax}, {toMin}, {toMax}, {steps})' + out = f'map_range_stepped({value}, {fromMin}, {fromMax}, {toMin}, {toMax}, {steps})' elif interp == "SMOOTHSTEP": - state.curshader.add_function(c_functions.str_map_range_smoothstep) - return f'map_range_smoothstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_smoothstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' elif interp == "SMOOTHERSTEP": - state.curshader.add_function(c_functions.str_map_range_smootherstep) - return f'map_range_smootherstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + out = f'map_range_smootherstep({value}, {fromMin}, {fromMax}, {toMin}, {toMax})' + + else: + log.warn(f'Interpolation mode {interp} not supported for Map Range node') + return '0.0' + + if node.clamp: + out = f'clamp({out}, {toMin}, {toMax})' + + return out def parse_blackbody(node: bpy.types.ShaderNodeBlackbody, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index 16f36294d0..4b95449533 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -106,11 +106,10 @@ def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: N # clearcoar_normal = c.parse_vector_input(node.inputs[21]) # tangent = c.parse_vector_input(node.inputs[22]) if state.parse_opacity: - state.out_opacity = c.parse_value_input(node.inputs[21]) - if len(node.inputs) >= 21: #do we need to test that ? + state.out_rior = c.parse_value_input(node.inputs[16]) + if len(node.inputs) >= 21: state.out_opacity = c.parse_value_input(node.inputs[21]) - def parse_bsdfdiffuse(node: bpy.types.ShaderNodeBsdfDiffuse, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: c.write_normal(node.inputs[2]) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 494e5af4cd..5111427261 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -260,6 +260,7 @@ def make_deferred(con_mesh, rpasses): frag.write('vec2 posa = (wvpposition.xy / wvpposition.w) * 0.5 + 0.5;') frag.write('vec2 posb = (prevwvpposition.xy / prevwvpposition.w) * 0.5 + 0.5;') frag.write('fragColor[GBUF_IDX_2].rg = vec2(posa - posb);') + frag.write('fragColor[GBUF_IDX_2].b = 0.0;') if mat_state.material.arm_ignore_irradiance: frag.write('fragColor[GBUF_IDX_2].b = 1.0;') @@ -535,25 +536,21 @@ def make_forward(con_mesh): frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) if not blend: - mrt = 1 - if rpdat.rp_ssr: - mrt += 1 - if rpdat.rp_ss_refraction: - mrt += 1 - if mrt > 1: + mrt = rpdat.rp_ssr or rpdat.rp_ss_refraction + if mrt: # Store light gbuffer for post-processing - frag.add_out('vec4 fragColor[{0}]'.format(mrt)) + frag.add_out('vec4 fragColor[GBUF_SIZE]') frag.add_include('std/gbuffer.glsl') frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') - frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') - frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') - - if mrt > 2: + frag.write('fragColor[GBUF_IDX_0] = vec4(direct + indirect, packFloat2(occlusion, specular));') + frag.write('fragColor[GBUF_IDX_1] = vec4(n.xy, roughness, metallic);') + if '_SSRefraction' in wrd.world_defs: if parse_opacity: - frag.write('fragColor[{0}] = vec4(rior, opacity, 0.0, 0.0);'.format(mrt-1)) + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(rior, opacity, 0.0, 0.0);') else: - frag.write('fragColor[{0}] = vec4(1.0, 1.0, 0.0, 0.0);'.format(mrt-1)) + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 0.0);') + else: frag.add_out('vec4 fragColor[1]') frag.write('fragColor[0] = vec4(direct + indirect, 1.0);') diff --git a/blender/arm/material/mat_batch.py b/blender/arm/material/mat_batch.py index 3d2746c31b..862a189628 100644 --- a/blender/arm/material/mat_batch.py +++ b/blender/arm/material/mat_batch.py @@ -1,12 +1,15 @@ +import bpy import arm import arm.material.cycles as cycles import arm.material.make_shader as make_shader import arm.material.mat_state as mat_state +import arm.utils as arm_utils if arm.is_reload(__name__): cycles = arm.reload_module(cycles) make_shader = arm.reload_module(make_shader) mat_state = arm.reload_module(mat_state) + arm_utils = arm.reload_module(arm_utils) else: arm.enable_reload(__name__) @@ -25,7 +28,7 @@ def traverse_tree(node, sign): sign += 'o' # Unconnected socket return sign -def get_signature(mat): +def get_signature(mat, object: bpy.types.Object): nodes = mat.node_tree.nodes output_node = cycles.node_by_type(nodes, 'OUTPUT_MATERIAL') @@ -54,6 +57,8 @@ def get_signature(mat): sign += mat.arm_skip_context if mat.arm_skip_context != '' else '0' sign += '1' if mat.arm_particle_fade else '0' sign += mat.arm_billboard + sign += '_skin' if arm_utils.export_bone_data(object) else '0' + sign += '_morph' if arm_utils.export_morph_targets(object) else '0' return sign def traverse_tree2(node, ar): @@ -105,7 +110,7 @@ def build(materialArray, mat_users, mat_armusers): # Update signatures for mat in materialArray: if mat.signature == '' or not mat.arm_cached: - mat.signature = get_signature(mat) + mat.signature = get_signature(mat, mat_users[mat][0]) # Group signatures if mat.signature in signatureDict: signatureDict[mat.signature].append(mat) diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py index 6df03f2e67..c1ce9be998 100644 --- a/blender/arm/material/mat_utils.py +++ b/blender/arm/material/mat_utils.py @@ -47,7 +47,7 @@ def get_rpasses(material): else: ar.append('mesh') for con in add_mesh_contexts: - ar.append(con) + ar.append(con) if is_transluc(material) and not material.arm_discard and rpdat.rp_translucency_state != 'Off' and not material.arm_blending: ar.append('translucent') if rpdat.rp_voxelao and has_voxels: diff --git a/blender/arm/material/node_meta.py b/blender/arm/material/node_meta.py index 4860df4f62..623b2abe4e 100644 --- a/blender/arm/material/node_meta.py +++ b/blender/arm/material/node_meta.py @@ -70,7 +70,7 @@ class MaterialNodeMeta: 'HUE_SAT': MaterialNodeMeta(parse_func=nodes_color.parse_huesat), 'INVERT': MaterialNodeMeta(parse_func=nodes_color.parse_invert), 'LIGHT_FALLOFF': MaterialNodeMeta(parse_func=nodes_color.parse_lightfalloff), - 'MIX_RGB': MaterialNodeMeta(parse_func=nodes_color.parse_mixrgb), + 'MIX': MaterialNodeMeta(parse_func=nodes_color.parse_mix), # --- nodes_converter 'BLACKBODY': MaterialNodeMeta(parse_func=nodes_converter.parse_blackbody), diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 3e24b70444..998469e957 100644 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -33,7 +33,7 @@ class ArmLogicTree(bpy.types.NodeTree): """Logic nodes""" bl_idname = 'ArmLogicTreeType' bl_label = 'Logic Node Editor' - bl_icon = 'DECORATE' + bl_icon = 'NODETREE' def update(self): pass @@ -311,7 +311,7 @@ def execute(self, context): def poll(cls, context): return context.space_data is not None and context.space_data.type == 'NODE_EDITOR' -class ARM_UL_interface_sockets(bpy.types.UIList): +class ARM_UL_InterfaceSockets(bpy.types.UIList): """UI List of input and output sockets""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): socket = item @@ -356,26 +356,32 @@ def unregister_draw(cls): bpy.types.SpaceNodeEditor.draw_handler_remove(cls.draw_handler, 'WINDOW') cls.draw_handler = None + +__REG_CLASSES = ( + ArmLogicTree, + ArmOpenNodeHaxeSource, + ArmOpenNodePythonSource, + ArmOpenNodeWikiEntry, + ARM_OT_ReplaceNodesOperator, + ARM_MT_NodeAddOverride, + ARM_OT_AddNodeOverride, + ARM_UL_InterfaceSockets, + ARM_PT_LogicNodePanel, + ARM_PT_NodeDevelopment +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): arm.logicnode.arm_nodes.register() arm.logicnode.arm_sockets.register() arm.logicnode.arm_node_group.register() + arm.logicnode.tree_variables.register() - bpy.utils.register_class(ArmLogicTree) - bpy.utils.register_class(ArmOpenNodeHaxeSource) - bpy.utils.register_class(ArmOpenNodePythonSource) - bpy.utils.register_class(ArmOpenNodeWikiEntry) - bpy.utils.register_class(ARM_OT_ReplaceNodesOperator) ARM_MT_NodeAddOverride.overridden_menu = bpy.types.NODE_MT_add ARM_MT_NodeAddOverride.overridden_draw = bpy.types.NODE_MT_add.draw - bpy.utils.register_class(ARM_MT_NodeAddOverride) - bpy.utils.register_class(ARM_OT_AddNodeOverride) - bpy.utils.register_class(ARM_UL_interface_sockets) - # Register panels in correct order - bpy.utils.register_class(ARM_PT_LogicNodePanel) - arm.logicnode.tree_variables.register() - bpy.utils.register_class(ARM_PT_NodeDevelopment) + __reg_classes() arm.logicnode.init_categories() DrawNodeBreadCrumbs.register_draw() @@ -388,20 +394,10 @@ def unregister(): # Ensure that globals are reset if the addon is enabled again in the same Blender session arm_nodes.reset_globals() - bpy.utils.unregister_class(ARM_PT_NodeDevelopment) - arm.logicnode.tree_variables.unregister() - bpy.utils.unregister_class(ARM_PT_LogicNodePanel) - - bpy.utils.unregister_class(ARM_OT_ReplaceNodesOperator) - bpy.utils.unregister_class(ArmLogicTree) - bpy.utils.unregister_class(ArmOpenNodeHaxeSource) - bpy.utils.unregister_class(ArmOpenNodePythonSource) - bpy.utils.unregister_class(ArmOpenNodeWikiEntry) - bpy.utils.unregister_class(ARM_OT_AddNodeOverride) - bpy.utils.unregister_class(ARM_MT_NodeAddOverride) + __unreg_classes() bpy.utils.register_class(ARM_MT_NodeAddOverride.overridden_menu) - bpy.utils.unregister_class(ARM_UL_interface_sockets) + arm.logicnode.tree_variables.unregister() arm.logicnode.arm_node_group.unregister() arm.logicnode.arm_sockets.unregister() arm.logicnode.arm_nodes.unregister() diff --git a/blender/arm/props.py b/blender/arm/props.py index 43f52a0206..30ebdbdeda 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.5' +arm_version = '2023.9' arm_commit = '$Id$' def get_project_html5_copy(self): @@ -197,6 +197,40 @@ def init_properties(): items=[('Bullet', 'Bullet', 'Bullet'), ('Oimo', 'Oimo', 'Oimo')], name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) + bpy.types.World.arm_bullet_dbg_draw_wireframe = BoolProperty( + name="Collider Wireframes", default=False, + description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations" + ) + bpy.types.World.arm_bullet_dbg_draw_aabb = BoolProperty( + name="Axis-aligned Minimum Bounding Boxes", default=False, + description="Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes" + ) + bpy.types.World.arm_bullet_dbg_draw_contact_points = BoolProperty( + name="Contact Points", default=False, + description="Visualize contact points of multiple colliders" + ) + bpy.types.World.arm_bullet_dbg_draw_constraints = BoolProperty( + name="Constraints", default=False, + description="Draw axis gizmos for important constraint points" + ) + bpy.types.World.arm_bullet_dbg_draw_constraint_limits = BoolProperty( + name="Constraint Limits", default=False, + description="Draw additional constraint information such as distance or angle limits" + ) + bpy.types.World.arm_bullet_dbg_draw_normals = BoolProperty( + name="Face Normals", default=False, + description=( + "Draw the normal vectors of the triangles of the physics collider meshes." + " This only works for mesh collision shapes" + ) + ) + bpy.types.World.arm_bullet_dbg_draw_axis_gizmo = BoolProperty( + name="Axis Gizmos", default=False, + description=( + "Draw a small axis gizmo at the origin of the collision shape." + " Only works if \"Collider Wireframes\" is enabled as well" + ) + ) bpy.types.World.arm_navigation = EnumProperty( items=[('Disabled', 'Disabled', 'Disabled'), ('Auto', 'Auto', 'Auto'), @@ -220,6 +254,15 @@ def init_properties(): ('Enabled', 'Enabled', 'Enabled')], name="Audio", default='Enabled', update=assets.invalidate_compiler_cache) bpy.types.World.arm_khafile = PointerProperty(name="Append Khafile", description="Source appended to the project's khafile.js after it is generated", update=assets.invalidate_compiler_cache, type=bpy.types.Text) + bpy.types.World.arm_canvas_img_scaling_quality = EnumProperty( + name='Canvas Image Quality', + description='The quality with which to scale images drawn to Kha canvases', + items=[ + ('low', 'Low', 'Low quality. Scaling usually takes place using a point filter.'), + ('high', 'High', 'High quality. Scaling usually takes place using a bilinear filter.'), + ], + default='low' + ) bpy.types.World.arm_texture_quality = FloatProperty(name="Texture Quality", default=1.0, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_sound_quality = FloatProperty(name="Sound Quality", default=0.9, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_copy_override = BoolProperty(name="Copy Override", description="Overrides any existing files when copying", default=False, update=assets.invalidate_compiled_data) diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py index ca26aa3aee..9c71ed2fe5 100644 --- a/blender/arm/props_bake.py +++ b/blender/arm/props_bake.py @@ -350,52 +350,57 @@ def execute(self, context): bpy.data.materials.remove(mat, do_unlink=True) return{'FINISHED'} + +__REG_CLASSES = ( + ArmBakeListItem, + ARM_UL_BakeList, + ArmBakeListNewItem, + ArmBakeListDeleteItem, + ArmBakeListMoveItem, + ArmBakeButton, + ArmBakeApplyButton, + ArmBakeSpecialsMenu, + ArmBakeAddAllButton, + ArmBakeAddSelectedButton, + ArmBakeClearAllButton, + ArmBakeRemoveBakedMaterialsButton +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmBakeListItem) - bpy.utils.register_class(ARM_UL_BakeList) - bpy.utils.register_class(ArmBakeListNewItem) - bpy.utils.register_class(ArmBakeListDeleteItem) - bpy.utils.register_class(ArmBakeListMoveItem) - bpy.utils.register_class(ArmBakeButton) - bpy.utils.register_class(ArmBakeApplyButton) - bpy.utils.register_class(ArmBakeSpecialsMenu) - bpy.utils.register_class(ArmBakeAddAllButton) - bpy.utils.register_class(ArmBakeAddSelectedButton) - bpy.utils.register_class(ArmBakeClearAllButton) - bpy.utils.register_class(ArmBakeRemoveBakedMaterialsButton) - bpy.types.Scene.arm_bakelist_scale = FloatProperty(name="Resolution", description="Resolution scale", default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0, subtype='PERCENTAGE') + __reg_classes() + + bpy.types.Scene.arm_bakelist_scale = FloatProperty( + name="Resolution", description="Resolution scale", subtype='PERCENTAGE', + default=100.0, min=1, max=1000, soft_min=1, soft_max=100.0 + ) bpy.types.Scene.arm_bakelist = CollectionProperty(type=ArmBakeListItem) bpy.types.Scene.arm_bakelist_index = IntProperty(name="Index for my_list", default=0) bpy.types.Scene.arm_bakelist_unwrap = EnumProperty( - items = [('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), - ('Smart UV Project', 'Smart UV Project', 'Smart UV Project')], - name = "UV Unwrap", default='Smart UV Project') - - - #Register lightmapper + name="UV Unwrap", default='Smart UV Project', + items=[ + ('Lightmap Pack', 'Lightmap Pack', 'Lightmap Pack'), + ('Smart UV Project', 'Smart UV Project', 'Smart UV Project') + ] + ) + + # Register lightmapper bpy.types.Scene.arm_bakemode = EnumProperty( - items = [('Static Map', 'Static Map', 'Static Map'), - ('Lightmap', 'Lightmap', 'Lightmap')], - name = "Bake mode", default='Static Map') + name="Bake mode", default='Static Map', + items=[ + ('Static Map', 'Static Map', 'Static Map'), + ('Lightmap', 'Lightmap', 'Lightmap') + ] + ) operators.register() properties.register() + def unregister(): - bpy.utils.unregister_class(ArmBakeListItem) - bpy.utils.unregister_class(ARM_UL_BakeList) - bpy.utils.unregister_class(ArmBakeListNewItem) - bpy.utils.unregister_class(ArmBakeListDeleteItem) - bpy.utils.unregister_class(ArmBakeListMoveItem) - bpy.utils.unregister_class(ArmBakeButton) - bpy.utils.unregister_class(ArmBakeApplyButton) - bpy.utils.unregister_class(ArmBakeSpecialsMenu) - bpy.utils.unregister_class(ArmBakeAddAllButton) - bpy.utils.unregister_class(ArmBakeAddSelectedButton) - bpy.utils.unregister_class(ArmBakeClearAllButton) - bpy.utils.unregister_class(ArmBakeRemoveBakedMaterialsButton) - - #Unregister lightmapper + __unreg_classes() + # Unregister lightmapper operators.unregister() - properties.unregister() \ No newline at end of file + properties.unregister() diff --git a/blender/arm/props_exporter.py b/blender/arm/props_exporter.py index f8620a8952..9384176090 100644 --- a/blender/arm/props_exporter.py +++ b/blender/arm/props_exporter.py @@ -451,7 +451,7 @@ def execute(self, context): return{'FINISHED'} -REG_CLASSES = ( +__REG_CLASSES = ( ArmExporterListItem, ArmExporterAndroidPermissionListItem, ArmExporterAndroidAbiListItem, @@ -470,11 +470,11 @@ def execute(self, context): ArmoryExporterOpenFolderButton, ARM_OT_ExporterOpenVS ) -_reg_classes, _unreg_classes = bpy.utils.register_classes_factory(REG_CLASSES) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): - _reg_classes() + __reg_classes() bpy.types.World.arm_exporterlist = CollectionProperty(type=ArmExporterListItem) bpy.types.World.arm_exporterlist_index = IntProperty(name="Index for my_list", default=0) @@ -485,4 +485,4 @@ def register(): def unregister(): - _unreg_classes() + __unreg_classes() diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index 2ad67bf2f0..66da8cc4d9 100644 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -138,20 +138,20 @@ def execute(self, context): return{'CANCELLED'} return{'FINISHED'} + +__REG_CLASSES = ( + ArmLodListItem, + ARM_UL_LodList, + ArmLodListNewItem, + ArmLodListDeleteItem, + ArmLodListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmLodListItem) - bpy.utils.register_class(ARM_UL_LodList) - bpy.utils.register_class(ArmLodListNewItem) - bpy.utils.register_class(ArmLodListDeleteItem) - bpy.utils.register_class(ArmLodListMoveItem) + __reg_classes() bpy.types.Mesh.arm_lodlist = CollectionProperty(type=ArmLodListItem) bpy.types.Mesh.arm_lodlist_index = IntProperty(name="Index for my_list", default=0) bpy.types.Mesh.arm_lod_material = BoolProperty(name="Material Lod", description="Use materials of lod objects", default=False) - -def unregister(): - bpy.utils.unregister_class(ArmLodListItem) - bpy.utils.unregister_class(ARM_UL_LodList) - bpy.utils.unregister_class(ArmLodListNewItem) - bpy.utils.unregister_class(ArmLodListDeleteItem) - bpy.utils.unregister_class(ArmLodListMoveItem) diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py index 510e10eb5f..b7fb24ed20 100644 --- a/blender/arm/props_properties.py +++ b/blender/arm/props_properties.py @@ -141,18 +141,18 @@ def draw_properties(layout, obj): op = col.operator("arm_propertylist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' + +__REG_CLASSES = ( + ArmPropertyListItem, + ARM_UL_PropertyList, + ArmPropertyListNewItem, + ArmPropertyListDeleteItem, + ArmPropertyListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmPropertyListItem) - bpy.utils.register_class(ARM_UL_PropertyList) - bpy.utils.register_class(ArmPropertyListNewItem) - bpy.utils.register_class(ArmPropertyListDeleteItem) - bpy.utils.register_class(ArmPropertyListMoveItem) + __reg_classes() bpy.types.Object.arm_propertylist = CollectionProperty(type=ArmPropertyListItem) bpy.types.Object.arm_propertylist_index = IntProperty(name="Index for arm_propertylist", default=0) - -def unregister(): - bpy.utils.unregister_class(ArmPropertyListItem) - bpy.utils.unregister_class(ARM_UL_PropertyList) - bpy.utils.unregister_class(ArmPropertyListNewItem) - bpy.utils.unregister_class(ArmPropertyListDeleteItem) - bpy.utils.unregister_class(ArmPropertyListMoveItem) diff --git a/blender/arm/props_renderpath.py b/blender/arm/props_renderpath.py index c6c4d6685e..17a7703cef 100644 --- a/blender/arm/props_renderpath.py +++ b/blender/arm/props_renderpath.py @@ -561,7 +561,7 @@ class ArmRPListItem(bpy.types.PropertyGroup): arm_ssr_search_dist: FloatProperty(name="Search", default=5.0, update=assets.invalidate_shader_cache) arm_ssr_falloff_exp: FloatProperty(name="Falloff", default=5.0, update=assets.invalidate_shader_cache) arm_ssr_jitter: FloatProperty(name="Jitter", default=0.6, update=assets.invalidate_shader_cache) - arm_ss_refraction_ray_step: FloatProperty(name="Step", default=0.25, update=assets.invalidate_shader_cache) + arm_ss_refraction_ray_step: FloatProperty(name="Step", default=0.05, update=assets.invalidate_shader_cache) arm_ss_refraction_search_dist: FloatProperty(name="Search", default=5.0, update=assets.invalidate_shader_cache) arm_ss_refraction_falloff_exp: FloatProperty(name="Falloff", default=5.0, update=assets.invalidate_shader_cache) arm_ss_refraction_jitter: FloatProperty(name="Jitter", default=0.6, update=assets.invalidate_shader_cache) @@ -743,21 +743,19 @@ def execute(self, context): return{'FINISHED'} +__REG_CLASSES = ( + ArmRPListItem, + ARM_UL_RPList, + ArmRPListNewItem, + ArmRPListDeleteItem, + ArmRPListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmRPListItem) - bpy.utils.register_class(ARM_UL_RPList) - bpy.utils.register_class(ArmRPListNewItem) - bpy.utils.register_class(ArmRPListDeleteItem) - bpy.utils.register_class(ArmRPListMoveItem) + __reg_classes() bpy.types.World.arm_rplist = CollectionProperty(type=ArmRPListItem) bpy.types.World.rp_driver_list = CollectionProperty(type=bpy.types.PropertyGroup) bpy.types.World.arm_rplist_index = IntProperty(name="Index for my_list", default=0, update=update_renderpath) - - -def unregister(): - bpy.utils.unregister_class(ArmRPListItem) - bpy.utils.unregister_class(ARM_UL_RPList) - bpy.utils.unregister_class(ArmRPListNewItem) - bpy.utils.unregister_class(ArmRPListDeleteItem) - bpy.utils.unregister_class(ArmRPListMoveItem) diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py index 1f7d53b6db..d625087d3b 100644 --- a/blender/arm/props_tilesheet.py +++ b/blender/arm/props_tilesheet.py @@ -252,31 +252,25 @@ def execute(self, context): return{'CANCELLED'} return{'FINISHED'} + +__REG_CLASSES = ( + ArmTilesheetActionListItem, + ARM_UL_TilesheetActionList, + ArmTilesheetActionListNewItem, + ArmTilesheetActionListDeleteItem, + ArmTilesheetActionListMoveItem, + + ArmTilesheetListItem, + ARM_UL_TilesheetList, + ArmTilesheetListNewItem, + ArmTilesheetListDeleteItem, + ArmTilesheetListMoveItem, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmTilesheetActionListItem) - bpy.utils.register_class(ARM_UL_TilesheetActionList) - bpy.utils.register_class(ArmTilesheetActionListNewItem) - bpy.utils.register_class(ArmTilesheetActionListDeleteItem) - bpy.utils.register_class(ArmTilesheetActionListMoveItem) - - bpy.utils.register_class(ArmTilesheetListItem) - bpy.utils.register_class(ARM_UL_TilesheetList) - bpy.utils.register_class(ArmTilesheetListNewItem) - bpy.utils.register_class(ArmTilesheetListDeleteItem) - bpy.utils.register_class(ArmTilesheetListMoveItem) + __reg_classes() bpy.types.World.arm_tilesheetlist = CollectionProperty(type=ArmTilesheetListItem) bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for arm_tilesheetlist", default=0) - -def unregister(): - bpy.utils.unregister_class(ArmTilesheetListItem) - bpy.utils.unregister_class(ARM_UL_TilesheetList) - bpy.utils.unregister_class(ArmTilesheetListNewItem) - bpy.utils.unregister_class(ArmTilesheetListDeleteItem) - bpy.utils.unregister_class(ArmTilesheetListMoveItem) - - bpy.utils.unregister_class(ArmTilesheetActionListItem) - bpy.utils.unregister_class(ARM_UL_TilesheetActionList) - bpy.utils.unregister_class(ArmTilesheetActionListNewItem) - bpy.utils.unregister_class(ArmTilesheetActionListDeleteItem) - bpy.utils.unregister_class(ArmTilesheetActionListMoveItem) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 566ca5a455..de91282963 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -954,59 +954,40 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b row = layout.row() row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + +__REG_CLASSES = ( + ArmTraitListItem, + ARM_UL_TraitList, + ArmTraitListNewItem, + ArmTraitListDeleteItem, + ArmTraitListDeleteItemScene, + ArmTraitListMoveItem, + ArmEditScriptButton, + ArmEditBundledScriptButton, + ArmEditWasmScriptButton, + ArmoryGenerateNavmeshButton, + ArmEditCanvasButton, + ArmNewScriptDialog, + ArmNewTreeNodeDialog, + ArmEditTreeNodeDialog, + ArmGetTreeNodeDialog, + ArmNewCanvasDialog, + ArmNewWasmButton, + ArmRefreshScriptsButton, + ArmRefreshObjectScriptsButton, + ArmRefreshCanvasListButton, + ARM_PT_TraitPanel, + ARM_PT_SceneTraitPanel, + ARM_OT_CopyTraitsFromActive, + ARM_MT_context_menu, +) +__reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ArmTraitListItem) - bpy.utils.register_class(ARM_UL_TraitList) - bpy.utils.register_class(ArmTraitListNewItem) - bpy.utils.register_class(ArmTraitListDeleteItem) - bpy.utils.register_class(ArmTraitListDeleteItemScene) - bpy.utils.register_class(ArmTraitListMoveItem) - bpy.utils.register_class(ArmEditScriptButton) - bpy.utils.register_class(ArmEditBundledScriptButton) - bpy.utils.register_class(ArmEditWasmScriptButton) - bpy.utils.register_class(ArmoryGenerateNavmeshButton) - bpy.utils.register_class(ArmEditCanvasButton) - bpy.utils.register_class(ArmNewScriptDialog) - bpy.utils.register_class(ArmNewTreeNodeDialog) - bpy.utils.register_class(ArmEditTreeNodeDialog) - bpy.utils.register_class(ArmGetTreeNodeDialog) - bpy.utils.register_class(ArmNewCanvasDialog) - bpy.utils.register_class(ArmNewWasmButton) - bpy.utils.register_class(ArmRefreshScriptsButton) - bpy.utils.register_class(ArmRefreshObjectScriptsButton) - bpy.utils.register_class(ArmRefreshCanvasListButton) - bpy.utils.register_class(ARM_PT_TraitPanel) - bpy.utils.register_class(ARM_PT_SceneTraitPanel) - bpy.utils.register_class(ARM_OT_CopyTraitsFromActive) - bpy.utils.register_class(ARM_MT_context_menu) + __reg_classes() bpy.types.Object.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) bpy.types.Object.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) bpy.types.Scene.arm_traitlist = CollectionProperty(type=ArmTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) bpy.types.Scene.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - -def unregister(): - bpy.utils.unregister_class(ARM_OT_CopyTraitsFromActive) - bpy.utils.unregister_class(ARM_MT_context_menu) - bpy.utils.unregister_class(ArmTraitListItem) - bpy.utils.unregister_class(ARM_UL_TraitList) - bpy.utils.unregister_class(ArmTraitListNewItem) - bpy.utils.unregister_class(ArmTraitListDeleteItem) - bpy.utils.unregister_class(ArmTraitListDeleteItemScene) - bpy.utils.unregister_class(ArmTraitListMoveItem) - bpy.utils.unregister_class(ArmEditScriptButton) - bpy.utils.unregister_class(ArmEditBundledScriptButton) - bpy.utils.unregister_class(ArmEditWasmScriptButton) - bpy.utils.unregister_class(ArmoryGenerateNavmeshButton) - bpy.utils.unregister_class(ArmEditCanvasButton) - bpy.utils.unregister_class(ArmNewScriptDialog) - bpy.utils.unregister_class(ArmGetTreeNodeDialog) - bpy.utils.unregister_class(ArmEditTreeNodeDialog) - bpy.utils.unregister_class(ArmNewTreeNodeDialog) - bpy.utils.unregister_class(ArmNewCanvasDialog) - bpy.utils.unregister_class(ArmNewWasmButton) - bpy.utils.unregister_class(ArmRefreshObjectScriptsButton) - bpy.utils.unregister_class(ArmRefreshScriptsButton) - bpy.utils.unregister_class(ArmRefreshCanvasListButton) - bpy.utils.unregister_class(ARM_PT_TraitPanel) - bpy.utils.unregister_class(ARM_PT_SceneTraitPanel) diff --git a/blender/arm/props_traits_props.py b/blender/arm/props_traits_props.py index 126d01582e..8e353f2d25 100644 --- a/blender/arm/props_traits_props.py +++ b/blender/arm/props_traits_props.py @@ -152,13 +152,9 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn layout.alignment = 'CENTER' -def register(): - bpy.utils.register_class(ArmTraitPropWarning) - bpy.utils.register_class(ArmTraitPropListItem) - bpy.utils.register_class(ARM_UL_PropList) - - -def unregister(): - bpy.utils.unregister_class(ARM_UL_PropList) - bpy.utils.unregister_class(ArmTraitPropListItem) - bpy.utils.unregister_class(ArmTraitPropWarning) +__REG_CLASSES = ( + ArmTraitPropWarning, + ArmTraitPropListItem, + ARM_UL_PropList, +) +register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index b3a5aabbdb..0af31aeb63 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1072,6 +1072,8 @@ def draw(self, context): col.prop(wrd, 'arm_export_tangents') col = layout.column(heading='Quality') + row = col.row() # To expand below property UI horizontally + row.prop(wrd, 'arm_canvas_img_scaling_quality', expand=True) col.prop(wrd, 'arm_texture_quality') col.prop(wrd, 'arm_sound_quality') @@ -1396,6 +1398,7 @@ def draw_header(self, context): row.operator("arm.play", icon="PLAY", text="") else: row.operator("arm.stop", icon="SEQUENCE_COLOR_01", text="") + row.operator("arm.clean_menu", icon="BRUSH_DATA", text="") row.operator("arm.open_editor", icon="DESKTOP", text="") row.operator("arm.open_project_folder", icon="FILE_FOLDER", text="") @@ -2651,6 +2654,39 @@ def execute(self, context): return {'FINISHED'} + +class ARM_PT_BulletDebugDrawingPanel(bpy.types.Panel): + bl_label = "Armory Debug Drawing" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "SCENE_PT_rigid_body_world" + + @classmethod + def poll(cls, context): + return context.scene.rigidbody_world is not None + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Arm'] + + if wrd.arm_physics_engine != 'Bullet': + row = layout.row() + row.alert = True + row.label(text="Physics debug drawing is only supported for the Bullet physics engine") + + col = layout.column(align=False) + col.prop(wrd, "arm_bullet_dbg_draw_wireframe") + col.prop(wrd, "arm_bullet_dbg_draw_aabb") + col.prop(wrd, "arm_bullet_dbg_draw_contact_points") + col.prop(wrd, "arm_bullet_dbg_draw_constraints") + col.prop(wrd, "arm_bullet_dbg_draw_constraint_limits") + col.prop(wrd, "arm_bullet_dbg_draw_normals") + col.prop(wrd, "arm_bullet_dbg_draw_axis_gizmo") + def draw_custom_node_menu(self, context): """Extension of the node context menu. @@ -2718,79 +2754,85 @@ def draw_multiline_with_icon(layout: bpy.types.UILayout, layout_width_px: int, i return col +__REG_CLASSES = ( + ARM_PT_ObjectPropsPanel, + ARM_PT_ModifiersPropsPanel, + ARM_PT_ParticlesPropsPanel, + ARM_PT_PhysicsPropsPanel, + ARM_PT_DataPropsPanel, + ARM_PT_ScenePropsPanel, + ARM_PT_WorldPropsPanel, + InvalidateCacheButton, + InvalidateMaterialCacheButton, + ARM_OT_NewCustomMaterial, + ARM_PG_BindTexturesListItem, + ARM_UL_BindTexturesList, + ARM_OT_BindTexturesListNewItem, + ARM_OT_BindTexturesListDeleteItem, + ARM_PT_MaterialPropsPanel, + ARM_PT_BindTexturesPropsPanel, + ARM_PT_MaterialBlendingPropsPanel, + ARM_PT_MaterialDriverPropsPanel, + ARM_PT_ArmoryPlayerPanel, + ARM_PT_TopbarPanel, + ARM_PT_ArmoryExporterPanel, + ARM_PT_ArmoryExporterAndroidSettingsPanel, + ARM_PT_ArmoryExporterAndroidPermissionsPanel, + ARM_PT_ArmoryExporterAndroidAbiPanel, + ARM_PT_ArmoryExporterAndroidBuildAPKPanel, + ARM_PT_ArmoryExporterHTML5SettingsPanel, + ARM_PT_ArmoryExporterWindowsSettingsPanel, + ARM_PT_ArmoryProjectPanel, + ARM_PT_ProjectFlagsPanel, + ARM_PT_ProjectFlagsDebugConsolePanel, + ARM_PT_ProjectWindowPanel, + ARM_PT_ProjectModulesPanel, + ARM_PT_RenderPathPanel, + ARM_PT_RenderPathRendererPanel, + ARM_PT_RenderPathShadowsPanel, + ARM_PT_RenderPathVoxelsPanel, + ARM_PT_RenderPathWorldPanel, + ARM_PT_RenderPathPostProcessPanel, + ARM_PT_RenderPathCompositorPanel, + ARM_PT_BakePanel, + # ArmVirtualInputPanel, + ArmoryPlayButton, + ArmoryStopButton, + ArmoryBuildProjectButton, + ArmoryOpenProjectFolderButton, + ArmoryOpenEditorButton, + CleanMenu, + CleanButtonMenu, + ArmoryCleanProjectButton, + ArmoryPublishProjectButton, + ArmGenLodButton, + ARM_PT_LodPanel, + ArmGenTerrainButton, + ARM_PT_TerrainPanel, + ARM_PT_TilesheetPanel, + ArmPrintTraitsButton, + ARM_PT_MaterialNodePanel, + ARM_OT_UpdateFileSDK, + ARM_OT_CopyToBundled, + ARM_OT_ShowFileVersionInfo, + ARM_OT_ShowNodeUpdateErrors, + ARM_OT_DiscardPopup, + ArmoryUpdateListAndroidEmulatorButton, + ArmoryUpdateListAndroidEmulatorRunButton, + ArmoryUpdateListInstalledVSButton, + ARM_PT_BulletDebugDrawingPanel, + scene.TLM_PT_Settings, + scene.TLM_PT_Denoise, + scene.TLM_PT_Filtering, + scene.TLM_PT_Encoding, + scene.TLM_PT_Utility, + scene.TLM_PT_Additional, +) +__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) + + def register(): - bpy.utils.register_class(ARM_PT_ObjectPropsPanel) - bpy.utils.register_class(ARM_PT_ModifiersPropsPanel) - bpy.utils.register_class(ARM_PT_ParticlesPropsPanel) - bpy.utils.register_class(ARM_PT_PhysicsPropsPanel) - bpy.utils.register_class(ARM_PT_DataPropsPanel) - bpy.utils.register_class(ARM_PT_ScenePropsPanel) - bpy.utils.register_class(ARM_PT_WorldPropsPanel) - bpy.utils.register_class(InvalidateCacheButton) - bpy.utils.register_class(InvalidateMaterialCacheButton) - bpy.utils.register_class(ARM_OT_NewCustomMaterial) - bpy.utils.register_class(ARM_PG_BindTexturesListItem) - bpy.utils.register_class(ARM_UL_BindTexturesList) - bpy.utils.register_class(ARM_OT_BindTexturesListNewItem) - bpy.utils.register_class(ARM_OT_BindTexturesListDeleteItem) - bpy.utils.register_class(ARM_PT_MaterialPropsPanel) - bpy.utils.register_class(ARM_PT_BindTexturesPropsPanel) - bpy.utils.register_class(ARM_PT_MaterialBlendingPropsPanel) - bpy.utils.register_class(ARM_PT_MaterialDriverPropsPanel) - bpy.utils.register_class(ARM_PT_ArmoryPlayerPanel) - bpy.utils.register_class(ARM_PT_TopbarPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidAbiPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) - bpy.utils.register_class(ARM_PT_ArmoryProjectPanel) - bpy.utils.register_class(ARM_PT_ProjectFlagsPanel) - bpy.utils.register_class(ARM_PT_ProjectFlagsDebugConsolePanel) - bpy.utils.register_class(ARM_PT_ProjectWindowPanel) - bpy.utils.register_class(ARM_PT_ProjectModulesPanel) - bpy.utils.register_class(ARM_PT_RenderPathPanel) - bpy.utils.register_class(ARM_PT_RenderPathRendererPanel) - bpy.utils.register_class(ARM_PT_RenderPathShadowsPanel) - bpy.utils.register_class(ARM_PT_RenderPathVoxelsPanel) - bpy.utils.register_class(ARM_PT_RenderPathWorldPanel) - bpy.utils.register_class(ARM_PT_RenderPathPostProcessPanel) - bpy.utils.register_class(ARM_PT_RenderPathCompositorPanel) - bpy.utils.register_class(ARM_PT_BakePanel) - # bpy.utils.register_class(ArmVirtualInputPanel) - bpy.utils.register_class(ArmoryPlayButton) - bpy.utils.register_class(ArmoryStopButton) - bpy.utils.register_class(ArmoryBuildProjectButton) - bpy.utils.register_class(ArmoryOpenProjectFolderButton) - bpy.utils.register_class(ArmoryOpenEditorButton) - bpy.utils.register_class(CleanMenu) - bpy.utils.register_class(CleanButtonMenu) - bpy.utils.register_class(ArmoryCleanProjectButton) - bpy.utils.register_class(ArmoryPublishProjectButton) - bpy.utils.register_class(ArmGenLodButton) - bpy.utils.register_class(ARM_PT_LodPanel) - bpy.utils.register_class(ArmGenTerrainButton) - bpy.utils.register_class(ARM_PT_TerrainPanel) - bpy.utils.register_class(ARM_PT_TilesheetPanel) - bpy.utils.register_class(ArmPrintTraitsButton) - bpy.utils.register_class(ARM_PT_MaterialNodePanel) - bpy.utils.register_class(ARM_OT_UpdateFileSDK) - bpy.utils.register_class(ARM_OT_CopyToBundled) - bpy.utils.register_class(ARM_OT_ShowFileVersionInfo) - bpy.utils.register_class(ARM_OT_ShowNodeUpdateErrors) - bpy.utils.register_class(ARM_OT_DiscardPopup) - bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorButton) - bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) - bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) - - bpy.utils.register_class(scene.TLM_PT_Settings) - bpy.utils.register_class(scene.TLM_PT_Denoise) - bpy.utils.register_class(scene.TLM_PT_Filtering) - bpy.utils.register_class(scene.TLM_PT_Encoding) - bpy.utils.register_class(scene.TLM_PT_Utility) - bpy.utils.register_class(scene.TLM_PT_Additional) + __reg_classes() bpy.types.VIEW3D_HT_header.append(draw_view3d_header) bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) @@ -2800,81 +2842,11 @@ def register(): bpy.types.Material.arm_bind_textures_list = CollectionProperty(type=ARM_PG_BindTexturesListItem) bpy.types.Material.arm_bind_textures_list_index = IntProperty(name='Index for arm_bind_textures_list', default=0) + def unregister(): bpy.types.NODE_MT_context_menu.remove(draw_custom_node_menu) bpy.types.VIEW3D_MT_object.remove(draw_view3d_object_menu) bpy.types.VIEW3D_HT_header.remove(draw_view3d_header) bpy.types.TOPBAR_HT_upper_bar.remove(draw_space_topbar) - bpy.utils.unregister_class(ArmoryUpdateListInstalledVSButton) - bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorRunButton) - bpy.utils.unregister_class(ArmoryUpdateListAndroidEmulatorButton) - bpy.utils.unregister_class(ARM_OT_DiscardPopup) - bpy.utils.unregister_class(ARM_OT_ShowNodeUpdateErrors) - bpy.utils.unregister_class(ARM_OT_CopyToBundled) - bpy.utils.unregister_class(ARM_OT_ShowFileVersionInfo) - bpy.utils.unregister_class(ARM_OT_UpdateFileSDK) - bpy.utils.unregister_class(ARM_PT_ObjectPropsPanel) - bpy.utils.unregister_class(ARM_PT_ModifiersPropsPanel) - bpy.utils.unregister_class(ARM_PT_ParticlesPropsPanel) - bpy.utils.unregister_class(ARM_PT_PhysicsPropsPanel) - bpy.utils.unregister_class(ARM_PT_DataPropsPanel) - bpy.utils.unregister_class(ARM_PT_WorldPropsPanel) - bpy.utils.unregister_class(ARM_PT_ScenePropsPanel) - bpy.utils.unregister_class(InvalidateCacheButton) - bpy.utils.unregister_class(InvalidateMaterialCacheButton) - bpy.utils.unregister_class(ARM_OT_NewCustomMaterial) - bpy.utils.unregister_class(ARM_PT_MaterialDriverPropsPanel) - bpy.utils.unregister_class(ARM_PT_MaterialBlendingPropsPanel) - bpy.utils.unregister_class(ARM_PT_BindTexturesPropsPanel) - bpy.utils.unregister_class(ARM_PT_MaterialPropsPanel) - bpy.utils.unregister_class(ARM_OT_BindTexturesListDeleteItem) - bpy.utils.unregister_class(ARM_OT_BindTexturesListNewItem) - bpy.utils.unregister_class(ARM_UL_BindTexturesList) - bpy.utils.unregister_class(ARM_PG_BindTexturesListItem) - bpy.utils.unregister_class(ARM_PT_ArmoryPlayerPanel) - bpy.utils.unregister_class(ARM_PT_TopbarPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterWindowsSettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterHTML5SettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidBuildAPKPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidAbiPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidPermissionsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterAndroidSettingsPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryExporterPanel) - bpy.utils.unregister_class(ARM_PT_ArmoryProjectPanel) - bpy.utils.unregister_class(ARM_PT_ProjectFlagsDebugConsolePanel) - bpy.utils.unregister_class(ARM_PT_ProjectFlagsPanel) - bpy.utils.unregister_class(ARM_PT_ProjectWindowPanel) - bpy.utils.unregister_class(ARM_PT_ProjectModulesPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathRendererPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathShadowsPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathVoxelsPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathWorldPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathPostProcessPanel) - bpy.utils.unregister_class(ARM_PT_RenderPathCompositorPanel) - bpy.utils.unregister_class(ARM_PT_BakePanel) - # bpy.utils.unregister_class(ArmVirtualInputPanel) - bpy.utils.unregister_class(ArmoryPlayButton) - bpy.utils.unregister_class(ArmoryStopButton) - bpy.utils.unregister_class(ArmoryBuildProjectButton) - bpy.utils.unregister_class(ArmoryOpenProjectFolderButton) - bpy.utils.unregister_class(ArmoryOpenEditorButton) - bpy.utils.unregister_class(CleanMenu) - bpy.utils.unregister_class(CleanButtonMenu) - bpy.utils.unregister_class(ArmoryCleanProjectButton) - bpy.utils.unregister_class(ArmoryPublishProjectButton) - bpy.utils.unregister_class(ArmGenLodButton) - bpy.utils.unregister_class(ARM_PT_LodPanel) - bpy.utils.unregister_class(ArmGenTerrainButton) - bpy.utils.unregister_class(ARM_PT_TerrainPanel) - bpy.utils.unregister_class(ARM_PT_TilesheetPanel) - bpy.utils.unregister_class(ArmPrintTraitsButton) - bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) - - bpy.utils.unregister_class(scene.TLM_PT_Settings) - bpy.utils.unregister_class(scene.TLM_PT_Denoise) - bpy.utils.unregister_class(scene.TLM_PT_Filtering) - bpy.utils.unregister_class(scene.TLM_PT_Encoding) - bpy.utils.unregister_class(scene.TLM_PT_Utility) - bpy.utils.unregister_class(scene.TLM_PT_Additional) + __unreg_classes() diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 521718c18d..c6a2b3cf45 100644 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -727,7 +727,7 @@ def safesrc(s): def safestr(s: str) -> str: """Outputs a string where special characters have been replaced with '_', which can be safely used in file and path names.""" - for c in r'''[]/\;,><&*:%=+@!#^()|?^'"''': + for c in r'''[]/\;,><&*:§$%=+@!#^()|?^'"''': s = s.replace(c, '_') return ''.join([i if ord(i) < 128 else '_' for i in s]) @@ -800,11 +800,11 @@ def get_cascade_size(rpdat): def check_blender_version(op: bpy.types.Operator): - """Check whether the user uses the correct Blender version, if not - report in UI. + """Check whether the Blender version is supported by Armory, + if not, report in UI. """ - if not compare_version_blender_arm(): - op.report({'INFO'}, 'For Armory to work correctly, you need Blender 3.3 LTS.') + if bpy.app.version[0] != 3 or bpy.app.version[1] != 6: + op.report({'INFO'}, 'For Armory to work correctly, you need Blender 3.6 LTS.') def check_saved(self): @@ -1136,8 +1136,6 @@ def get_link_web_server(): addon_prefs = get_arm_preferences() return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server -def compare_version_blender_arm(): - return not (bpy.app.version[0] != 3 or bpy.app.version[1] != 3) def get_file_arm_version_tuple() -> tuple[int]: wrd = bpy.data.worlds['Arm'] diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index d969267676..3a38093b9d 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -499,6 +499,10 @@ class Main { f.write(""" armory.system.Starter.numAssets = """ + str(len(asset_references)) + """; armory.system.Starter.drawLoading = """ + loadscreen_class + """.render;""") + if wrd.arm_canvas_img_scaling_quality == 'low': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") + elif wrd.arm_canvas_img_scaling_quality == 'high': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") f.write(""" armory.system.Starter.main( '""" + arm.utils.safestr(scene_name) + scene_ext + """', @@ -569,27 +573,26 @@ def write_compiledglsl(defs, make_variants): continue # Write a shader variant instead f.write("#define " + d + "\n") - if rpdat.rp_renderer == 'Deferred': - gbuffer_size = make_renderpath.get_num_gbuffer_rts_deferred() - f.write(f'#define GBUF_SIZE {gbuffer_size}\n') + gbuffer_size = make_renderpath.get_num_gbuffer_rts() + f.write(f'#define GBUF_SIZE {gbuffer_size}\n') - # Write indices of G-Buffer render targets - f.write('#define GBUF_IDX_0 0\n') - f.write('#define GBUF_IDX_1 1\n') + # Write indices of G-Buffer render targets + f.write('#define GBUF_IDX_0 0\n') + f.write('#define GBUF_IDX_1 1\n') - idx_emission = 2 - idx_refraction = 2 - if '_gbuffer2' in wrd.world_defs: - f.write('#define GBUF_IDX_2 2\n') - idx_emission += 1 - idx_refraction += 1 + idx_emission = 2 + idx_refraction = 2 + if '_gbuffer2' in wrd.world_defs: + f.write('#define GBUF_IDX_2 2\n') + idx_emission += 1 + idx_refraction += 1 - if '_EmissionShaded' in wrd.world_defs: - f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') - idx_refraction += 1 + if '_EmissionShaded' in wrd.world_defs: + f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') + idx_refraction += 1 - if '_SSRefraction' in wrd.world_defs: - f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') + if '_SSRefraction' in wrd.world_defs: + f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') f.write("""#if defined(HLSL) || defined(METAL) #define _InvY From a6cc650ebe10ae34c88ccf52d2fb8d563b03bfca Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 16 Sep 2023 22:14:19 +0200 Subject: [PATCH 058/175] Change bind and shader constants names --- Sources/armory/trait/internal/UniformsManager.hx | 8 ++++---- blender/arm/material/make.py | 12 ++++++------ blender/arm/material/shader.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/armory/trait/internal/UniformsManager.hx b/Sources/armory/trait/internal/UniformsManager.hx index 3be9499b10..8739c08da6 100644 --- a/Sources/armory/trait/internal/UniformsManager.hx +++ b/Sources/armory/trait/internal/UniformsManager.hx @@ -110,15 +110,15 @@ class UniformsManager extends Trait{ switch (constant.type) { case "float": var link = constant.link; - var value = constant.float; + var value = constant.floatValue; 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); + vec.x = constant.vec3Value.get(0); + vec.y = constant.vec3Value.get(1); + vec.z = constant.vec3Value.get(2); setVec3Value(material, object, constant.link, vec); register(Vector); diff --git a/blender/arm/material/make.py b/blender/arm/material/make.py index ba3b3e73a8..c49c388301 100644 --- a/blender/arm/material/make.py +++ b/blender/arm/material/make.py @@ -96,10 +96,10 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], mat_data['contexts'].append(c) if rp == 'mesh': - c['bind_constants'].append({'name': 'receiveShadow', 'bool': material.arm_receive_shadow}) + c['bind_constants'].append({'name': 'receiveShadow', 'boolValue': material.arm_receive_shadow}) if material.arm_material_id != 0: - c['bind_constants'].append({'name': 'materialID', 'int': material.arm_material_id}) + c['bind_constants'].append({'name': 'materialID', 'intValue': material.arm_material_id}) if material.arm_material_id == 2: wrd.world_defs += '_Hair' @@ -107,10 +107,10 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], elif rpdat.rp_sss_state != 'Off': const = {'name': 'materialID'} if needs_sss: - const['int'] = 2 + const['intValue'] = 2 sss_used = True else: - const['int'] = 0 + const['intValue'] = 0 c['bind_constants'].append(const) # TODO: Mesh only material batching @@ -133,10 +133,10 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], for inp in node.inputs: if inp.is_uniform: uname = arm.utils.safesrc(inp.node.name) + arm.utils.safesrc(inp.name) # Merge with cycles module - c['bind_constants'].append({'name': uname, cycles.glsl_type(inp.type): glsl_value(inp.default_value)}) + c['bind_constants'].append({'name': uname, cycles.glsl_type(inp.type)+'Value': glsl_value(inp.default_value)}) elif rp == 'translucent': - c['bind_constants'].append({'name': 'receiveShadow', 'bool': material.arm_receive_shadow}) + c['bind_constants'].append({'name': 'receiveShadow', 'boolValue': material.arm_receive_shadow}) elif rp == 'shadowmap': if wrd.arm_batch_materials: diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 48707cc9ae..bf6c7b8cfa 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -124,9 +124,9 @@ def add_constant(self, ctype, name, link=None, default_value=None, is_arm_mat_pa c['link'] = link if default_value is not None: if ctype == 'float': - c['float'] = default_value + c['floatValue'] = default_value if ctype == 'vec3': - c['vec3'] = default_value + c['vec3Value'] = default_value if is_arm_mat_param is not None: c['is_arm_parameter'] = True self.constants.append(c) From 1f434e83e5657b365f2c983450855fdd5daaf266 Mon Sep 17 00:00:00 2001 From: Sylvio Sell Date: Mon, 18 Sep 2023 21:36:48 +0200 Subject: [PATCH 059/175] adding new node for math terms (also by formula-lib) - can be used to change term-expr. at runtime or to bind terms to other termparams --- Sources/armory/logicnode/MathTermNode.hx | 127 +++++++++++++++++++++ blender/arm/logicnode/math/LN_math_term.py | 70 ++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 Sources/armory/logicnode/MathTermNode.hx create mode 100644 blender/arm/logicnode/math/LN_math_term.py diff --git a/Sources/armory/logicnode/MathTermNode.hx b/Sources/armory/logicnode/MathTermNode.hx new file mode 100644 index 0000000000..b7c1b7c11e --- /dev/null +++ b/Sources/armory/logicnode/MathTermNode.hx @@ -0,0 +1,127 @@ +package armory.logicnode; + +import haxe.io.Bytes; +import haxe.io.BytesInput; +import haxe.io.BytesOutput; +import armory.logicnode.MathExpressionNode.Formula; +import armory.logicnode.MathExpressionNode.FormulaException; + +class MathTermNode extends LogicNode { + + public var property0:Bool; // bind param values + + public function new(tree: LogicTree) { + super(tree); + } + + var simplifyError:String = null; + var derivateError:String = null; + var resultError:String = null; + + override function get(from: Int): Dynamic { + + var error:String = null; + var errorPos:Int = -1; + + var formula:Formula = null; + + try { + formula = new Formula(inputs[0].get()); + } + catch(e:FormulaException) { + error = e.msg; + errorPos = e.pos; + } + + // bind input values to formula parameters + if ((error == null) && (property0 || from == 3)) { + try { + bindValuesToFormulaParams(formula); + } + catch(e:FormulaException) { + error = e.msg; + errorPos = e.pos; + } + } + + if (from == 0) { // -------- Formula ---------- + return (error == null) ? formula : null; + } + else if (from == 1) { // -------- Simplifyed ---------- + var f:Formula = null; + simplifyError = null; + if (error == null) { + try { + f = formula.simplify(); + } + catch(e:FormulaException) { + simplifyError = e.msg; + } + } + return f; + } + else if (from == 2) { // -------- Derivate ---------- + var f:Formula = null; + derivateError = null; + if (error == null) { + try { + f = formula.derivate( inputs[1].get() ); + } + catch(e:FormulaException) { + derivateError = e.msg; + } + } + return f; + } + else if (from == 3) { // -------- Result ---------- + var result:Float = 0.0; + resultError = null; + if (error == null) { + try { + result = formula.result; + } + catch(e:FormulaException) { + resultError = e.msg; + } + } + return result; + } + else if (from == 4) { // -------- Error ---------- + if (error != null) return error; + else { + var errorMessage:String = ""; + if (simplifyError != null) errorMessage += "Simplifyed:" + simplifyError + " "; + if (derivateError != null) errorMessage += "Derivate:" + derivateError + " "; + if (resultError != null) errorMessage += "Result:" + resultError + " "; + return errorMessage; + } + } + else { // -------- Error Position ---------- + return errorPos; + } + } + + public inline function bindValuesToFormulaParams(formula:Formula) { + var i = 1; + while (i < inputs.length) + { + if (inputs[i+1].get() != null) + { + if (Std.isOfType(inputs[i+1].get(), Float)) { + // trace ("Float param") + formula.bind( (inputs[i+1].get():Float), inputs[i].get() ); + } + else if (Std.isOfType(inputs[i+1].get(), String)) { + // trace ("String param") + formula.bind( (inputs[i+1].get():String), inputs[i].get() ); + } + else { + // trace ("Formula param") + formula.bind( (inputs[i+1].get():Formula), inputs[i].get() ); + } + } + i+=2; + } + + } +} diff --git a/blender/arm/logicnode/math/LN_math_term.py b/blender/arm/logicnode/math/LN_math_term.py new file mode 100644 index 0000000000..3c4842db06 --- /dev/null +++ b/blender/arm/logicnode/math/LN_math_term.py @@ -0,0 +1,70 @@ +from arm.logicnode.arm_nodes import * +import re + +class MathTermNode(ArmLogicTreeNode): + """Formula for symbolic Math""" + bl_idname = 'LNMathTermNode' + bl_label = 'Math Term' + arm_version = 0 + + num_params: IntProperty(default=2, min=0) + + property0: HaxeBoolProperty('property0', name='Resolve params', description='Resolve input param values/subterms for output term/transformations', default=False) + + def __init__(self): + super(MathTermNode, self).__init__() + self.register_id() + + + def arm_init(self, context): + + # OUTPUTS: + self.add_output('ArmDynamicSocket', 'Math Term') + self.add_output('ArmDynamicSocket', 'Simplifyed') + self.add_output('ArmDynamicSocket', 'Derivate') + self.add_output('ArmFloatSocket', 'Result') + self.add_output('ArmStringSocket', 'Error') + self.add_output('ArmIntSocket', 'ErrorPos') + + # INPUTS: + + # HOW to setup a Tooltip here and how to put it above the param-add/remove-buttons into layout ? + self.add_input('ArmStringSocket', 'Math Term', default_value='a+b') + + # two default parameters at start + self.add_input('ArmStringSocket', 'Param 0', default_value='a') + self.add_input('ArmDynamicSocket', 'Value / Term 0') + + self.add_input('ArmStringSocket', 'Param 1', default_value='b') + self.add_input('ArmDynamicSocket', 'Value / Term 1') + + def add_sockets(self): + self.add_input('ArmStringSocket', 'Name ' + str(self.num_params)) + #self.add_input('ArmFloatSocket', 'Value ' + str(self.num_params)) + self.add_input('ArmDynamicSocket', 'Value / Term ' + str(self.num_params)) + self.num_params += 1 + + def remove_sockets(self): + if self.num_params > 0: + self.inputs.remove(self.inputs.values()[-1]) + self.inputs.remove(self.inputs.values()[-1]) + self.num_params -= 1 + + def draw_buttons(self, context, layout): + # Bind values to params Property + layout.prop(self, 'property0') + + # Button ADD parameter + row = layout.row(align=True) + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='Add Param', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_sockets' + + # Button REMOVE parameter + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'remove_sockets' + if self.num_params == 0: + column.enabled = False From 5afd3a4d66298ef7d8ae9d62d86e2d4e96451871 Mon Sep 17 00:00:00 2001 From: Sylvio Sell Date: Wed, 20 Sep 2023 14:06:02 +0200 Subject: [PATCH 060/175] upgrading to formula 0.43 lib - fixes an error if build with no-inline option --- Sources/armory/logicnode/MathExpressionNode.hx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/armory/logicnode/MathExpressionNode.hx b/Sources/armory/logicnode/MathExpressionNode.hx index 49c7ff136d..2211f33c0b 100644 --- a/Sources/armory/logicnode/MathExpressionNode.hx +++ b/Sources/armory/logicnode/MathExpressionNode.hx @@ -635,8 +635,8 @@ class TermNode { * static Function Pointers (to stored in this.operation) * */ - static function opName(t:TermNode) :Float if (t.left!=null) return t.left.result else ErrorMsg.emptyFunction(t.symbol); - static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else ErrorMsg.missingParameter(t.symbol); + static function opName(t:TermNode) :Float if (t.left != null) return t.left.result else { ErrorMsg.emptyFunction(t.symbol); return 0; } + static function opParam(t:TermNode):Float if (t.left!=null) return t.left.result else { ErrorMsg.missingParameter(t.symbol); return 0; } static function opValue(t:TermNode):Float return t.value; static var MathOp:MapFloat> = [ @@ -735,7 +735,8 @@ class TermNode { static function parseString(s:String, errPos:Int, ?params:Map):TermNode { var t:TermNode = null; var operations:Array = new Array(); - var e, f:String; + var e:String = ""; + var f:String; var negate:Bool; var spaces:Int = 0; @@ -822,7 +823,7 @@ class TermNode { } if ( operations.length > 0 ) { - if ( operations[operations.length-1].right == null ) ErrorMsg.missingRightOperand(errPos-spaces); + if ( operations[operations.length - 1].right == null ) { ErrorMsg.missingRightOperand(errPos - spaces); return t;} else { operations.sort(function(a:OperationNode, b:OperationNode):Int { @@ -879,6 +880,7 @@ class TermNode { } if (s.indexOf(")") == 0) ErrorMsg.noOpeningBracket(errPos); else ErrorMsg.wrongChar(errPos); + return ""; } From 1bbae96a3a7246d52cbeda99b705ba44bff9ad3d Mon Sep 17 00:00:00 2001 From: 1k8 Date: Sat, 23 Sep 2023 00:05:14 +0200 Subject: [PATCH 061/175] Allow building with ZUI disabled The following lines were added even when there is no canvas, debugging is disabled, and ZUI is disabled armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low; causing Type not found : zui.Zui --- blender/arm/write_data.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index 7a2a64ec58..26e53a2241 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -498,10 +498,11 @@ class Main { f.write(""" armory.system.Starter.numAssets = """ + str(len(asset_references)) + """; armory.system.Starter.drawLoading = """ + loadscreen_class + """.render;""") - if wrd.arm_canvas_img_scaling_quality == 'low': - f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") - elif wrd.arm_canvas_img_scaling_quality == 'high': - f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") + if wrd.arm_ui == 'Enabled': + if wrd.arm_canvas_img_scaling_quality == 'low': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") + elif wrd.arm_canvas_img_scaling_quality == 'high': + f.write(f"armory.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") f.write(""" armory.system.Starter.main( '""" + arm.utils.safestr(scene_name) + scene_ext + """', From d63743fea5ae86f69fdfcc2a07d2fb7e37030d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:05:28 +0200 Subject: [PATCH 062/175] Fix node replacement routine for Gate node --- blender/arm/logicnode/logic/LN_gate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/blender/arm/logicnode/logic/LN_gate.py b/blender/arm/logicnode/logic/LN_gate.py index 9ab7cde859..7ff3a6069a 100644 --- a/blender/arm/logicnode/logic/LN_gate.py +++ b/blender/arm/logicnode/logic/LN_gate.py @@ -68,11 +68,10 @@ def draw_buttons(self, context, layout): column.enabled = False def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 2): - raise LookupError() - if self.arm_version == 1 or self.arm_version == 2: return NodeReplacement( - 'LNGateNode', self.arm_version, 'LNGateNode', 2, + 'LNGateNode', self.arm_version, 'LNGateNode', 3, in_socket_mapping={0:0, 1:1, 2:2}, out_socket_mapping={0:0, 1:1} - ) \ No newline at end of file + ) + else: + raise LookupError() From 34cc5d819543c6a6beebef6a097661b8f91447d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:06:37 +0200 Subject: [PATCH 063/175] Fix node replacement routine for Array Float node --- blender/arm/logicnode/array/LN_array_float.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/blender/arm/logicnode/array/LN_array_float.py b/blender/arm/logicnode/array/LN_array_float.py index 9b417de449..8ebc164d9c 100644 --- a/blender/arm/logicnode/array/LN_array_float.py +++ b/blender/arm/logicnode/array/LN_array_float.py @@ -44,7 +44,18 @@ def synchronize_from_master(self, master_node: ArmLogicVariableNodeMixin): inp.default_value_raw = master_node.inputs[i].get_default_value() def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 2): + if self.arm_version < 0 or self.arm_version > 2: raise LookupError() - - return NodeReplacement.Identity(self) + + newnode = node_tree.nodes.new(FloatArrayNode.bl_idname) + for inp_old in self.inputs: + inp_new = newnode.add_input('ArmFloatSocket', inp_old.name) + inp_new.hide = self.arm_logic_id != '' + inp_new.enabled = self.arm_logic_id != '' + inp_new.default_value_raw = inp_old.get_default_value() + NodeReplacement.replace_input_socket(node_tree, inp_old, inp_new) + + NodeReplacement.replace_output_socket(node_tree, self.outputs[0], newnode.outputs[0]) + NodeReplacement.replace_output_socket(node_tree, self.outputs[1], newnode.outputs[1]) + + return newnode From 25c8e85aa31418cf6b46bf6ebcb5e98bcaf85a5b Mon Sep 17 00:00:00 2001 From: 1k8 Date: Thu, 28 Sep 2023 00:56:44 +0200 Subject: [PATCH 064/175] Update LN_network_close_connection.py Draw missing property1 --- blender/arm/logicnode/network/LN_network_close_connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/logicnode/network/LN_network_close_connection.py b/blender/arm/logicnode/network/LN_network_close_connection.py index 296c3d39d8..37cbe02226 100644 --- a/blender/arm/logicnode/network/LN_network_close_connection.py +++ b/blender/arm/logicnode/network/LN_network_close_connection.py @@ -32,3 +32,4 @@ def arm_init(self, context): def draw_buttons(self, context, layout): layout.prop(self, 'property0') + layout.prop(self, 'property1') From 30ced7128abcd183e50519c8f37ba362f6172e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:36:31 +0200 Subject: [PATCH 065/175] Add TCanvas.visible attribute to hide entire canvas --- Sources/armory/trait/internal/CanvasScript.hx | 24 +++++++++++++++---- Sources/armory/ui/Canvas.hx | 22 +++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Sources/armory/trait/internal/CanvasScript.hx b/Sources/armory/trait/internal/CanvasScript.hx index a25461619a..1974f92393 100644 --- a/Sources/armory/trait/internal/CanvasScript.hx +++ b/Sources/armory/trait/internal/CanvasScript.hx @@ -44,7 +44,7 @@ class CanvasScript extends Trait { } iron.data.Data.getFont(font, function(defaultFont: kha.Font) { - var c: TCanvas = haxe.Json.parse(blob.toString()); + var c: TCanvas = Canvas.parseCanvasFromBlob(blob); if (c.theme == null) c.theme = Canvas.themes[0].NAME; cui = new Zui({font: defaultFont, theme: Canvas.getTheme(c.theme)}); @@ -140,12 +140,26 @@ class CanvasScript extends Trait { return cui.ops.scaleFactor; } + @:deprecated("Please use setCanvasVisible() instead") + public inline function setCanvasVisibility(visible: Bool) { + setCanvasVisible(visible); + } + + /** + Set whether the active canvas is visible. + + Note that elements of invisible canvases are not rendered and computed, + so it is not possible to interact with those elements on the screen. + **/ + public inline function setCanvasVisible(visible: Bool) { + canvas.visible = visible; + } + /** - Set visibility of canvas - @param visible Whether canvas should be visible or not + Get whether the active canvas is visible. **/ - public function setCanvasVisibility(visible: Bool){ - for (e in canvas.elements) e.visible = visible; + public inline function getCanvasVisible(): Bool { + return canvas.visible; } /** diff --git a/Sources/armory/ui/Canvas.hx b/Sources/armory/ui/Canvas.hx index 6d9d0be022..1db372a562 100644 --- a/Sources/armory/ui/Canvas.hx +++ b/Sources/armory/ui/Canvas.hx @@ -22,11 +22,13 @@ class Canvas { public static function draw(ui: Zui, canvas: TCanvas, g: kha.graphics2.Graphics): Array { + events.resize(0); + + if (!canvas.visible) return events; + screenW = kha.System.windowWidth(); screenH = kha.System.windowHeight(); - events.resize(0); - _ui = ui; g.end(); @@ -313,6 +315,21 @@ class Canvas { if (rotated) ui.g.popTransformation(); } + /** + Parse the content of the given blob object and return a `TCanvas` object + from it. + **/ + public static function parseCanvasFromBlob(blob: kha.Blob): TCanvas { + final raw: haxe.DynamicAccess = haxe.Json.parse(blob.toString()); + + // Ensure TCanvas has all attributes even for older files + if (!raw.exists("visible")) { + raw.set("visible", true); + } + + return (raw: Dynamic); + } + static inline function getText(canvas: TCanvas, e: TElement): String { return e.text; } @@ -414,6 +431,7 @@ typedef TCanvas = { var height: Int; var elements: Array; var theme: String; + var visible: Bool; @:optional var assets: Array; @:optional var locales: Array; } From dc5f6bf6a56e9b9ae24791ce94f3d79d62950047 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Mon, 2 Oct 2023 16:15:53 +0700 Subject: [PATCH 066/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 30ebdbdeda..802076a441 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.9' +arm_version = '2023.10' arm_commit = '$Id$' def get_project_html5_copy(self): From 9b884ac9b5653a0eca0af30b9c60236bfc0549e5 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:12:36 +0200 Subject: [PATCH 067/175] Use the new recast module --- Sources/armory/system/Starter.hx | 17 +++++++++++++++-- blender/arm/write_data.py | 11 +++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Sources/armory/system/Starter.hx b/Sources/armory/system/Starter.hx index d97ab0708c..68cc89d296 100644 --- a/Sources/armory/system/Starter.hx +++ b/Sources/armory/system/Starter.hx @@ -96,8 +96,17 @@ class Starter { function loadLib(name: String) { kha.Assets.loadBlobFromPath(name, function(b: kha.Blob) { js.Syntax.code("(1, eval)({0})", b.toString()); - tasks--; - start(); + #if kha_krom + js.Syntax.code("Recast({print:function(s){iron.log(s);},instantiateWasm:function(imports,successCallback) { + var wasmbin = Krom.loadBlob('recast.wasm.wasm'); + var module = new WebAssembly.Module(wasmbin); + var inst = new WebAssembly.Instance(module,imports); + successCallback(inst); + return inst.exports; + }}).then(function(){ tasks--; start();})"); + #else + js.Syntax.code("Recast({print:function(s){iron.log(s);}}).then(function(){ tasks--; start();})"); + #end }); } #end @@ -115,8 +124,12 @@ class Starter { #if (js && arm_navigation) tasks++; + #if kha_krom + loadLib("recast.wasm.js"); + #else loadLib("recast.js"); #end + #end #if (arm_config) tasks++; diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index 26e53a2241..60146ddd53 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -159,8 +159,15 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo assets.add_khafile_def('arm_navigation') if not os.path.exists('Libraries/haxerecast'): khafile.write(add_armory_library(sdk_path + '/lib/', 'haxerecast', rel_path=do_relpath_sdk)) - if state.target.startswith('krom') or state.target == 'html5': - recastjs_path = sdk_path + '/lib/haxerecast/js/recast/recast.js' + if state.target.startswith('krom'): + recastjs_path = sdk_path + '/lib/haxerecast/recastjs/recast.wasm.js' + recastjs_path = recastjs_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(recastjs_path, rel_path=do_relpath_sdk)) + recastjs_wasm_path = sdk_path + '/lib/haxerecast/recastjs/recast.wasm.wasm' + recastjs_wasm_path = recastjs_wasm_path.replace('\\', '/').replace('//', '/') + khafile.write(add_assets(recastjs_wasm_path, rel_path=do_relpath_sdk)) + elif state.target == 'html5' or state.target == 'node': + recastjs_path = sdk_path + '/lib/haxerecast/recastjs/recast.js' recastjs_path = recastjs_path.replace('\\', '/').replace('//', '/') khafile.write(add_assets(recastjs_path, rel_path=do_relpath_sdk)) From 1a47556639ada89ccd962dc54c76648acd481375 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:13:25 +0200 Subject: [PATCH 068/175] Implement debug draw for navmesh --- .../trait/navigation/DebugDrawHelper.hx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Sources/armory/trait/navigation/DebugDrawHelper.hx diff --git a/Sources/armory/trait/navigation/DebugDrawHelper.hx b/Sources/armory/trait/navigation/DebugDrawHelper.hx new file mode 100644 index 0000000000..acafd05ce7 --- /dev/null +++ b/Sources/armory/trait/navigation/DebugDrawHelper.hx @@ -0,0 +1,102 @@ +package armory.trait.navigation; + +import kha.FastFloat; +import kha.System; + +import iron.math.Vec4; + +#if arm_ui +import armory.ui.Canvas; +#end + +#if arm_navigation +class DebugDrawHelper { + final navigation: Navigation; + final lines: Array = []; + + var debugMode: Navigation.DebugDrawMode = NoDebug; + + public function new(navigation: Navigation) { + this.navigation = navigation; + + iron.App.notifyOnRender2D(onRender); + } + + public function drawLine(from: Vec4, to: Vec4, color: kha.Color) { + + final fromScreenSpace = worldToScreenFast(new Vec4().setFrom(from)); + final toScreenSpace = worldToScreenFast(new Vec4().setFrom(to)); + + // For now don't draw lines if any point is outside of clip space z, + // investigate how to clamp lines to clip space borders + if (fromScreenSpace.w == 1 && toScreenSpace.w == 1) { + lines.push({ + fromX: fromScreenSpace.x, + fromY: fromScreenSpace.y, + toX: toScreenSpace.x, + toY: toScreenSpace.y, + color: color + }); + } + } + + public function setDebugMode(debugMode: Navigation.DebugDrawMode) { + this.debugMode = debugMode; + } + + public function getDebugMode(): Navigation.DebugDrawMode { + return debugMode; + } + + function onRender(g: kha.graphics2.Graphics) { + + if (getDebugMode() == NoDebug) { + return; + } + + for(navMesh in Navigation.active.navMeshes) { + navMesh.drawDebugMesh(this); + } + + g.opacity = 1.0; + + for (line in lines) { + g.color = line.color; + g.drawLine(line.fromX, line.fromY, line.toX, line.toY, 1.0); + } + lines.resize(0); + } + + /** + Transform a world coordinate vector into screen space and store the + result in the input vector's x and y coordinates. The w coordinate is + set to 0 if the input vector is outside the active camera's far and near + planes, and 1 otherwise. + **/ + inline function worldToScreenFast(loc: Vec4): Vec4 { + final cam = iron.Scene.active.camera; + loc.w = 1.0; + loc.applyproj(cam.VP); + + if (loc.z < -1 || loc.z > 1) { + loc.w = 0.0; + } + else { + loc.x = (loc.x + 1) * 0.5 * System.windowWidth(); + loc.y = (1 - loc.y) * 0.5 * System.windowHeight(); + loc.w = 1.0; + } + + return loc; + } +} + +@:structInit +class LineData { + public var fromX: FastFloat; + public var fromY: FastFloat; + public var toX: FastFloat; + public var toY: FastFloat; + public var color: kha.Color; +} +#end From 52a3ca8c17c95b166e0b4a8e4ed1c347d6f3efd5 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:13:49 +0200 Subject: [PATCH 069/175] Implement new navigation class --- Sources/armory/trait/navigation/Navigation.hx | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Sources/armory/trait/navigation/Navigation.hx b/Sources/armory/trait/navigation/Navigation.hx index 99ab5f6ac3..5721a8982a 100644 --- a/Sources/armory/trait/navigation/Navigation.hx +++ b/Sources/armory/trait/navigation/Navigation.hx @@ -1,26 +1,47 @@ package armory.trait.navigation; #if arm_navigation -import haxerecast.Recast; +import iron.math.Vec4; + import armory.trait.NavMesh; -#end class Navigation extends iron.Trait { -#if (!arm_navigation) - public function new() { super(); } -#else - public static var active: Navigation = null; public var navMeshes: Array = []; - public var recast: Recast; + //public var recast: Recast; + + public var debugDrawHelper: DebugDrawHelper = null; public function new() { super(); active = this; + initDebugDrawing(); + + } + + function initDebugDrawing() { + debugDrawHelper = new DebugDrawHelper(this); + } +} + +enum abstract DebugDrawMode(Int) from Int to Int { + /** All debug flags off. **/ + var NoDebug = 0; + + /** Draw wireframes of the NavMesh. **/ + var DrawWireframe = 1; +} - recast = new Recast(); +class RecastConversions { + public static function recastVec3FromVec4(vec: Vec4): recast.Recast.Vec3{ + return new recast.Recast.Vec3(vec.x, vec.z, vec.y); } -#end + + public static function vec4FromRecastVec3(vec: recast.Recast.Vec3): Vec4 { + return new Vec4(vec.x, vec.z, vec.y); + } + } +#end \ No newline at end of file From bad789e95584f9f37994eaec0513017153c98037 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:14:40 +0200 Subject: [PATCH 070/175] Implement updated recast navigation functions --- Sources/armory/trait/NavMesh.hx | 507 +++++++++++++++++++++++++++++--- 1 file changed, 464 insertions(+), 43 deletions(-) diff --git a/Sources/armory/trait/NavMesh.hx b/Sources/armory/trait/NavMesh.hx index 421e5504c6..9d135140b1 100644 --- a/Sources/armory/trait/NavMesh.hx +++ b/Sources/armory/trait/NavMesh.hx @@ -1,83 +1,504 @@ package armory.trait; #if arm_navigation +import recast.Recast.RecastConfigHelper; +import haxe.ds.Vector; +import armory.trait.navigation.DebugDrawHelper; +import kha.arrays.Float32Array; +import iron.object.Object; import armory.trait.navigation.Navigation; -import haxerecast.Recast; -#end -import iron.Trait; import iron.data.Data; import iron.math.Vec4; +import iron.system.Time; +import kha.arrays.Uint32Array; +import iron.object.MeshObject; +import iron.data.Geometry; +import iron.data.MeshData; +#end +import iron.Trait; class NavMesh extends Trait { -#if (!arm_navigation) + #if !arm_navigation public function new() { super(); } -#else + #else + + @prop + public var debugDraw:Bool = false; // recast config: + /// Also use immidiate children for nav mesh construction. @prop - public var cellSize: Float = 0.3; // voxelization cell size + public var combineImmidiateChildren:Bool = true; + /// The width of the field along the x-axis. [Limit: >= 0] [Units: vx] @prop - public var cellHeight: Float = 0.2; // voxelization cell height + public var width:Int = 0; + + /// The height of the field along the z-axis. [Limit: >= 0] [Units: vx] @prop - public var agentHeight: Float = 2.0; // agent capsule height + public var height:Int = 0; + + /// Enable for tiled mesh and using temporary obstacles. `tileSize` will be ignored if set to false. + @prop + public var tiledMesh:Bool = true; + + /// The width/height size of tile's on the xz-plane. [Limit: >= 0] [Units: vx] + @prop + public var tileSize:Int = 50; + + /// The size of the non-navigable border around the heightfield. [Limit: >=0] [Units: vx] @prop - public var agentRadius: Float = 0.4; // agent capsule radius + public var borderSize:Int = 0; + + /// The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu] + @prop + public var cellSize:Float = 0.2; + + /// The y-axis cell size to use for fields. [Limit: > 0] [Units: wu] @prop - public var agentMaxClimb: Float = 0.9; // how high steps agents can climb, in voxels + public var cellHeight:Float = 0.3; + + /// The minimum bounds of the field's AABB. [(x, y, z)] [Units: wu] + //public var bmin:haxe.ds.Vector; + + /// The maximum bounds of the field's AABB. [(x, y, z)] [Units: wu] + //public var bmax:haxe.ds.Vector; + + /// The maximum slope that is considered walkable. [Limits: 0 <= value < 90] [Units: Degrees] @prop - public var agentMaxSlope: Float = 45.0; // maximum slope angle, in degrees + public var walkableSlopeAngle:Float = 45.0; - var recast: Recast = null; - var ready = false; + /// Minimum floor to 'ceiling' height that will still allow the floor area to + /// be considered walkable. [Limit: >= 3] [Units: vx] + @prop + public var walkableHeight:Int = 3; + + /// Maximum ledge height that is considered to still be traversable. [Limit: >=0] [Units: vx] + @prop + public var walkableClimb:Int = 2; + + /// The distance to erode/shrink the walkable area of the heightfield away from + /// obstructions. [Limit: >=0] [Units: vx] + @prop + public var walkableRadius:Int = 1; + + /// The maximum allowed length for contour edges along the border of the mesh. [Limit: >=0] [Units: vx] + @prop + public var maxEdgeLen:Int = 12; + + /// The maximum distance a simplfied contour's border edges should deviate + /// the original raw contour. [Limit: >=0] [Units: vx] + @prop + public var maxSimplificationError:Float = 1.3; + + /// The minimum number of cells allowed to form isolated island areas. [Limit: >=0] [Units: vx] + @prop + public var minRegionArea:Int = 8; + + /// Any regions with a span count smaller than this value will, if possible, + /// be merged with larger regions. [Limit: >=0] [Units: vx] + @prop + public var mergeRegionArea:Int = 20; + + /// The maximum number of vertices allowed for polygons generated during the + /// contour to polygon conversion process. [Limit: >= 3] + @prop + public var maxVertsPerPoly:Int = 6; + + /// Sets the sampling distance to use when generating the detail mesh. + /// (For height detail only.) [Limits: 0 or >= 0.9] [Units: wu] + @prop + public var detailSampleDist:Float = 6; + + /// The maximum distance the detail mesh surface should deviate from heightfield + /// data. (For height detail only.) [Limit: >=0] [Units: wu] + @prop + public var detailSampleMaxError:Float = 1; + + // maximum number of crowd agents + @prop + public var maxCrowdAgents: Int = 50; + + // maximum radius of crowd agents + @prop + public var maxCrowdAgentRadius: Float = 3.0; + + var recastNavMesh: recast.Recast.NavMesh = null; + var recastCrowd: recast.Recast.Crowd = null; + public var ready(default, null) = false; + + var crowdAgentMap: Map = new Map(); + + var tempObstacleCounter = 0; + + var tempObstacleMap: Map = new Map(); + + var recastObstacleMap: Map = new Map(); + + public var navMeshDebugColor: kha.Color = Green; + + var v:Vec4 = new Vec4(); public function new() { super(); - notifyOnInit(init); + notifyOnInit(initNavMesh); + notifyOnUpdate(updateNavMesh); } - function init() { + function initNavMesh() { + if (ready) return; Navigation.active.navMeshes.push(this); - // Load navmesh - var name = "nav_" + cast(object, iron.object.MeshObject).data.name + ".arm"; - Data.getBlob(name, function(b: kha.Blob) { + if(debugDraw) Navigation.active.debugDrawHelper.setDebugMode(DrawWireframe); + + recastNavMesh = new recast.Recast.NavMesh(); + var recastConfig = new recast.Recast.RcConfig(); + + recastConfig.width = width; + recastConfig.height = height; + if(tiledMesh) { + recastConfig.tileSize = tileSize; + } + else { + recastConfig.tileSize = 0; + } + recastConfig.borderSize = borderSize; + recastConfig.cs = cellSize; + recastConfig.ch = cellHeight; + recastConfig.walkableSlopeAngle = walkableSlopeAngle; + recastConfig.walkableHeight = walkableHeight; + recastConfig.walkableClimb = walkableClimb; + recastConfig.walkableRadius = walkableRadius; + recastConfig.maxEdgeLen = maxEdgeLen; + recastConfig.maxSimplificationError = maxSimplificationError; + recastConfig.minRegionArea = minRegionArea; + recastConfig.mergeRegionArea = mergeRegionArea; + recastConfig.maxVertsPerPoly = maxVertsPerPoly; + recastConfig.detailSampleDist = detailSampleDist; + recastConfig.detailSampleMaxError = detailSampleMaxError; + + var positionsArray = new Array(); + var indexArray = new Array(); - recast = Navigation.active.recast; - recast.OBJDataLoader(b.toString(), function() { - var settings = [ - "cellSize" => cellSize, - "cellHeight" => cellHeight, - "agentHeight" => agentHeight, - "agentRadius" => agentRadius, - "agentMaxClimb" => agentMaxClimb, - "agentMaxSlope" => agentMaxSlope, - ]; - recast.settings(settings); + var currentIndexOffset = 0; + var reducedMeshData = getVerticesIndicesFromMesh(object, currentIndexOffset); + currentIndexOffset = reducedMeshData.maxIndex + 1; + positionsArray = positionsArray.concat(reducedMeshData.positions.toArray()); + indexArray = indexArray.concat(reducedMeshData.indices.toArray()); - recast.buildSolo(); - ready = true; - }); - }); + if(combineImmidiateChildren) { + for(child in object.children) { + + if(child.raw.type != "mesh_object") continue; + + reducedMeshData = getVerticesIndicesFromMesh(child, currentIndexOffset); + currentIndexOffset = reducedMeshData.maxIndex + 1; + + positionsArray = positionsArray.concat(reducedMeshData.positions.toArray()); + indexArray = indexArray.concat(reducedMeshData.indices.toArray()); + } + } + + #if js + var positionsVector = Vector.fromArrayCopy(positionsArray); + var vecindVector = Vector.fromArrayCopy(indexArray); + + recastNavMesh.build(positionsVector, positionsVector.length, vecindVector, vecindVector.length, recastConfig); + #else + var posLength = positionsArray.length; + var positionsVector = new recast.Recast.RcFloatArray(posLength); + for(i in 0...posLength) { + positionsVector.set(i, positionsArray[i]); + } + + var indexLength = indexArray.length; + var vecindVector = new recast.Recast.RcIntArray(indexLength); + for(i in 0...indexLength) { + vecindVector.set(i, indexArray[i]); + } + + recastNavMesh.build(positionsVector.raw, posLength, vecindVector.raw, indexLength, recastConfig); + #end + notifyOnUpdate(updateNavMesh); + ready = true; + } + + public function reconstructNavMesh() { + removeUpdate(updateNavMesh); + if(recastCrowd != null) { + for(agent in crowdAgentMap.keys()) { + removeCrowdAgent(agent); + } + recastCrowd.destroy(); + } + for(obstacle in tempObstacleMap.keys()) { + removeTempObstacle(obstacle); + } + ready = false; + initNavMesh(); + } + + public function updateNavMesh() { + if(!ready) return; + recastNavMesh.update(); } public function findPath(from: Vec4, to: Vec4, done: Array->Void) { if (!ready) return; - recast.findPath(from.x, from.z, from.y, to.x, to.z, to.y, 200, function(path: Array) { - var ar: Array = []; - for (p in path) ar.push(new Vec4(p.x, p.z, p.y - cellHeight)); - done(ar); - }); + var start = RecastConversions.recastVec3FromVec4(from); + var end = RecastConversions.recastVec3FromVec4(to); + var navPath = recastNavMesh.computePath(start, end); + + var pathVec = new Array(); + for(i in 0...navPath.getPointCount()) { + pathVec.push(RecastConversions.vec4FromRecastVec3(navPath.getPoint(i))); + } + + done(pathVec); + } + + public function getRandomPointAround(position: Vec4, radius: Float):Vec4 { + if (!ready) return null; + var randomPoint = recastNavMesh.getRandomPointAround(RecastConversions.recastVec3FromVec4(position), radius); + return RecastConversions.vec4FromRecastVec3(randomPoint); } - public function getRandomPoint(done: Vec4->Void) { + public function initCrowd(maxAgents: Int, maxAgentRadius: Float) { if (!ready) return; - recast.getRandomPoint(function(x: Float, y: Float, z: Float) { - done(new Vec4(x, z, -y)); - }); + recastCrowd = new recast.Recast.Crowd(maxAgents, maxAgentRadius, recastNavMesh.getNavMesh()); + notifyOnUpdate(crowdUpdate); } -#end + public function addCrowdAgent(agent: NavCrowd, position: Vec4, radius: Float, height: Float, maxAcceleration: Float, maxSpeed: Float, updateFlags: Int = 7, separationWeight: Float = 1.0, collisionQueryRange: Float = 1.0): Int { + if(!ready) return -1; + if(recastCrowd == null) initCrowd(maxCrowdAgents, maxCrowdAgentRadius); + var crowdAgentParams = new recast.Recast.DtCrowdAgentParams(); + crowdAgentParams.radius = radius; + crowdAgentParams.height = height; + crowdAgentParams.maxAcceleration = maxAcceleration; + crowdAgentParams.maxSpeed = maxSpeed; + crowdAgentParams.separationWeight = separationWeight; + crowdAgentParams.collisionQueryRange = collisionQueryRange; + crowdAgentParams.updateFlags = updateFlags; + crowdAgentParams.pathOptimizationRange=0; + var crowdAgentID = recastCrowd.addAgent(RecastConversions.recastVec3FromVec4(position), crowdAgentParams); + crowdAgentMap.set(crowdAgentID, agent); + return crowdAgentID; + } + + public function removeCrowdAgent(agentID: Int) { + if(!ready) return; + if(recastCrowd == null) return; + crowdAgentMap.remove(agentID); + recastCrowd.removeAgent(agentID); + } + + public function crowdUpdate() { + if(!ready) return; + if(recastCrowd == null) return; + recastCrowd.update(Time.delta); + } + + public function crowdGetAgentPosition(agentID: Int):Vec4 { + if(!ready) return null; + if(recastCrowd == null) return null; + var pos = recastCrowd.getAgentPosition(agentID); + var armPos = RecastConversions.vec4FromRecastVec3(pos); + #if hl + pos.delete(); + #end + return armPos; + } + + public function crowdGetAgentVelocity(agentID: Int):Vec4 { + if(!ready) return null; + if(recastCrowd == null) return null; + var vel = recastCrowd.getAgentVelocity(agentID); + var armVel = RecastConversions.vec4FromRecastVec3(vel); + #if hl + vel.delete(); + #end + return armVel; + } + + public function crowdAgentGoto(agentID: Int, destination: Vec4) { + if(!ready) return; + if(recastCrowd == null) return; + recastCrowd.agentGoto(agentID, RecastConversions.recastVec3FromVec4(destination)); + } + + public function crowdAgentTeleport(agentID: Int, destination: Vec4) { + if(!ready) return; + if(recastCrowd == null) return; + recastCrowd.agentTeleport(agentID, RecastConversions.recastVec3FromVec4(destination)); + } + + public function addCylinderObstacle(navObstacle: NavObstacle, position: Vec4, radius: Float, height: Float): Int { + if(!ready) return -1; + if(!tiledMesh) return -1; + var pos = RecastConversions.recastVec3FromVec4(position); + var obstacle = recastNavMesh.addCylinderObstacle(pos, radius, height); + var obstacleID = tempObstacleCounter; + tempObstacleMap.set(obstacleID, navObstacle); + recastObstacleMap.set(obstacleID, obstacle); + tempObstacleCounter++; + return obstacleID; + } + + public function addBoxObstacle(navObstacle: NavObstacle, position: Vec4, dimensions: Vec4, angle: Float): Int { + if(!ready) return -1; + if(!tiledMesh) return -1; + var pos = RecastConversions.recastVec3FromVec4(position); + var dim = RecastConversions.recastVec3FromVec4(dimensions); + var obstacle = recastNavMesh.addBoxObstacle(pos, dim, angle); + var obstacleID = tempObstacleCounter; + tempObstacleMap.set(obstacleID, navObstacle); + recastObstacleMap.set(obstacleID, obstacle); + tempObstacleCounter++; + return obstacleID; + } + + public function removeTempObstacle(obstacleID: Int) { + if(!ready) return; + if(!tiledMesh) return; + tempObstacleMap.remove(obstacleID); + var obstacleRef = recastObstacleMap.get(obstacleID); + recastNavMesh.removeObstacle(obstacleRef); + recastObstacleMap.remove(obstacleID); + } + + function fromI16(ar: kha.arrays.Int16Array, scalePos: Float): haxe.ds.Vector { + var vals = new haxe.ds.Vector(Std.int(ar.length / 4) * 3); + for (i in 0...Std.int(vals.length / 3)) { + vals[i * 3 ] = (ar[i * 4 ] / 32767) * scalePos; + vals[i * 3 + 1] = (ar[i * 4 + 1] / 32767) * scalePos; + vals[i * 3 + 2] = (ar[i * 4 + 2] / 32767) * scalePos; + } + return vals; + } + + function fromU32(ars: Array): haxe.ds.Vector { + var len = 0; + for (ar in ars) len += ar.length; + var vals = new haxe.ds.Vector(len); + var i = 0; + for (ar in ars) { + for (j in 0...ar.length) { + vals[i] = ar[j]; + i++; + } + } + return vals; + } + + function generateVertexIndexMap(ind: haxe.ds.Vector, vert: haxe.ds.Vector):Map> { + var vertexIndexMap = new Map(); + for (i in 0...ind.length) { + var currentVertex = vert[i]; + var currentIndex = ind[i]; + + var mapping = vertexIndexMap.get(currentVertex); + if (mapping == null) { + vertexIndexMap.set(currentVertex, [currentIndex]); + } + else { + if(! mapping.contains(currentIndex)) mapping.push(currentIndex); + } + } + return vertexIndexMap; + } + + function getVerticesIndicesFromMesh(object: Object, indexOffset:Int = 0): MeshData { + + var vertexIndexMap: Map>; + + var mo = cast(object, MeshObject); + var geom = mo.data.geom; + var rawData = mo.data.raw; + var vertexMap: Array = []; + for (ind in rawData.index_arrays) { + if (ind.vertex_map == null) return null; + vertexMap.push(ind.vertex_map); + } + + var vecind = fromU32(geom.indices); + var vertexMapArray = fromU32(vertexMap); + + vertexIndexMap = generateVertexIndexMap(vecind, vertexMapArray); + + // Parented object - clear parent location + if (object.parent != null && object.parent.name != "") { + object.transform.loc.x += object.parent.transform.worldx(); + object.transform.loc.y += object.parent.transform.worldy(); + object.transform.loc.z += object.parent.transform.worldz(); + object.transform.localOnly = true; + object.transform.buildMatrix(); + } + + var positions = fromI16(geom.positions.values, mo.data.scalePos); + for (i in 0...Std.int(positions.length / 3)) { + v.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); + v.applyQuat(object.transform.rot); + v.x *= object.transform.scale.x; + v.y *= object.transform.scale.y; + v.z *= object.transform.scale.z; + v.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); + positions[i * 3 ] = v.x; + positions[i * 3 + 1] = v.y; + positions[i * 3 + 2] = v.z; + } + + + var vertsLength = 0; + for (key in vertexIndexMap.keys()) vertsLength++; + var positionsVector: haxe.ds.Vector = new haxe.ds.Vector(vertsLength * 3); + for (key in 0...vertsLength) { + var i = vertexIndexMap.get(key)[0]; + // Y and Z are interchanged in Recast + positionsVector.set(key * 3 , positions[i * 3 ]); + positionsVector.set(key * 3 + 1, positions[i * 3 + 2]); + positionsVector.set(key * 3 + 2, positions[i * 3 + 1]); + } + + var vecindVector: haxe.ds.Vector = new haxe.ds.Vector(vertexMapArray.length); + var maxIndex = 0; + for (i in 0...vecindVector.length){ + var idx = vertexMapArray.get(i); + var idxOffset = idx + indexOffset; + vecindVector.set(i, idxOffset); + + if(maxIndex < idxOffset) maxIndex = idxOffset; + } + + return { positions: positionsVector, indices: vecindVector, maxIndex: maxIndex}; + + } + + public function drawDebugMesh(helper: DebugDrawHelper) { + var recastDebugNavMesh = recastNavMesh.getDebugNavMesh(); + var triangleCount = recastDebugNavMesh.getTriangleCount(); + for(index in 0...triangleCount) { + var triangle = recastDebugNavMesh.getTriangle(index); + var point0 = RecastConversions.vec4FromRecastVec3(triangle.getPoint(0)); + var point1 = RecastConversions.vec4FromRecastVec3(triangle.getPoint(1)); + var point2 = RecastConversions.vec4FromRecastVec3(triangle.getPoint(2)); + + helper.drawLine(point0, point1, navMeshDebugColor); + helper.drawLine(point1, point2, navMeshDebugColor); + helper.drawLine(point2, point0, navMeshDebugColor); + + } + #if hl + recastDebugNavMesh.delete(); + #end + } + #end +} + +typedef MeshData = { + var positions: haxe.ds.Vector; + var indices: haxe.ds.Vector; + var maxIndex:Int; } From b3f7283ef5339f6dcad1ef5c649bd2261835cba2 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:15:07 +0200 Subject: [PATCH 071/175] implement nav crowds --- Sources/armory/trait/NavCrowd.hx | 138 ++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/Sources/armory/trait/NavCrowd.hx b/Sources/armory/trait/NavCrowd.hx index b074971e35..69c65d78ce 100644 --- a/Sources/armory/trait/NavCrowd.hx +++ b/Sources/armory/trait/NavCrowd.hx @@ -1,10 +1,146 @@ package armory.trait; +#if arm_navigation +import iron.math.Quat; +import iron.math.Vec4; +import armory.trait.navigation.Navigation; +#end import iron.Trait; class NavCrowd extends Trait { + #if !arm_navigation + public function new() { super(); } + #else + + // Position offset for the agent. + @prop + public var offset: Vec4 = new Vec4(); + + // Radius of the agent. + @prop + public var radius: Float = 1.0; + + // Height of the agent. + @prop + public var height: Float = 1.0; + + // Should the agent turn. + @prop + var turn: Bool = true; + + // Turn rate in range (0, 1). + // 0 = No turn. + // 1 = Instant turn without interpolation. + @prop + public var turnSpeed: Float = 0.1; + + // Threshold to avoid turning at low velocities which might causer jittering. + @prop + public var turnVelocityThreshold: Float = 0.1; + + // Maximum speed of the crowd agent + @prop + public var maxSpeed: Float = 5.0; + + // Maximum acceleration of the agent + @prop + public var maxAcceleration: Float = 100.0; + + // How separated should the agents be. Effective when crowd separation flag is enabled. + @prop + public var separationWeight: Float = 1.0; + + // Distance to check for collisions. Typically should be greater than agent radius. + @prop + public var collisionQueryRange: Float = 2.0; + + // Anticipate turns and modify agent path + @prop + public var anticipateTurns: Bool = false; + + @prop + public var obstacleAvoidance: Bool = false; + + @prop + public var crowdSeparation: Bool = false; + + @prop + public var optimizeVisibility: Bool = false; + + @prop + public var optimizeTopology: Bool = false; + + public var agentReady(default, null) = false; + var activeNavMesh: NavMesh = null; + var agentID = -1; + + static inline var EPSILON = 0.0001; + public function new() { super(); + + notifyOnUpdate(addAgent); + notifyOnRemove(removeAgent); + } + + function addAgent() { + + if(Navigation.active.navMeshes.length < 1) return; + + if(! Navigation.active.navMeshes[0].ready) return; + + activeNavMesh = Navigation.active.navMeshes[0]; + + var flags: Int = 0; + if(anticipateTurns) flags += 1; + if(obstacleAvoidance) flags += 2; + if(crowdSeparation) flags += 4; + if(optimizeVisibility) flags += 8; + if(optimizeTopology) flags += 16; + + var position = object.transform.world.getLoc(); + agentID = activeNavMesh.addCrowdAgent(this, position, radius, height, maxAcceleration, maxSpeed, flags, separationWeight, collisionQueryRange); + + notifyOnUpdate(updateCrowdAgent); + removeUpdate(addAgent); + agentReady = true; + } + + public function crowdAgentGoto(position: Vec4) { + if(!agentReady) return; + + activeNavMesh.crowdAgentGoto(agentID, position); + } + + public function crowdAgentTeleport(position: Vec4) { + if(!agentReady) return; + + activeNavMesh.crowdAgentTeleport(agentID, position); + } + + function removeAgent() { + activeNavMesh.removeCrowdAgent(agentID); + } + + function updateCrowdAgent() { + if(!agentReady) return; + var pos = activeNavMesh.crowdGetAgentPosition(agentID); + pos.add(offset); + object.transform.loc.setFrom(pos); + if(turn) turnAgent(); + object.transform.buildMatrix(); + } + + function turnAgent() { + var vel = activeNavMesh.crowdGetAgentVelocity(agentID); + vel.z = 0; + if(vel.length() < turnVelocityThreshold) return; + vel.normalize(); + var targetRot = new Quat().fromTo(new Vec4(1, 0, 0, 1), vel); + var currentRot = new Quat().setFrom(object.transform.rot); + var res = new Quat().lerp(currentRot, targetRot, turnSpeed); + object.transform.rot = res; } -} + #end +} \ No newline at end of file From 232d8e660be368891e8617aa71d7e768c0212bda Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:15:23 +0200 Subject: [PATCH 072/175] implement nav obstacles --- Sources/armory/trait/NavObstacle.hx | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Sources/armory/trait/NavObstacle.hx diff --git a/Sources/armory/trait/NavObstacle.hx b/Sources/armory/trait/NavObstacle.hx new file mode 100644 index 0000000000..9c5fd7500a --- /dev/null +++ b/Sources/armory/trait/NavObstacle.hx @@ -0,0 +1,60 @@ +package armory.trait; + +#if arm_navigation +import iron.math.Vec4; +import armory.trait.navigation.Navigation; +#end +import iron.Trait; + +class NavObstacle extends Trait { + + #if !arm_navigation + public function new() { super(); } + #else + + @prop + public var radius: Float = 1.0; + + @prop + public var height: Float = 1.0; + + var obstacleID: Int = -1; + + public var obstacleReady (default, null) = false; + + var activeNavMesh: NavMesh = null; + + var initialPosition: Vec4 = new Vec4(); + + public function new() { + super(); + notifyOnUpdate(addObstacle); + notifyOnRemove(removeObstacle); + } + + function addObstacle() { + + if(Navigation.active.navMeshes.length < 1) return; + + if(! Navigation.active.navMeshes[0].ready) return; + + activeNavMesh = Navigation.active.navMeshes[0]; + + initialPosition = object.transform.world.getLoc(); + obstacleID = activeNavMesh.addCylinderObstacle(this, initialPosition, radius, height); + + notifyOnUpdate(updateObstaclePosition); + removeUpdate(addObstacle); + obstacleReady = true; + } + + function removeObstacle() { + activeNavMesh.removeTempObstacle(obstacleID); + } + + function updateObstaclePosition() { + object.transform.loc.setFrom(initialPosition); + object.transform.buildMatrix(); + } + #end +} \ No newline at end of file From decaf1f193d4dc7202a97f04f22a189b36fec681 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:15:41 +0200 Subject: [PATCH 073/175] Implement crowd go to location node --- .../armory/logicnode/CrowdGoToLocationNode.hx | 34 +++++++++++++++++++ .../navmesh/LN_crowd_go_to_location.py | 20 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Sources/armory/logicnode/CrowdGoToLocationNode.hx create mode 100644 blender/arm/logicnode/navmesh/LN_crowd_go_to_location.py diff --git a/Sources/armory/logicnode/CrowdGoToLocationNode.hx b/Sources/armory/logicnode/CrowdGoToLocationNode.hx new file mode 100644 index 0000000000..8da5d996a6 --- /dev/null +++ b/Sources/armory/logicnode/CrowdGoToLocationNode.hx @@ -0,0 +1,34 @@ +package armory.logicnode; + +#if arm_navigation +import armory.trait.navigation.Navigation; +#end + +import iron.object.Object; +import iron.math.Vec4; + +class CrowdGoToLocationNode extends LogicNode { + + var object: Object; + var location: Vec4; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + object = inputs[1].get(); + location = inputs[2].get(); + + assert(Error, object != null, "The object input not be null"); + assert(Error, location != null, "The location to navigate to must not be null"); + +#if arm_navigation + assert(Error, Navigation.active.navMeshes.length > 0, "No Navigation Mesh Present"); + var crowdAgent: armory.trait.NavCrowd = object.getTrait(armory.trait.NavCrowd); + assert(Error, crowdAgent != null, "Object does not have a NavCrowd trait"); + crowdAgent.crowdAgentGoto(location); +#end + runOutput(0); + } +} diff --git a/blender/arm/logicnode/navmesh/LN_crowd_go_to_location.py b/blender/arm/logicnode/navmesh/LN_crowd_go_to_location.py new file mode 100644 index 0000000000..1542ace1b4 --- /dev/null +++ b/blender/arm/logicnode/navmesh/LN_crowd_go_to_location.py @@ -0,0 +1,20 @@ +from arm.logicnode.arm_nodes import * + +class GoToLocationNode(ArmLogicTreeNode): + """Makes a NavCrowd agent go to location. + + @input In: Start navigation. + @input Object: The object to navigate. Object must have `NavCrowd` trait applied. + @input Location: Closest point on the navmesh to navigate to. + """ + bl_idname = 'LNCrowdGoToLocationNode' + bl_label = 'Crowd Go to Location' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmVectorSocket', 'Location') + + self.add_output('ArmNodeSocketAction', 'Out') + From 27441c9194cb9c25bdf05833857682a39d3ec964 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Fri, 29 Sep 2023 23:28:19 +0200 Subject: [PATCH 074/175] remove navmesh preview button --- blender/arm/props_traits.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index de91282963..3734209c86 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -868,11 +868,8 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b # Bundled scripts else: - if item.class_name_prop == 'NavMesh': - row.operator("arm.generate_navmesh", icon="UV_VERTEXSEL") - else: - row.enabled = item.class_name_prop != '' - row.operator("arm.edit_bundled_script", icon_value=ICON_HAXE).is_object = is_object + row.enabled = item.class_name_prop != '' + row.operator("arm.edit_bundled_script", icon_value=ICON_HAXE).is_object = is_object refresh_op = "arm.refresh_object_scripts" if is_object else "arm.refresh_scripts" row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") From b69c7166da725cae54d9d1cca788fbecceb9393d Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:46:40 -0300 Subject: [PATCH 075/175] Add files via upload --- blender/arm/logicnode/event/LN_on_remove.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 blender/arm/logicnode/event/LN_on_remove.py diff --git a/blender/arm/logicnode/event/LN_on_remove.py b/blender/arm/logicnode/event/LN_on_remove.py new file mode 100644 index 0000000000..f6280cc112 --- /dev/null +++ b/blender/arm/logicnode/event/LN_on_remove.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class OnRemoveNode(ArmLogicTreeNode): + """Activates the output when logic tree is removed""" + bl_idname = 'LNOnRemoveNode' + bl_label = 'On Remove' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') From 947f8ad7501c1c3e3ec952ee5aae51d74a54a7f7 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:47:23 -0300 Subject: [PATCH 076/175] Add files via upload --- Sources/armory/logicnode/OnRemoveNode.hx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Sources/armory/logicnode/OnRemoveNode.hx diff --git a/Sources/armory/logicnode/OnRemoveNode.hx b/Sources/armory/logicnode/OnRemoveNode.hx new file mode 100644 index 0000000000..31d7885f6b --- /dev/null +++ b/Sources/armory/logicnode/OnRemoveNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; + +class OnRemoveNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + tree.notifyOnRemove(onRemove); + } + + function onRemove() { + runOutput(0); + } +} From 37bb766ebe349e588819cf167d0ec237b21b493b Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 7 Oct 2023 14:10:33 +0200 Subject: [PATCH 077/175] use billboard corrected normal matrix --- blender/arm/material/make_finalize.py | 10 +++++++++- blender/arm/material/make_mesh.py | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/blender/arm/material/make_finalize.py b/blender/arm/material/make_finalize.py index 7525b517cf..65c77554dc 100644 --- a/blender/arm/material/make_finalize.py +++ b/blender/arm/material/make_finalize.py @@ -1,10 +1,12 @@ import bpy import arm +import arm.material.mat_state as mat_state import arm.material.make_tess as make_tess from arm.material.shader import ShaderContext if arm.is_reload(__name__): + mat_state = arm.reload_module(mat_state) make_tess = arm.reload_module(make_tess) arm.material.shader = arm.reload_module(arm.material.shader) from arm.material.shader import ShaderContext @@ -27,7 +29,13 @@ def make(con_mesh: ShaderContext): # 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') + billboard = mat_state.material.arm_billboard + if billboard == 'spherical': + vert.add_uniform('mat3 N', '_normalMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat3 N', '_normalMatrixCylinder') + else: + vert.add_uniform('mat3 N', '_normalMatrix') vert.write_attrib('wnormal = normalize(N * vec3(nor.xy, pos.w));') frag.write_attrib('vec3 n = normalize(wnormal);') diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 47ca06ea05..21842198fc 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -104,7 +104,13 @@ def make_base(con_mesh, parse_opacity): tesc = None tese = None - vert.add_uniform('mat3 N', '_normalMatrix') + billboard = mat_state.material.arm_billboard + if billboard == 'spherical': + vert.add_uniform('mat3 N', '_normalMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat3 N', '_normalMatrixCylinder') + else: + vert.add_uniform('mat3 N', '_normalMatrix') vert.write_attrib('vec4 spos = vec4(pos.xyz, 1.0);') vattr_written = False From 21e6b061e6d7e68afe3a3edb544e2e3359cd21c8 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 8 Oct 2023 23:57:35 +0200 Subject: [PATCH 078/175] Add new custom tilesheet node group --- blender/data/arm_data.blend | Bin 96980 -> 100127 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/blender/data/arm_data.blend b/blender/data/arm_data.blend index 70634fe10793ecd8a5fcddcb976b2b4eba93be12..76d5f37d0f3cdbb5fb24edd5f633f53fe657d54c 100644 GIT binary patch literal 100127 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 literal 96980 zcmeFY2UOEp)Gs>fD5I#1g`$*BNSp zl+XhN5&%iteR6zCIviL&C)$jqdIJZQuH(f%VT4VO2La|Mj8@w)>BqFPknn z<%Ev-{Z_C8NG&b8{X_7?rP+kvcl`C+Xqs_N!FZ_mjz_2NSFm3d;e3ExpUVadeF6!? zmSh^?GIL09=*Ah!p4u#NjuoRTOK!!0&h$uUA6QlPgyx%Z^4xTgeYlyrLZkD$OX8

g))kcl|kK5x2vH+^W{Zo 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 j0Q=BE#%)&_ugbcazui-&Xkh~V67gLPov= From 432db24dfd61fd5320d6b4b99cc474419d596a68 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 8 Oct 2023 23:58:47 +0200 Subject: [PATCH 079/175] change tilesheet to activeTilesheet --- Sources/armory/logicnode/PauseTilesheetNode.hx | 2 +- Sources/armory/logicnode/PlayTilesheetNode.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/PauseTilesheetNode.hx b/Sources/armory/logicnode/PauseTilesheetNode.hx index 55566edb82..13c29262ae 100644 --- a/Sources/armory/logicnode/PauseTilesheetNode.hx +++ b/Sources/armory/logicnode/PauseTilesheetNode.hx @@ -13,7 +13,7 @@ class PauseTilesheetNode extends LogicNode { if (object == null) return; - object.tilesheet.pause(); + object.activeTilesheet.pause(); runOutput(0); } diff --git a/Sources/armory/logicnode/PlayTilesheetNode.hx b/Sources/armory/logicnode/PlayTilesheetNode.hx index 6c06b94c28..9bd5785a20 100644 --- a/Sources/armory/logicnode/PlayTilesheetNode.hx +++ b/Sources/armory/logicnode/PlayTilesheetNode.hx @@ -14,7 +14,7 @@ class PlayTilesheetNode extends LogicNode { if (object == null) return; - object.tilesheet.play(action, function() { + object.activeTilesheet.play(action, function() { runOutput(1); }); From b24b3cc0d26e8c821189c33db57a134f622a70d2 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 8 Oct 2023 23:59:22 +0200 Subject: [PATCH 080/175] upgrade getTilesheetStateNode --- .../armory/logicnode/GetTilesheetStateNode.hx | 10 ++++--- .../animation/LN_get_tilesheet_state.py | 28 +++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Sources/armory/logicnode/GetTilesheetStateNode.hx b/Sources/armory/logicnode/GetTilesheetStateNode.hx index 65a03bb841..16cba09428 100644 --- a/Sources/armory/logicnode/GetTilesheetStateNode.hx +++ b/Sources/armory/logicnode/GetTilesheetStateNode.hx @@ -13,12 +13,14 @@ class GetTilesheetStateNode extends LogicNode { if (object == null) return null; - var tilesheet = object.tilesheet; + var tilesheet = object.activeTilesheet; return switch (from) { - case 0: tilesheet.action.name; - case 1: tilesheet.frame; - case 2: tilesheet.paused; + case 0: tilesheet.raw.name; + case 1: tilesheet.action.name; + case 2: tilesheet.getFrameOffset(); + case 3: tilesheet.frame; + case 4: tilesheet.paused; default: null; } } diff --git a/blender/arm/logicnode/animation/LN_get_tilesheet_state.py b/blender/arm/logicnode/animation/LN_get_tilesheet_state.py index 09adee812e..4472236344 100644 --- a/blender/arm/logicnode/animation/LN_get_tilesheet_state.py +++ b/blender/arm/logicnode/animation/LN_get_tilesheet_state.py @@ -1,15 +1,37 @@ from arm.logicnode.arm_nodes import * class GetTilesheetStateNode(ArmLogicTreeNode): - """Returns the information about the current tilesheet of the given object.""" + """Returns the information about the current tilesheet of the given object. + + @output Active Tilesheet: Current active tilesheet. + + @output Active Action: Current action in the tilesheet. + + @output Frame: Frame offset with 0 as the first frame of the active action. + + @output Absolute Frame: Absolute frame index in this tilesheet. + + @output Is Paused: Tilesheet action paused. + """ bl_idname = 'LNGetTilesheetStateNode' bl_label = 'Get Tilesheet State' - arm_version = 1 + arm_version = 2 arm_section = 'tilesheet' def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') - self.add_output('ArmStringSocket', 'Name') + self.add_output('ArmStringSocket', 'Active Tilesheet') + self.add_output('ArmStringSocket', 'Active Action') self.add_output('ArmIntSocket', 'Frame') + self.add_output('ArmIntSocket', 'Absolute Frame') self.add_output('ArmBoolSocket', 'Is Paused') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNGetTilesheetStateNode', self.arm_version, 'LNGetTilesheetStateNode', 2, + in_socket_mapping={}, out_socket_mapping={0:1, 1:3, 2:4} + ) From 0de5637f64ea13fb037e7266cbf2108f1c5b8178 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Mon, 9 Oct 2023 00:00:05 +0200 Subject: [PATCH 081/175] Add new SetActiveTilesheetNode --- .../logicnode/SetActiveTilesheetNode.hx | 23 +++++++++++++++++++ .../animation/LN_set_active_tilesheet.py | 16 +++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Sources/armory/logicnode/SetActiveTilesheetNode.hx create mode 100644 blender/arm/logicnode/animation/LN_set_active_tilesheet.py diff --git a/Sources/armory/logicnode/SetActiveTilesheetNode.hx b/Sources/armory/logicnode/SetActiveTilesheetNode.hx new file mode 100644 index 0000000000..0d3eece63b --- /dev/null +++ b/Sources/armory/logicnode/SetActiveTilesheetNode.hx @@ -0,0 +1,23 @@ +package armory.logicnode; + +import iron.Scene; +import iron.object.MeshObject; + +class SetActiveTilesheetNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var tilesheet: String = inputs[2].get(); + var action: String = inputs[3].get(); + + if (object == null) return; + + object.setActiveTilesheet(Scene.active.raw.name, tilesheet, action); + + runOutput(0); + } +} diff --git a/blender/arm/logicnode/animation/LN_set_active_tilesheet.py b/blender/arm/logicnode/animation/LN_set_active_tilesheet.py new file mode 100644 index 0000000000..6bc5246a6b --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_active_tilesheet.py @@ -0,0 +1,16 @@ +from arm.logicnode.arm_nodes import * + +class SetActiveTilesheetNode(ArmLogicTreeNode): + """Set the active tilesheet.""" + bl_idname = 'LNSetActiveTilesheetNode' + bl_label = 'Set Active Tilesheet' + arm_version = 1 + arm_section = 'tilesheet' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmStringSocket', 'Tilesheet') + self.add_input('ArmStringSocket', 'Action') + + self.add_output('ArmNodeSocketAction', 'Out') \ No newline at end of file From 09818b5223b47beb3a29cac824a63b3c5d218f23 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Mon, 9 Oct 2023 00:00:25 +0200 Subject: [PATCH 082/175] Add new SetTilesheetFrameNode --- .../armory/logicnode/SetTilesheetFrameNode.hx | 21 +++++++++++++++++++ .../animation/LN_set_tilesheet_frame.py | 17 +++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 Sources/armory/logicnode/SetTilesheetFrameNode.hx create mode 100644 blender/arm/logicnode/animation/LN_set_tilesheet_frame.py diff --git a/Sources/armory/logicnode/SetTilesheetFrameNode.hx b/Sources/armory/logicnode/SetTilesheetFrameNode.hx new file mode 100644 index 0000000000..27eb54765d --- /dev/null +++ b/Sources/armory/logicnode/SetTilesheetFrameNode.hx @@ -0,0 +1,21 @@ +package armory.logicnode; + +import iron.object.MeshObject; + +class SetTilesheetFrameNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var object: MeshObject = inputs[1].get(); + var frame: Int = inputs[2].get(); + + if (object == null) return; + + object.activeTilesheet.setFrameOffset(frame); + + runOutput(0); + } +} diff --git a/blender/arm/logicnode/animation/LN_set_tilesheet_frame.py b/blender/arm/logicnode/animation/LN_set_tilesheet_frame.py new file mode 100644 index 0000000000..89f133c2da --- /dev/null +++ b/blender/arm/logicnode/animation/LN_set_tilesheet_frame.py @@ -0,0 +1,17 @@ +from arm.logicnode.arm_nodes import * + +class SetTilesheetFrame(ArmLogicTreeNode): + """Set the frame of the current tilesheet action. + @input Frame: Frame offset to set with 0 as the first frame of the active action. + """ + bl_idname = 'LNSetTilesheetFrameNode' + bl_label = 'Set Tilesheet Frame' + arm_version = 1 + arm_section = 'tilesheet' + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmNodeSocketObject', 'Object') + self.add_input('ArmIntSocket', 'Frame') + + self.add_output('ArmNodeSocketAction', 'Out') From 57623383fbccd420effa58f04157abeda5d71c72 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Mon, 9 Oct 2023 00:02:06 +0200 Subject: [PATCH 083/175] fix ShaderDataNode when type is vec2 Blender nodes always uses vec3. Using vec2 in the node caused shader compile errors. --- blender/arm/material/arm_nodes/shader_data_node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blender/arm/material/arm_nodes/shader_data_node.py b/blender/arm/material/arm_nodes/shader_data_node.py index d4f530ee00..4094fd0438 100644 --- a/blender/arm/material/arm_nodes/shader_data_node.py +++ b/blender/arm/material/arm_nodes/shader_data_node.py @@ -84,6 +84,9 @@ def __parse(self, out_socket: NodeSocket, state: ParserState) -> str: state.frag.add_uniform('vec2 screenSize', link='_screenSize') return f'textureLod({self.variable_name}, gl_FragCoord.xy / screenSize, 0.0).rgb' + if self.variable_type == "vec2": + return f'vec3({self.variable_name}.xy, 0)' + return self.variable_name else: From 6922211c22bf93671e495581b2f2280bb3f762d3 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Mon, 9 Oct 2023 00:03:06 +0200 Subject: [PATCH 084/175] Add option to use custom tilesheet node --- blender/arm/exporter.py | 3 ++- blender/arm/props.py | 1 + blender/arm/props_ui.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index b81b1afc1e..bd73697535 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -590,7 +590,8 @@ def create_material_variants(scene: bpy.types.Scene) -> Tuple[List[bpy.types.Mat variant_suffix = '_armskin' # Tilesheets elif bobject.arm_tilesheet != '': - variant_suffix = '_armtile' + if not bobject.arm_use_custom_tilesheet_node: + variant_suffix = '_armtile' elif arm.utils.export_morph_targets(bobject): variant_suffix = '_armskey' diff --git a/blender/arm/props.py b/blender/arm/props.py index 802076a441..d60e300382 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -360,6 +360,7 @@ def init_properties(): 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='') + bpy.types.Object.arm_use_custom_tilesheet_node = BoolProperty(name="Use custom tilesheet node", description="Use custom tilesheet shader node", default=False) # For speakers bpy.types.Speaker.arm_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False) bpy.types.Speaker.arm_loop = BoolProperty(name="Loop", description="Loop this sound", default=False) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index b6a21424a1..fed7645140 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -80,6 +80,7 @@ def draw(self, context): selected_ts = ts break layout.prop_search(obj, "arm_tilesheet_action", selected_ts, "arm_tilesheetactionlist", text="Action") + layout.prop(obj, "arm_use_custom_tilesheet_node") # Properties list arm.props_properties.draw_properties(layout, obj) From c28d7622a37ef76290b1ebb9a1022e90a868db63 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Mon, 9 Oct 2023 00:03:30 +0200 Subject: [PATCH 085/175] import CustomTilesheet shader node group --- blender/arm/handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blender/arm/handlers.py b/blender/arm/handlers.py index 8743a9e770..56579fc37f 100644 --- a/blender/arm/handlers.py +++ b/blender/arm/handlers.py @@ -248,6 +248,9 @@ def reload_blend_data(): armory_pbr = bpy.data.node_groups.get('Armory PBR') if armory_pbr is None: load_library('Armory PBR') + custom_tilesheet = bpy.data.node_groups.get('CustomTilesheet') + if custom_tilesheet is None: + load_library('CustomTilesheet') def load_library(asset_name): From 3463d957032a6f9db13d5cb88c42cd1d98b58cee Mon Sep 17 00:00:00 2001 From: Brah Rah Date: Thu, 12 Oct 2023 13:38:05 +0200 Subject: [PATCH 086/175] added a new logic node called HideActiveCanvas This node hides and unhides the whole active canvas not just certain elements of the canvas. Thus users can easily enable and disable menus for example. I also highly suggest that the LN_set_canvas_visible and LN_get_canvas_visible node are renamed into something like canvas_element_visible. As the node does not actually set the whole canvas visible just parts of it. --- Sources/armory/logicnode/HideActiveCanvas.hx | 21 +++++++++++++++++++ .../logicnode/canvas/LN_HideActiveCanvas.py | 13 ++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 Sources/armory/logicnode/HideActiveCanvas.hx create mode 100644 blender/arm/logicnode/canvas/LN_HideActiveCanvas.py diff --git a/Sources/armory/logicnode/HideActiveCanvas.hx b/Sources/armory/logicnode/HideActiveCanvas.hx new file mode 100644 index 0000000000..6e265b60a6 --- /dev/null +++ b/Sources/armory/logicnode/HideActiveCanvas.hx @@ -0,0 +1,21 @@ +package armory.logicnode; +import armory.trait.internal.CanvasScript; + +class HideActiveCanvas extends LogicNode +{ + + public function new(tree:LogicTree) + { + super(tree); + } + + override function run(from: Int) + { + //get bool from socket + var value = inputs[1].get(); + CanvasScript.getActiveCanvas().setCanvasVisibility(value); + + // Execute next action linked to this node, this activates the output socket at position/index 0 + runOutput(0); + } +} \ No newline at end of file diff --git a/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py b/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py new file mode 100644 index 0000000000..c5691ea312 --- /dev/null +++ b/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class HideActiveCanvas(ArmLogicTreeNode): + """HideActiveCanvas""" + bl_idname = 'LNHideActiveCanvas' + bl_label = 'Hide Active Canvas' + bl_description = 'This node Hides and shows the whole Active Canvas' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.inputs.new('ArmBoolSocket', 'HideCanvas') \ No newline at end of file From e4a674b8717358ae74becc2516c5d6e3d21a2846 Mon Sep 17 00:00:00 2001 From: RPaladin <69180012+rpaladin@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:15:05 -0700 Subject: [PATCH 087/175] Fix typo --- blender/arm/logicnode/canvas/LN_set_canvas_visible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_visible.py b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py index dbe88b26bc..7bbe88b1d4 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_visible.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py @@ -1,7 +1,7 @@ from arm.logicnode.arm_nodes import * class CanvasSetVisibleNode(ArmLogicTreeNode): - """Sets whether the given UI element is visibile.""" + """Sets whether the given UI element is visible.""" bl_idname = 'LNCanvasSetVisibleNode' bl_label = 'Set Canvas Visible' arm_version = 1 From 77c458abb41b0fe93012eacedaaa63301249ed1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:37:31 +0200 Subject: [PATCH 088/175] Multiple HideActiveCanvasNode improvements - Fixed compilation if Zui is disabled - Fixed deprecation warning for setCanvasVisibility() - Fixed Haxe whitespace (spaces -> tabs) and curly braces code style - https://github.com/armory3d/armory/pull/2946#discussion_r1357148471 - Added proper docstring - Made the node more intuitive by using a "visibility" input instead of a "hide" input that still worked like a "visibility" input --- Sources/armory/logicnode/HideActiveCanvas.hx | 28 +++++++++---------- .../logicnode/canvas/LN_HideActiveCanvas.py | 13 ++++++--- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Sources/armory/logicnode/HideActiveCanvas.hx b/Sources/armory/logicnode/HideActiveCanvas.hx index 6e265b60a6..ab177a957c 100644 --- a/Sources/armory/logicnode/HideActiveCanvas.hx +++ b/Sources/armory/logicnode/HideActiveCanvas.hx @@ -1,21 +1,19 @@ package armory.logicnode; + import armory.trait.internal.CanvasScript; -class HideActiveCanvas extends LogicNode -{ +class HideActiveCanvas extends LogicNode { - public function new(tree:LogicTree) - { - super(tree); - } + public function new(tree:LogicTree) { + super(tree); + } - override function run(from: Int) - { - //get bool from socket - var value = inputs[1].get(); - CanvasScript.getActiveCanvas().setCanvasVisibility(value); + override function run(from: Int) { + #if arm_ui + var value: Bool = inputs[1].get(); + CanvasScript.getActiveCanvas().setCanvasVisible(value); + #end - // Execute next action linked to this node, this activates the output socket at position/index 0 - runOutput(0); - } -} \ No newline at end of file + runOutput(0); + } +} diff --git a/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py b/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py index c5691ea312..93bc3610df 100644 --- a/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py +++ b/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py @@ -1,13 +1,18 @@ from arm.logicnode.arm_nodes import * + class HideActiveCanvas(ArmLogicTreeNode): - """HideActiveCanvas""" + """Set whether the active canvas is visible. + + Note that elements of invisible canvases are not rendered and computed, + so it is not possible to interact with those elements on the screen + """ bl_idname = 'LNHideActiveCanvas' - bl_label = 'Hide Active Canvas' - bl_description = 'This node Hides and shows the whole Active Canvas' + bl_label = 'Set Global Canvas Visibility' + bl_width_default = 200 arm_version = 1 def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_output('ArmNodeSocketAction', 'Out') - self.inputs.new('ArmBoolSocket', 'HideCanvas') \ No newline at end of file + self.add_input('ArmBoolSocket', 'Canvas Visible', default_value=True) From 24a7f052daf9cd370eb01465a23a2a8a48ad77cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:48:02 +0200 Subject: [PATCH 089/175] HideActiveCanvas -> SetGlobalCanvasVisibilityNode --- .../{HideActiveCanvas.hx => SetGlobalCanvasVisibilityNode.hx} | 2 +- ...HideActiveCanvas.py => LN_set_global_canvas_visibility.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename Sources/armory/logicnode/{HideActiveCanvas.hx => SetGlobalCanvasVisibilityNode.hx} (84%) rename blender/arm/logicnode/canvas/{LN_HideActiveCanvas.py => LN_set_global_canvas_visibility.py} (84%) diff --git a/Sources/armory/logicnode/HideActiveCanvas.hx b/Sources/armory/logicnode/SetGlobalCanvasVisibilityNode.hx similarity index 84% rename from Sources/armory/logicnode/HideActiveCanvas.hx rename to Sources/armory/logicnode/SetGlobalCanvasVisibilityNode.hx index ab177a957c..45695dce82 100644 --- a/Sources/armory/logicnode/HideActiveCanvas.hx +++ b/Sources/armory/logicnode/SetGlobalCanvasVisibilityNode.hx @@ -2,7 +2,7 @@ package armory.logicnode; import armory.trait.internal.CanvasScript; -class HideActiveCanvas extends LogicNode { +class SetGlobalCanvasVisibilityNode extends LogicNode { public function new(tree:LogicTree) { super(tree); diff --git a/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py similarity index 84% rename from blender/arm/logicnode/canvas/LN_HideActiveCanvas.py rename to blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py index 93bc3610df..e399de896f 100644 --- a/blender/arm/logicnode/canvas/LN_HideActiveCanvas.py +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py @@ -1,13 +1,13 @@ from arm.logicnode.arm_nodes import * -class HideActiveCanvas(ArmLogicTreeNode): +class SetGlobalCanvasVisibilityNode(ArmLogicTreeNode): """Set whether the active canvas is visible. Note that elements of invisible canvases are not rendered and computed, so it is not possible to interact with those elements on the screen """ - bl_idname = 'LNHideActiveCanvas' + bl_idname = 'LNSetGlobalCanvasVisibilityNode' bl_label = 'Set Global Canvas Visibility' bl_width_default = 200 arm_version = 1 From 514a651d610ebf611fb38e53c0014bec525ccfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Sat, 14 Oct 2023 22:08:59 +0200 Subject: [PATCH 090/175] Add menu sections for canvas nodes --- blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_input_text.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_location.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_position.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_rotation.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_scale.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_slider.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_text.py | 2 ++ blender/arm/logicnode/canvas/LN_get_canvas_visible.py | 5 ++--- .../arm/logicnode/canvas/LN_get_global_canvas_font_size.py | 1 + blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py | 1 + blender/arm/logicnode/canvas/LN_on_canvas_element.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_asset.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_color.py | 1 + blender/arm/logicnode/canvas/LN_set_canvas_input_text.py | 2 ++ .../arm/logicnode/canvas/LN_set_canvas_input_text_focus.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_location.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_rotation.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_scale.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_slider.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_text.py | 2 ++ blender/arm/logicnode/canvas/LN_set_canvas_visible.py | 2 ++ .../arm/logicnode/canvas/LN_set_global_canvas_font_size.py | 1 + blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py | 1 + .../arm/logicnode/canvas/LN_set_global_canvas_visibility.py | 1 + blender/arm/logicnode/canvas/__init__.py | 6 ++++++ 29 files changed, 56 insertions(+), 3 deletions(-) diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py b/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py index de3cee1d59..ddbb872670 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_checkbox.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetCheckboxNode(ArmLogicTreeNode): """Returns whether the given UI checkbox is checked.""" bl_idname = 'LNCanvasGetCheckboxNode' bl_label = 'Get Canvas Checkbox' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py b/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py index e8d1a7d378..bf0b59d698 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_input_text.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetInputTextNode(ArmLogicTreeNode): """Returns the input text of the given UI element.""" bl_idname = 'LNCanvasGetInputTextNode' bl_label = 'Get Canvas Input Text' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_location.py b/blender/arm/logicnode/canvas/LN_get_canvas_location.py index 71e9491f93..ffee4cb5f7 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_location.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_location.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetLocationNode(ArmLogicTreeNode): """Returns the location of the given UI element (pixels).""" bl_idname = 'LNCanvasGetLocationNode' bl_label = 'Get Canvas Location' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_position.py b/blender/arm/logicnode/canvas/LN_get_canvas_position.py index 47efa750da..6c50e6025b 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_position.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_position.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetPositionNode(ArmLogicTreeNode): """TO DO.""" bl_idname = 'LNCanvasGetPositionNode' bl_label = 'Get Canvas Position' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py b/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py index 5e9435ca34..83f80ba948 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_progress_bar.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetPBNode(ArmLogicTreeNode): """Returns the value of the given UI progress bar.""" bl_idname = 'LNCanvasGetPBNode' bl_label = 'Get Canvas Progress Bar' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py b/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py index 3b7ffa9ecf..416451051f 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_rotation.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetRotationNode(ArmLogicTreeNode): """Returns the rotation of the given UI element.""" bl_idname = 'LNCanvasGetRotationNode' bl_label = 'Get Canvas Rotation' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_scale.py b/blender/arm/logicnode/canvas/LN_get_canvas_scale.py index 9e65ea3931..8882a4e69b 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_scale.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_scale.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetScaleNode(ArmLogicTreeNode): """Returns the scale of the given UI element.""" bl_idname = 'LNCanvasGetScaleNode' bl_label = 'Get Canvas Scale' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_slider.py b/blender/arm/logicnode/canvas/LN_get_canvas_slider.py index c0d09cc401..57f67e1e69 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_slider.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_slider.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetSliderNode(ArmLogicTreeNode): """Returns the value of the given UI slider.""" bl_idname = 'LNCanvasGetSliderNode' bl_label = 'Get Canvas Slider' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_text.py b/blender/arm/logicnode/canvas/LN_get_canvas_text.py index 8abbcef86a..eb296c4950 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_text.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_text.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasGetTextNode(ArmLogicTreeNode): """Sets the text of the given UI element.""" bl_idname = 'LNCanvasGetTextNode' bl_label = 'Get Canvas Text' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_canvas_visible.py b/blender/arm/logicnode/canvas/LN_get_canvas_visible.py index 3fb4ae1ba9..1579210fab 100644 --- a/blender/arm/logicnode/canvas/LN_get_canvas_visible.py +++ b/blender/arm/logicnode/canvas/LN_get_canvas_visible.py @@ -1,12 +1,11 @@ -import bpy -from bpy.props import * -from bpy.types import Node, NodeSocket from arm.logicnode.arm_nodes import * + class CanvasGetVisibleNode(ArmLogicTreeNode): """Returns whether the given UI element is visible.""" bl_idname = 'LNCanvasGetVisibleNode' bl_label = 'Get Canvas Visible' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py b/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py index a9b1f477a6..be9dd977e7 100644 --- a/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py +++ b/blender/arm/logicnode/canvas/LN_get_global_canvas_font_size.py @@ -5,6 +5,7 @@ class GetGlobalCanvasFontSizeNode(ArmLogicTreeNode): """Returns the font size of the entire UI Canvas.""" bl_idname = 'LNGetGlobalCanvasFontSizeNode' bl_label = 'Get Global Canvas Font Size' + arm_section = 'global' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py b/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py index ca832f21e0..d1cc54bce9 100644 --- a/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py +++ b/blender/arm/logicnode/canvas/LN_get_global_canvas_scale.py @@ -5,6 +5,7 @@ class GetGlobalCanvasScaleNode(ArmLogicTreeNode): """Returns the scale of the entire UI Canvas.""" bl_idname = 'LNGetGlobalCanvasScaleNode' bl_label = 'Get Global Canvas Scale' + arm_section = 'global' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_on_canvas_element.py b/blender/arm/logicnode/canvas/LN_on_canvas_element.py index fe65ad0cb1..8cf803f3e1 100644 --- a/blender/arm/logicnode/canvas/LN_on_canvas_element.py +++ b/blender/arm/logicnode/canvas/LN_on_canvas_element.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class OnCanvasElementNode(ArmLogicTreeNode): """Activates the output whether an action over the given UI element is done.""" bl_idname = 'LNOnCanvasElementNode' bl_label = 'On Canvas Element' + arm_section = 'events' arm_version = 1 property0: HaxeEnumProperty( diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_asset.py b/blender/arm/logicnode/canvas/LN_set_canvas_asset.py index bf734e709c..03a5397ed5 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_asset.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_asset.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetAssetNode(ArmLogicTreeNode): """Sets the asset of the given UI element.""" bl_idname = 'LNCanvasSetAssetNode' bl_label = 'Set Canvas Asset' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py b/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py index ad41a7ae8c..f40548537b 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_checkbox.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetCheckBoxNode(ArmLogicTreeNode): """Sets the state of the given UI checkbox.""" bl_idname = 'LNCanvasSetCheckBoxNode' bl_label = 'Set Canvas Checkbox' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_color.py b/blender/arm/logicnode/canvas/LN_set_canvas_color.py index f5f25aa419..186bb5f111 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_color.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_color.py @@ -17,6 +17,7 @@ class CanvasSetColorNode(ArmLogicTreeNode): """ bl_idname = 'LNCanvasSetColorNode' bl_label = 'Set Canvas Color' + arm_section = 'elements_general' arm_version = 1 property0: HaxeEnumProperty( diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py b/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py index 8140191672..b9b8c85ccd 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_input_text.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetInputTextNode(ArmLogicTreeNode): """Sets the input text of the given UI element.""" bl_idname = 'LNCanvasSetInputTextNode' bl_label = 'Set Canvas Input Text' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py b/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py index a4869c16f4..008660421a 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_input_text_focus.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetInputTextFocusNode(ArmLogicTreeNode): """Sets the input text focus of the given UI element.""" bl_idname = 'LNCanvasSetInputTextFocusNode' bl_label = 'Set Canvas Input Text Focus' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_location.py b/blender/arm/logicnode/canvas/LN_set_canvas_location.py index 034021171d..b1b77ce799 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_location.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_location.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetLocationNode(ArmLogicTreeNode): """Sets the location of the given UI element.""" bl_idname = 'LNCanvasSetLocationNode' bl_label = 'Set Canvas Location' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py b/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py index b5d5908518..d36c26343d 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_progress_bar.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetPBNode(ArmLogicTreeNode): """Sets the value of the given UI progress bar.""" bl_idname = 'LNCanvasSetPBNode' bl_label = 'Set Canvas Progress Bar' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py b/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py index ee9f6502bc..dfd34b49ef 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_rotation.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetRotationNode(ArmLogicTreeNode): """Sets the rotation of the given UI element.""" bl_idname = 'LNCanvasSetRotationNode' bl_label = 'Set Canvas Rotation' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_scale.py b/blender/arm/logicnode/canvas/LN_set_canvas_scale.py index 7afbf7abe0..bf2f89e193 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_scale.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_scale.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetScaleNode(ArmLogicTreeNode): """Sets the scale of the given UI element.""" bl_idname = 'LNCanvasSetScaleNode' bl_label = 'Set Canvas Scale' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_slider.py b/blender/arm/logicnode/canvas/LN_set_canvas_slider.py index 9988266b62..921840f78f 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_slider.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_slider.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetSliderNode(ArmLogicTreeNode): """Sets the value of the given UI slider.""" bl_idname = 'LNCanvasSetSliderNode' bl_label = 'Set Canvas Slider' + arm_section = 'elements_specific' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_text.py b/blender/arm/logicnode/canvas/LN_set_canvas_text.py index 462b7ba2da..d3189e6fdc 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_text.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_text.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetTextNode(ArmLogicTreeNode): """Sets the text of the given UI element.""" bl_idname = 'LNCanvasSetTextNode' bl_label = 'Set Canvas Text' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_canvas_visible.py b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py index 7bbe88b1d4..71c59a3de7 100644 --- a/blender/arm/logicnode/canvas/LN_set_canvas_visible.py +++ b/blender/arm/logicnode/canvas/LN_set_canvas_visible.py @@ -1,9 +1,11 @@ from arm.logicnode.arm_nodes import * + class CanvasSetVisibleNode(ArmLogicTreeNode): """Sets whether the given UI element is visible.""" bl_idname = 'LNCanvasSetVisibleNode' bl_label = 'Set Canvas Visible' + arm_section = 'elements_general' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py index b6d016c419..e3a97ee2c9 100644 --- a/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_font_size.py @@ -5,6 +5,7 @@ class SetGlobalCanvasFontSizeNode(ArmLogicTreeNode): """Sets the font size of the entire UI Canvas.""" bl_idname = 'LNSetGlobalCanvasFontSizeNode' bl_label = 'Set Global Canvas Font Size' + arm_section = 'global' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py index 3a86f84a10..47382623a7 100644 --- a/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_scale.py @@ -5,6 +5,7 @@ class SetGlobalCanvasScaleNode(ArmLogicTreeNode): """Sets the scale of the entire UI Canvas.""" bl_idname = 'LNSetGlobalCanvasScaleNode' bl_label = 'Set Global Canvas Scale' + arm_section = 'global' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py b/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py index e399de896f..86c3b1f4bd 100644 --- a/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py +++ b/blender/arm/logicnode/canvas/LN_set_global_canvas_visibility.py @@ -10,6 +10,7 @@ class SetGlobalCanvasVisibilityNode(ArmLogicTreeNode): bl_idname = 'LNSetGlobalCanvasVisibilityNode' bl_label = 'Set Global Canvas Visibility' bl_width_default = 200 + arm_section = 'global' arm_version = 1 def arm_init(self, context): diff --git a/blender/arm/logicnode/canvas/__init__.py b/blender/arm/logicnode/canvas/__init__.py index e69de29bb2..ee88530348 100644 --- a/blender/arm/logicnode/canvas/__init__.py +++ b/blender/arm/logicnode/canvas/__init__.py @@ -0,0 +1,6 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='events', category='Canvas') +add_node_section(name='elements_general', category='Canvas') +add_node_section(name='elements_specific', category='Canvas') +add_node_section(name='global', category='Canvas') From cefcd1f75712fe28e287dcd6473315f19dc33719 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:55:29 -0300 Subject: [PATCH 091/175] haxe code regex --- .../armory/logicnode/RegularExpressionNode.hx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Sources/armory/logicnode/RegularExpressionNode.hx diff --git a/Sources/armory/logicnode/RegularExpressionNode.hx b/Sources/armory/logicnode/RegularExpressionNode.hx new file mode 100644 index 0000000000..d1a8a399bb --- /dev/null +++ b/Sources/armory/logicnode/RegularExpressionNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; + +class RegularExpressionNode extends LogicNode { + + public var property0: String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var RegExp: EReg = new EReg(inputs[0].get(), inputs[1].get()); + var str: String = inputs[2].get(); + + switch (property0) { + case "Match": + var mch: Bool = RegExp.match(str); + + if (from == 0) + return mch; + + var mched: Array = []; + + if (mch){ + var lng: Int = inputs[0].get().split('(').length; + for(i in 1...lng) mched.push(RegExp.matched(i)); + } + + return mched; + + case "Split": + return RegExp.split(str); + case "Replace": + return RegExp.replace(str, inputs[3].get()); + + } + + return null; + + } +} From bd258e5063113b254deb6ad71af0133f30cb96f3 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:56:20 -0300 Subject: [PATCH 092/175] logic node py --- .../miscellaneous/LN_regular_expression.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 blender/arm/logicnode/miscellaneous/LN_regular_expression.py diff --git a/blender/arm/logicnode/miscellaneous/LN_regular_expression.py b/blender/arm/logicnode/miscellaneous/LN_regular_expression.py new file mode 100644 index 0000000000..b8536cefe7 --- /dev/null +++ b/blender/arm/logicnode/miscellaneous/LN_regular_expression.py @@ -0,0 +1,74 @@ +from arm.logicnode.arm_nodes import * + +class RegularExpressionNode(ArmLogicTreeNode): + """ + The first argument is a string with a regular expression pattern, the second one is a string with flags. + + @input RegExp Pattern: regular expression patterns such as + + . any character + * repeat zero-or-more + + repeat one-or-more + ? optional zero-or-one + [A-Z0-9] character ranges + [^\r\n\t] character not-in-range + (...) parenthesis to match groups of characters + ^ beginning of the string (beginning of a line in multiline matching mode) + $ end of the string (end of a line in multiline matching mode) + | "OR" statement. + + + @input RegExp Flags: possible flags are the following + + i case insensitive matching + g global replace or split, see below + m multiline matching, ^ and $ represent the beginning and end of a line + s the dot . will also match newlines (not supported by C# and JavaScript versions before ES2018) + u use UTF-8 matching (Neko and C++ targets only) + + @input String: String to match, split or replace + @input Replace: String to use when replace + + @ouput Match: boolean result comparing the regular expression pattern with the string + @output Matched: array containing list of matched patterns + @output Split: array string of string splits using the pattern + @output Replace: new string with the pattern replaced + + """ + bl_idname = 'LNRegularExpressionNode' + bl_label = 'Regular Expression' + + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.outputs) > 0: + self.outputs.remove(self.outputs[-1]) + if len(self.inputs) != 3: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Match': + self.add_output('ArmBoolSocket', 'Match') + self.add_output('ArmNodeSocketArray', 'Matched', is_var=False) + if self.property0 == 'Split': + self.add_output('ArmNodeSocketArray', 'Split', is_var=False) + if self.property0 == 'Replace': + self.add_input('ArmStringSocket', 'Replace') + self.add_output('ArmStringSocket', 'String') + + property0: HaxeEnumProperty( + 'property0', + items = [('Match', 'Match', 'A regular expression is used to compare a string. Use () in the pattern to retrieve Matched groups'), + ('Split', 'Split', 'A regular expression can also be used to split a string into several substrings'), + ('Replace', 'Replace', 'A regular expression can also be used to replace a part of the string')], + name='', default='Match', update=remove_extra_inputs) + + def arm_init(self, context): + + self.add_input('ArmStringSocket', 'RegExp Pattern') + self.add_input('ArmStringSocket', 'RegExp Flags') + self.add_input('ArmStringSocket', 'String') + + self.add_output('ArmBoolSocket', 'Match') + self.add_output('ArmNodeSocketArray', 'Matched', is_var=False) + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') \ No newline at end of file From bb0fa187c27d322ccce39fc2b9d07a44f02701f6 Mon Sep 17 00:00:00 2001 From: 1k8 Date: Wed, 18 Oct 2023 03:35:55 -0400 Subject: [PATCH 093/175] Update LN_network_host.py Remove update replacement for version 1 node --- blender/arm/logicnode/network/LN_network_host.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/blender/arm/logicnode/network/LN_network_host.py b/blender/arm/logicnode/network/LN_network_host.py index 833f82c0bc..45ecdcac37 100644 --- a/blender/arm/logicnode/network/LN_network_host.py +++ b/blender/arm/logicnode/network/LN_network_host.py @@ -55,16 +55,3 @@ def update_sockets(self, context): def draw_buttons(self, context, layout): layout.prop(self, 'property0') - def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 1): - raise LookupError() - - in_socket_mapping={0:0, 1:1, 2:2, 3:3} - if self.property0: - in_socket_mapping.update({4:4, 5:5}) - - return NodeReplacement( - 'LNNetworkHostNode', self.arm_version, 'LNNetworkHostNode', 4, - in_socket_mapping=in_socket_mapping, - out_socket_mapping={0:0, 1:1}, - property_mapping={'property0':'property0'}) From 24e70c7a574ebc8d40c817a2c7534636db5586ef Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 22 Oct 2023 11:23:58 -0300 Subject: [PATCH 094/175] haxe code --- Sources/armory/logicnode/WaitForNode.hx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Sources/armory/logicnode/WaitForNode.hx diff --git a/Sources/armory/logicnode/WaitForNode.hx b/Sources/armory/logicnode/WaitForNode.hx new file mode 100644 index 0000000000..bbc1edbb1c --- /dev/null +++ b/Sources/armory/logicnode/WaitForNode.hx @@ -0,0 +1,18 @@ +package armory.logicnode; + +class WaitForNode extends LogicNode { + + var froms: Array = []; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + if(!froms.contains(from)) froms.push(from); + if(inputs.length == froms.length) runOutput(0); + + } + +} From e4aab712954d18a132889c0cc5aa6c8ff6f46a19 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 22 Oct 2023 11:25:00 -0300 Subject: [PATCH 095/175] py node --- blender/arm/logicnode/logic/LN_wait_for.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 blender/arm/logicnode/logic/LN_wait_for.py diff --git a/blender/arm/logicnode/logic/LN_wait_for.py b/blender/arm/logicnode/logic/LN_wait_for.py new file mode 100644 index 0000000000..2279071bfa --- /dev/null +++ b/blender/arm/logicnode/logic/LN_wait_for.py @@ -0,0 +1,25 @@ +from arm.logicnode.arm_nodes import * + +class WaitForNode(ArmLogicTreeNode): + """Activates the output when all inputs are received.""" + bl_idname = 'LNWaitForNode' + bl_label = 'Wait for Input' + arm_section = 'flow' + arm_version = 1 + + def __init__(self): + super(WaitForNode, self).__init__() + array_nodes[str(id(self))] = self + + def arm_init(self, context): + self.add_output('ArmNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + 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' + op2 = row.operator('arm.node_remove_input', text='', icon='X', emboss=True) + op2.node_index = str(id(self)) + From 34add679861be61da2b3e3a6eaf6582d075876c6 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:50:44 -0300 Subject: [PATCH 096/175] add reset to froms --- Sources/armory/logicnode/WaitForNode.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/WaitForNode.hx b/Sources/armory/logicnode/WaitForNode.hx index bbc1edbb1c..99acf63565 100644 --- a/Sources/armory/logicnode/WaitForNode.hx +++ b/Sources/armory/logicnode/WaitForNode.hx @@ -11,7 +11,7 @@ class WaitForNode extends LogicNode { override function run(from: Int) { if(!froms.contains(from)) froms.push(from); - if(inputs.length == froms.length) runOutput(0); + if(inputs.length == froms.length){ runOutput(0); froms = []; } } From cd102d9948e9618120b0b4f9160e0c24571c80a8 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:57:57 -0300 Subject: [PATCH 097/175] Update LN_wait_for.py --- blender/arm/logicnode/logic/LN_wait_for.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/blender/arm/logicnode/logic/LN_wait_for.py b/blender/arm/logicnode/logic/LN_wait_for.py index 2279071bfa..0361e3d165 100644 --- a/blender/arm/logicnode/logic/LN_wait_for.py +++ b/blender/arm/logicnode/logic/LN_wait_for.py @@ -1,7 +1,13 @@ from arm.logicnode.arm_nodes import * class WaitForNode(ArmLogicTreeNode): - """Activates the output when all inputs are received.""" + """ + Activates the output when all inputs are received disregaring its order. The idea + is to wait for all inputs to be received to trigger the output. + + @input list of inputs to wait for triggering the output + @output is triggered when all inputs are received + """ bl_idname = 'LNWaitForNode' bl_label = 'Wait for Input' arm_section = 'flow' From 2c32ba469a437c61473d622cca3d5897b50f28f5 Mon Sep 17 00:00:00 2001 From: e2002e Date: Sun, 22 Oct 2023 21:35:27 +0200 Subject: [PATCH 098/175] use a constant for the ssrStep. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index b34bb333f6..8ecdef2049 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -25,7 +25,7 @@ vec3 hitCoord; float depth; const int numBinarySearchSteps = 7; -#define maxSteps int(ceil(1.0 / ssrRayStep) * ssrSearchDist) +const int maxSteps = int(ceil(1.0 / ssrRayStep) * ssrSearchDist); vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); From bc9055aa43f74c9629d38d5e469b67ca0a6f275d Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:55:54 -0300 Subject: [PATCH 099/175] Update LN_wait_for.py --- blender/arm/logicnode/logic/LN_wait_for.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blender/arm/logicnode/logic/LN_wait_for.py b/blender/arm/logicnode/logic/LN_wait_for.py index 0361e3d165..84a22f6a35 100644 --- a/blender/arm/logicnode/logic/LN_wait_for.py +++ b/blender/arm/logicnode/logic/LN_wait_for.py @@ -2,14 +2,14 @@ class WaitForNode(ArmLogicTreeNode): """ - Activates the output when all inputs are received disregaring its order. The idea - is to wait for all inputs to be received to trigger the output. + Activate the output when all inputs have been activated at least once since the node's initialization. + Use This node for parallel flows. Inputs don't need to be active at the same point in time. - @input list of inputs to wait for triggering the output - @output is triggered when all inputs are received + @input Input[0-n]: list of inputs to be activated + @output Output: output triggered when all inputs are activated """ bl_idname = 'LNWaitForNode' - bl_label = 'Wait for Input' + bl_label = 'Wait for All Inputs' arm_section = 'flow' arm_version = 1 From d8cd051aad1a80cacd81ca9847f2ec18f6b76876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:23:51 +0200 Subject: [PATCH 100/175] Fix compilation when using shadow map atlas of size 32768 --- Sources/armory/renderpath/Inc.hx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/armory/renderpath/Inc.hx b/Sources/armory/renderpath/Inc.hx index 2eef0b2eda..a1a468b06b 100644 --- a/Sources/armory/renderpath/Inc.hx +++ b/Sources/armory/renderpath/Inc.hx @@ -751,6 +751,8 @@ class ShadowMapAtlas { return 8192; #elseif (rp_shadowmap_atlas_max_size == 16384) return 16384; + #elseif (rp_shadowmap_atlas_max_size == 32768) + return 32768; #end #else switch (type) { @@ -765,6 +767,8 @@ class ShadowMapAtlas { return 8192; #elseif (rp_shadowmap_atlas_max_size_point == 16384) return 16384; + #elseif (rp_shadowmap_atlas_max_size_point == 32768) + return 32768; #end } case "spot": { @@ -780,6 +784,8 @@ class ShadowMapAtlas { return 8192; #elseif (rp_shadowmap_atlas_max_size_spot == 16384) return 16384; + #elseif (rp_shadowmap_atlas_max_size_spot == 32768) + return 32768; #end } case "sun": { @@ -795,6 +801,8 @@ class ShadowMapAtlas { return 8192; #elseif (rp_shadowmap_atlas_max_size_sun == 16384) return 16384; + #elseif (rp_shadowmap_atlas_max_size_sun == 32768) + return 32768; #end } default: { @@ -810,9 +818,10 @@ class ShadowMapAtlas { return 8192; #elseif (rp_shadowmap_atlas_max_size == 16384) return 16384; + #elseif (rp_shadowmap_atlas_max_size == 32768) + return 32768; #end } - } #end } From 1ba0bd24598187a4ceecf44aaeb6a710002516d3 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:07:10 -0300 Subject: [PATCH 101/175] Update LN_world_to_screen_space.py --- .../math/LN_world_to_screen_space.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/blender/arm/logicnode/math/LN_world_to_screen_space.py b/blender/arm/logicnode/math/LN_world_to_screen_space.py index b3f16b6a26..3e0f27b185 100644 --- a/blender/arm/logicnode/math/LN_world_to_screen_space.py +++ b/blender/arm/logicnode/math/LN_world_to_screen_space.py @@ -1,13 +1,35 @@ from arm.logicnode.arm_nodes import * class WorldToScreenSpaceNode(ArmLogicTreeNode): - """Transforms the given world coordinates into screen coordinates.""" + """Transforms the given world coordinates into screen coordinates, + using the active camera or a selected camera.""" bl_idname = 'LNWorldToScreenSpaceNode' bl_label = 'World to Screen Space' arm_section = 'matrix' - arm_version = 1 + arm_version = 2 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Selected Camera': + self.add_input('ArmNodeSocketObject', 'Camera') + + property0: HaxeEnumProperty( + 'property0', + items = [('Active Camera', 'Active Camera', 'Active Camera'), + ('Selected Camera', 'Selected Camera', 'Selected Camera')], + name='', default='Active Camera', update=remove_extra_inputs) def arm_init(self, context): self.add_input('ArmVectorSocket', 'World') self.add_output('ArmVectorSocket', 'Screen') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement.Identity(self) From 4b4d235ec8951d632a404fa8f79eae30d2642737 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:09:42 -0300 Subject: [PATCH 102/175] Update WorldToScreenSpaceNode.hx --- Sources/armory/logicnode/WorldToScreenSpaceNode.hx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/WorldToScreenSpaceNode.hx b/Sources/armory/logicnode/WorldToScreenSpaceNode.hx index 6ca33573aa..d0f075c886 100644 --- a/Sources/armory/logicnode/WorldToScreenSpaceNode.hx +++ b/Sources/armory/logicnode/WorldToScreenSpaceNode.hx @@ -3,11 +3,13 @@ package armory.logicnode; import iron.math.Vec4; import iron.math.Vec2; import iron.App; +import iron.object.CameraObject; class WorldToScreenSpaceNode extends LogicNode { public var property0: String; var v = new Vec4(); + var cam: CameraObject; public function new(tree: LogicTree) { super(tree); @@ -17,7 +19,11 @@ class WorldToScreenSpaceNode extends LogicNode { var v1: Vec4 = inputs[0].get(); if (v1 == null) return null; - var cam = iron.Scene.active.camera; + if(property0 == 'Active Camera') + cam = iron.Scene.active.camera; + else + cam = inputs[1].get(); + v.setFrom(v1); v.applyproj(cam.V); v.applyproj(cam.P); From cc5a9ffe4de63a95dc7cc3047a24137522844fd2 Mon Sep 17 00:00:00 2001 From: e2002e Date: Fri, 27 Oct 2023 15:58:19 +0200 Subject: [PATCH 103/175] Forward rp don't use GBUF_SIZE. normalizing the whole reflect / refract functions. --- .../custom_mat_deferred.frag.glsl | 2 +- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 4 +- blender/arm/make_renderpath.py | 2 +- blender/arm/material/make_mesh.py | 18 ++++---- blender/arm/write_data.py | 41 ++++++++++--------- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl index d93a2903a0..3eae02fdef 100644 --- a/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl +++ b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl @@ -50,6 +50,6 @@ void main() { #endif #ifdef _SSRefraction - fragColor[GBUF_IDX_REFRACTION] = vec4(1.0); + fragColor[GBUF_IDX_REFRACTION] = vec4(ior, opacity, 0.0, 0.0); #endif } diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index ba193cb399..8ec5889574 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -91,7 +91,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 reflected = reflect(normalize(viewPos), viewNormal); + vec3 reflected = normalize(reflect(viewPos, viewNormal)); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 2088b8fcb2..cd8063afad 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -27,7 +27,7 @@ vec3 hitCoord; float depth; vec3 viewPos; -#define maxSteps int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist) +const int maxSteps = int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist); vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); @@ -100,7 +100,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); - vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); + vec3 refracted = normalize(refract(viewPos, viewNormal, 1.0 / ior)); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/blender/arm/make_renderpath.py b/blender/arm/make_renderpath.py index 6654cfd3f4..12b81cb504 100644 --- a/blender/arm/make_renderpath.py +++ b/blender/arm/make_renderpath.py @@ -438,7 +438,7 @@ def build(): callback() -def get_num_gbuffer_rts()-> int: +def get_num_gbuffer_rts_deferred()-> int: """Return the number of render targets required for the G-Buffer.""" wrd = bpy.data.worlds['Arm'] diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 5111427261..738fae5409 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -536,20 +536,24 @@ def make_forward(con_mesh): frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) if not blend: - mrt = rpdat.rp_ssr or rpdat.rp_ss_refraction - if mrt: + mrt = 0 + if rpdat.rp_ssr: + mrt = 2 + if rpdat.rp_ss_refraction: + mrt += 1 + if mrt != 0: # Store light gbuffer for post-processing - frag.add_out('vec4 fragColor[GBUF_SIZE]') + frag.add_out('vec4 fragColor[{mrt}]'.format(mrt = mrt)) frag.add_include('std/gbuffer.glsl') frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') - frag.write('fragColor[GBUF_IDX_0] = vec4(direct + indirect, packFloat2(occlusion, specular));') - frag.write('fragColor[GBUF_IDX_1] = vec4(n.xy, roughness, metallic);') + frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') + frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') if '_SSRefraction' in wrd.world_defs: if parse_opacity: - frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(rior, opacity, 0.0, 0.0);') + frag.write('fragColor[2] = vec4(rior, opacity, 0.0, 0.0);') else: - frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 0.0);') + frag.write('fragColor[2] = vec4(1.0, 1.0, 0.0, 0.0);') else: frag.add_out('vec4 fragColor[1]') diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index 3a38093b9d..0f54a61051 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -573,26 +573,27 @@ def write_compiledglsl(defs, make_variants): continue # Write a shader variant instead f.write("#define " + d + "\n") - gbuffer_size = make_renderpath.get_num_gbuffer_rts() - f.write(f'#define GBUF_SIZE {gbuffer_size}\n') - - # Write indices of G-Buffer render targets - f.write('#define GBUF_IDX_0 0\n') - f.write('#define GBUF_IDX_1 1\n') - - idx_emission = 2 - idx_refraction = 2 - if '_gbuffer2' in wrd.world_defs: - f.write('#define GBUF_IDX_2 2\n') - idx_emission += 1 - idx_refraction += 1 - - if '_EmissionShaded' in wrd.world_defs: - f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') - idx_refraction += 1 - - if '_SSRefraction' in wrd.world_defs: - f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') + if rpdat.rp_renderer == "Deferred": + gbuffer_size = make_renderpath.get_num_gbuffer_rts_deferred() + f.write(f'#define GBUF_SIZE {gbuffer_size}\n') + + # Write indices of G-Buffer render targets + f.write('#define GBUF_IDX_0 0\n') + f.write('#define GBUF_IDX_1 1\n') + + idx_emission = 2 + idx_refraction = 2 + if '_gbuffer2' in wrd.world_defs: + f.write('#define GBUF_IDX_2 2\n') + idx_emission += 1 + idx_refraction += 1 + + if '_EmissionShaded' in wrd.world_defs: + f.write(f'#define GBUF_IDX_EMISSION {idx_emission}\n') + idx_refraction += 1 + + if '_SSRefraction' in wrd.world_defs: + f.write(f'#define GBUF_IDX_REFRACTION {idx_refraction}\n') f.write("""#if defined(HLSL) || defined(METAL) #define _InvY From d5809cd1b33b3d6b645eaefaf8317241482b2ef3 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 29 Oct 2023 19:37:50 +0100 Subject: [PATCH 104/175] fix compilation --- Sources/armory/logicnode/ResumeTilesheetNode.hx | 2 +- Sources/armory/logicnode/SetTilesheetPausedNode.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/armory/logicnode/ResumeTilesheetNode.hx b/Sources/armory/logicnode/ResumeTilesheetNode.hx index fbb7b8cb5a..5712675c38 100644 --- a/Sources/armory/logicnode/ResumeTilesheetNode.hx +++ b/Sources/armory/logicnode/ResumeTilesheetNode.hx @@ -13,7 +13,7 @@ class ResumeTilesheetNode extends LogicNode { if (object == null) return; - object.tilesheet.resume(); + object.activeTilesheet.resume(); runOutput(0); } diff --git a/Sources/armory/logicnode/SetTilesheetPausedNode.hx b/Sources/armory/logicnode/SetTilesheetPausedNode.hx index 6b1d3cde84..e267b71995 100644 --- a/Sources/armory/logicnode/SetTilesheetPausedNode.hx +++ b/Sources/armory/logicnode/SetTilesheetPausedNode.hx @@ -14,7 +14,7 @@ class SetTilesheetPausedNode extends LogicNode { if (object == null) return; - paused ? object.tilesheet.pause() : object.tilesheet.resume(); + paused ? object.activeTilesheet.pause() : object.activeTilesheet.resume(); runOutput(0); } From 90fb3ea8fe733a6d7d7e4016e54debd26e7dad0d Mon Sep 17 00:00:00 2001 From: e2002e Date: Mon, 30 Oct 2023 11:58:26 +0100 Subject: [PATCH 105/175] normalize getViewPos in getDeltaDepth. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 8ec5889574..9c13c39014 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -38,7 +38,7 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); return viewPos.z - hit.z; } diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index cd8063afad..eb0773644d 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -41,7 +41,7 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); return viewPos.z - hit.z; } From 65ad3802fd5dd6a4a1e75e38def34f2549b81238 Mon Sep 17 00:00:00 2001 From: e2002e Date: Mon, 30 Oct 2023 12:00:38 +0100 Subject: [PATCH 106/175] keep depth variable global --- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index eb0773644d..728a150436 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -40,7 +40,7 @@ vec2 getProjectedCoord(const vec3 hit) { } float getDeltaDepth(const vec3 hit) { - float depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); return viewPos.z - hit.z; } From 2b6eb113202226f7b37370a8d21f4ec639865341 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Wed, 1 Nov 2023 12:32:45 +0100 Subject: [PATCH 107/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index d60e300382..605f7fc411 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.10' +arm_version = '2023.11' arm_commit = '$Id$' def get_project_html5_copy(self): From b06974c95347d74da4d71ff9552d5c6900b9b338 Mon Sep 17 00:00:00 2001 From: e2002e Date: Wed, 1 Nov 2023 14:06:07 +0100 Subject: [PATCH 108/175] fix leaks in reflection (as it was introduced on this branch). --- Shaders/ssr_pass/ssr_pass.frag.glsl | 8 +++---- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 4 ++-- Shaders/std/gbuffer.glsl | 2 +- .../armory/renderpath/RenderPathDeferred.hx | 24 +++++++++++++++---- .../math/LN_world_to_screen_space.py | 2 +- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 9c13c39014..015adc713c 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -38,7 +38,7 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); + vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } @@ -80,8 +80,8 @@ void main() { float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } - float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; - if (d == 1.0) { fragColor.rgb = vec3(0.0); return; } + depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (depth == 1.0) { fragColor.rgb = vec3(0.0); return; } vec2 enc = g0.rg; vec3 n; @@ -90,7 +90,7 @@ void main() { n = normalize(n); vec3 viewNormal = V3 * n; - vec3 viewPos = getPosView(viewRay, d, cameraProj); + vec3 viewPos = getPosView(viewRay, depth, cameraProj); vec3 reflected = normalize(reflect(viewPos, viewNormal)); hitCoord = viewPos; diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 728a150436..612db5cb7c 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -41,7 +41,7 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); + vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } @@ -101,7 +101,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); vec3 refracted = normalize(refract(viewPos, viewNormal, 1.0 / ior)); - hitCoord = viewPos; + hitCoord = -viewPos; #ifdef _CPostprocess vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; diff --git a/Shaders/std/gbuffer.glsl b/Shaders/std/gbuffer.glsl index dc424e3eae..07652a50bf 100644 --- a/Shaders/std/gbuffer.glsl +++ b/Shaders/std/gbuffer.glsl @@ -15,7 +15,7 @@ vec3 getNor(const vec2 enc) { vec3 getPosView(const vec3 viewRay, const float depth, const vec2 cameraProj) { float linearDepth = cameraProj.y / (cameraProj.x - depth); - // float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); + //float linearDepth = cameraProj.y / ((depth * 0.5 + 0.5) - cameraProj.x); return viewRay * linearDepth; } diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 48b65d0e2e..96feac681d 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -752,6 +752,10 @@ class RenderPathDeferred { path.bindTarget("_main", "gbufferD"); path.bindTarget("gbuffer0", "gbuffer0"); path.drawShader("shader_datas/sss_pass/sss_pass_y"); + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); + #end } #end @@ -759,6 +763,10 @@ class RenderPathDeferred { { if (armory.data.Config.raw.rp_ssrefr != false) { + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer1"); // Unbind depth so we can read it + #end + //save depth path.setTarget("gbufferD1"); path.bindTarget("_main", "tex"); @@ -844,17 +852,21 @@ class RenderPathDeferred { path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); + #end } } #end - #if (!kha_opengl) - path.setDepthFrom("tex", "gbuffer0"); // Re-bind depth - #end - #if rp_ssr { if (armory.data.Config.raw.rp_ssr != false) { + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer1"); // Unbind depth so we can read it + #end + #if rp_ssr_half var targeta = "ssra"; var targetb = "ssrb"; @@ -884,6 +896,10 @@ class RenderPathDeferred { path.bindTarget(targetb, "tex"); path.bindTarget("gbuffer0", "gbuffer0"); path.drawShader("shader_datas/blur_adaptive_pass/blur_adaptive_pass_y3_blend"); + + #if (!kha_opengl) + path.setDepthFrom("tex", "gbuffer0"); + #end } } #end diff --git a/blender/arm/logicnode/math/LN_world_to_screen_space.py b/blender/arm/logicnode/math/LN_world_to_screen_space.py index 3e0f27b185..5aa1947523 100644 --- a/blender/arm/logicnode/math/LN_world_to_screen_space.py +++ b/blender/arm/logicnode/math/LN_world_to_screen_space.py @@ -8,7 +8,7 @@ class WorldToScreenSpaceNode(ArmLogicTreeNode): arm_section = 'matrix' arm_version = 2 - def remove_extra_inputs(self, context): + def remove_extra_inputs(self, context): while len(self.inputs) > 1: self.inputs.remove(self.inputs[-1]) if self.property0 == 'Selected Camera': From b1bcf3b62d361d4f1ef85919ea3369585599f917 Mon Sep 17 00:00:00 2001 From: e2002e Date: Wed, 1 Nov 2023 14:11:21 +0100 Subject: [PATCH 109/175] don't normalize refract / reflect. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 015adc713c..06ce4dcff3 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -91,7 +91,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); - vec3 reflected = normalize(reflect(viewPos, viewNormal)); + vec3 reflected = reflect(viewPos, viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 612db5cb7c..03b5e038ca 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -100,7 +100,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); - vec3 refracted = normalize(refract(viewPos, viewNormal, 1.0 / ior)); + vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); hitCoord = -viewPos; #ifdef _CPostprocess From 0d1101b8b0b1094afd01d6c51dbb9eeec60215e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:15:11 +0100 Subject: [PATCH 110/175] [Hotfix/release broken] Fix Python indentation error --- blender/arm/logicnode/math/LN_world_to_screen_space.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/logicnode/math/LN_world_to_screen_space.py b/blender/arm/logicnode/math/LN_world_to_screen_space.py index 3e0f27b185..330678a373 100644 --- a/blender/arm/logicnode/math/LN_world_to_screen_space.py +++ b/blender/arm/logicnode/math/LN_world_to_screen_space.py @@ -8,7 +8,7 @@ class WorldToScreenSpaceNode(ArmLogicTreeNode): arm_section = 'matrix' arm_version = 2 - def remove_extra_inputs(self, context): + def remove_extra_inputs(self, context): while len(self.inputs) > 1: self.inputs.remove(self.inputs[-1]) if self.property0 == 'Selected Camera': @@ -24,12 +24,12 @@ def arm_init(self, context): self.add_input('ArmVectorSocket', 'World') self.add_output('ArmVectorSocket', 'Screen') - + def draw_buttons(self, context, layout): layout.prop(self, 'property0') def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.arm_version not in (0, 1): raise LookupError() - + return NodeReplacement.Identity(self) From f08461e4adcec49458c81adbc828f8e2c61a837e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:48:28 +0100 Subject: [PATCH 111/175] Regular Expression node: fix docstring for wiki generation --- .../miscellaneous/LN_regular_expression.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/blender/arm/logicnode/miscellaneous/LN_regular_expression.py b/blender/arm/logicnode/miscellaneous/LN_regular_expression.py index b8536cefe7..a55906697a 100644 --- a/blender/arm/logicnode/miscellaneous/LN_regular_expression.py +++ b/blender/arm/logicnode/miscellaneous/LN_regular_expression.py @@ -5,31 +5,28 @@ class RegularExpressionNode(ArmLogicTreeNode): The first argument is a string with a regular expression pattern, the second one is a string with flags. @input RegExp Pattern: regular expression patterns such as - - . any character - * repeat zero-or-more - + repeat one-or-more - ? optional zero-or-one - [A-Z0-9] character ranges - [^\r\n\t] character not-in-range - (...) parenthesis to match groups of characters - ^ beginning of the string (beginning of a line in multiline matching mode) - $ end of the string (end of a line in multiline matching mode) - | "OR" statement. - + - `.`: any character + - `*`: repeat zero-or-more + - `+`: repeat one-or-more + - `?`: optional zero-or-one + - `[A-Z0-9]`: character ranges + - `[^\\r\\n\\t]`: character not-in-range + - `(...)`: parenthesis to match groups of characters + - `^`: beginning of the string (beginning of a line in multiline matching mode) + - `$`: end of the string (end of a line in multiline matching mode) + - `|`: "OR" statement. @input RegExp Flags: possible flags are the following - - i case insensitive matching - g global replace or split, see below - m multiline matching, ^ and $ represent the beginning and end of a line - s the dot . will also match newlines (not supported by C# and JavaScript versions before ES2018) - u use UTF-8 matching (Neko and C++ targets only) + - `i`: case insensitive matching + - `g`: global replace or split, see below + - `m`: multiline matching, ^ and $ represent the beginning and end of a line + - `s`: the dot . will also match newlines (not supported by C# and JavaScript versions before ES2018) + - `u`: use UTF-8 matching (Neko and C++ targets only) @input String: String to match, split or replace - @input Replace: String to use when replace + @input Replace: String to use when replace - @ouput Match: boolean result comparing the regular expression pattern with the string + @output Match: boolean result comparing the regular expression pattern with the string @output Matched: array containing list of matched patterns @output Split: array string of string splits using the pattern @output Replace: new string with the pattern replaced @@ -66,9 +63,9 @@ def arm_init(self, context): self.add_input('ArmStringSocket', 'RegExp Pattern') self.add_input('ArmStringSocket', 'RegExp Flags') self.add_input('ArmStringSocket', 'String') - + self.add_output('ArmBoolSocket', 'Match') self.add_output('ArmNodeSocketArray', 'Matched', is_var=False) def draw_buttons(self, context, layout): - layout.prop(self, 'property0') \ No newline at end of file + layout.prop(self, 'property0') From a933ea150ef4c68b68ecf80b4558925484a608e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:23:31 +0100 Subject: [PATCH 112/175] Show object properties in debug console --- Sources/armory/trait/internal/DebugConsole.hx | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 1574dd04af..442baba536 100644 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -502,6 +502,19 @@ class DebugConsole extends Trait { ui.unindent(); } + if (selectedObject.properties != null) { + ui.text("Properties:"); + ui.indent(); + + for (name => value in selectedObject.properties) { + ui.row([1/2, 1/2]); + ui.text(name); + ui.text(dynamicToUIString(value), Align.Right); + } + + ui.unindent(); + } + if (selectedObject.name == "Scene") { selectedType = "(Scene)"; if (iron.Scene.active.world != null) { @@ -936,21 +949,8 @@ class DebugConsole extends Trait { ui.text(fieldName + ""); var fieldValue = Reflect.field(trait, fieldName); - var fieldClass = Type.getClass(fieldValue); - - // Treat objects differently (VERY bad performance otherwise) - if (Reflect.isObject(fieldValue) && fieldClass != String) { - - if (fieldClass != null) { - ui.text('<${Type.getClassName(fieldClass)}>', Align.Right); - } else { - // Anonymous data structures for example - ui.text("", Align.Right); - } - } else { - ui.text(Std.string(fieldValue), Align.Right); - } + ui.text(dynamicToUIString(fieldValue), Align.Right); } ui.unindent(); @@ -961,6 +961,22 @@ class DebugConsole extends Trait { if (bindG) g.begin(false); } + function dynamicToUIString(value: Dynamic): String { + final valueClass = Type.getClass(value); + + // Don't convert objects to string, Haxe includes _all_ object fields + // (recursively) by default which does not fit in the UI and can cause performance issues + if (Reflect.isObject(value) && valueClass != String) { + if (valueClass != null) { + return '<${Type.getClassName(valueClass)}>'; + } + // Given value has no class, anonymous data structures for example + return ""; + } + + return Std.string(value); + } + function update() { armory.trait.WalkNavigation.enabled = !(ui.isScrolling || ui.dragHandle != null); updateTime += iron.App.updateTime; From dcda14cac891fbd9507fec4816fab8fda97afd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:28:26 +0100 Subject: [PATCH 113/175] Improve documentation of object property nodes --- .../logicnode/object/LN_get_object_property.py | 16 ++++++++++++++-- .../logicnode/object/LN_set_object_property.py | 13 +++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/blender/arm/logicnode/object/LN_get_object_property.py b/blender/arm/logicnode/object/LN_get_object_property.py index 3332677b09..e35fbf56f0 100644 --- a/blender/arm/logicnode/object/LN_get_object_property.py +++ b/blender/arm/logicnode/object/LN_get_object_property.py @@ -1,9 +1,21 @@ from arm.logicnode.arm_nodes import * + class GetPropertyNode(ArmLogicTreeNode): - """Returns the value of the given object property. + """Return the value of the given object property. If the object is `null` + or the property does not exist on the object, the node returns `null`. + + @input Object: The object to which the property belongs. + @input Property: The name of the property from which to get the value. + + @output Value: The value of the property. + @output Property: The name of the property as stated in the `Property` input. + + @see `Object Properties Panel > Armory Props > Properties` (do not confuse Armory object properties with Blender custom properties!) + @see [`iron.object.Object.properties`](https://api.armory3d.org/iron/object/Object.html#properties) - @seeNode Set Object Property""" + @seeNode Set Object Property + """ bl_idname = 'LNGetPropertyNode' bl_label = 'Get Object Property' arm_version = 1 diff --git a/blender/arm/logicnode/object/LN_set_object_property.py b/blender/arm/logicnode/object/LN_set_object_property.py index 326beb2f9a..bd4f47e70f 100644 --- a/blender/arm/logicnode/object/LN_set_object_property.py +++ b/blender/arm/logicnode/object/LN_set_object_property.py @@ -1,14 +1,23 @@ from arm.logicnode.arm_nodes import * + class SetPropertyNode(ArmLogicTreeNode): - """Sets the value of the given object property. + """Set the value of the given object property. This node can be used to share variables between different traits. If the trait(s) you want to access the variable with are on different objects, use the *[`Global Object`](#global-object)* node to store the data. Every trait can access this one. - @seeNode Get Object Property""" + @input Object: The object to which the property belongs. If the object is `null`, the node does not do anything. + @input Property: The name of the property from which to get the value. + @input Value: The value of the property. + + @see `Object Properties Panel > Armory Props > Properties` (do not confuse Armory object properties with Blender custom properties!) + @see [`iron.object.Object.properties`](https://api.armory3d.org/iron/object/Object.html#properties) + + @seeNode Get Object Property + """ bl_idname = 'LNSetPropertyNode' bl_label = 'Set Object Property' arm_section = 'props' From 3c959427cf5512650b5e003b1ad502ca47d3e44a Mon Sep 17 00:00:00 2001 From: tong Date: Tue, 7 Nov 2023 19:01:06 +0100 Subject: [PATCH 114/175] Keep WalkNavigation init function if arm_debug --- Sources/armory/trait/WalkNavigation.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/armory/trait/WalkNavigation.hx b/Sources/armory/trait/WalkNavigation.hx index 6bfc50d54d..00db00bea5 100644 --- a/Sources/armory/trait/WalkNavigation.hx +++ b/Sources/armory/trait/WalkNavigation.hx @@ -26,6 +26,7 @@ class WalkNavigation extends Trait { notifyOnInit(init); } + #if arm_debug @:keep #end function init() { keyboard = Input.getKeyboard(); gamepad = Input.getGamepad(); From 1673aa41d9139495d8fb81ee9faacfee519553e0 Mon Sep 17 00:00:00 2001 From: e2002e Date: Fri, 10 Nov 2023 09:45:58 +0100 Subject: [PATCH 115/175] normalize viewPos in reflect / refract functions. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 06ce4dcff3..4071602673 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -91,7 +91,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); - vec3 reflected = reflect(viewPos, viewNormal); + vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 03b5e038ca..2ed4d4e314 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -100,7 +100,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); - vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); + vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); hitCoord = -viewPos; #ifdef _CPostprocess From 5e627aadff4dcd9709c3b474ad8b4a6008b93b03 Mon Sep 17 00:00:00 2001 From: e2002e Date: Mon, 13 Nov 2023 19:58:58 +0100 Subject: [PATCH 116/175] simplify string format, register BSDF REFRACT, add default values to ior and opacity for deferred shaders. --- Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl | 2 ++ blender/arm/material/make_mesh.py | 4 ++-- blender/arm/material/node_meta.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl index 3eae02fdef..2128746c77 100644 --- a/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl +++ b/Shaders/custom_mat_presets/custom_mat_deferred.frag.glsl @@ -40,6 +40,8 @@ void main() { float specular = 1.0; uint materialId = 0; vec3 emissionCol = vec3(0.0); + float ior = 1.450; + float opacity = 1.0; // Store in gbuffer (see layout table above) fragColor[GBUF_IDX_0] = vec4(n.xy, roughness, packFloatInt16(metallic, materialId)); diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index b56af61dc2..efb81c67cc 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -542,14 +542,14 @@ def make_forward(con_mesh): frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) if not blend: - mrt = 0 + mrt = 0 # mrt: multiple render targets if rpdat.rp_ssr: mrt = 2 if rpdat.rp_ss_refraction: mrt += 1 if mrt != 0: # Store light gbuffer for post-processing - frag.add_out('vec4 fragColor[{mrt}]'.format(mrt = mrt)) + frag.add_out(f'vec4 fragColor[{mrt}]') frag.add_include('std/gbuffer.glsl') frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') diff --git a/blender/arm/material/node_meta.py b/blender/arm/material/node_meta.py index 623b2abe4e..1aef0568af 100644 --- a/blender/arm/material/node_meta.py +++ b/blender/arm/material/node_meta.py @@ -160,6 +160,7 @@ class MaterialNodeMeta: 'BSDF_PRINCIPLED': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfprincipled), 'BSDF_TRANSLUCENT': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdftranslucent), 'BSDF_TRANSPARENT': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdftransparent), + 'BSDF_REFRACTION': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfrefraction), 'BSDF_VELVET': MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfvelvet), 'EMISSION': MaterialNodeMeta(parse_func=nodes_shader.parse_emission), 'HOLDOUT': MaterialNodeMeta( From f226d3908e8119835b9ec9ec009c50604b20360a Mon Sep 17 00:00:00 2001 From: e2002e Date: Mon, 13 Nov 2023 23:42:51 +0100 Subject: [PATCH 117/175] rearanged mtr in make_mesh and renamed rior to ior. --- .../armory/renderpath/RenderPathDeferred.hx | 2 +- .../armory/renderpath/RenderPathForward.hx | 2 +- blender/arm/material/cycles.py | 4 ++-- .../arm/material/cycles_nodes/nodes_shader.py | 18 +++++++++--------- blender/arm/material/make_depth.py | 2 +- blender/arm/material/make_mesh.py | 19 +++++++++++-------- blender/arm/material/mat_utils.py | 2 +- blender/arm/material/parser_state.py | 6 +++--- 8 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 96feac681d..afa5ea29dd 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -334,7 +334,7 @@ class RenderPathDeferred { path.loadShader("shader_datas/ssrefr_pass/ssrefr_pass"); path.loadShader("shader_datas/copy_pass/copy_pass"); - //holds rior and opacity + //holds ior and opacity var t = new RenderTargetRaw(); t.name = "gbuffer_refraction"; t.width = 0; diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index 4d6c6430aa..64b6a61a33 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -120,7 +120,7 @@ class RenderPathForward { #if rp_ssrefr { - //holds rior and opacity + //holds ior and opacity var t = new RenderTargetRaw(); t.name = "gbuffer_refraction"; t.width = 0; diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 3751ce72cf..875d44328c 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -113,7 +113,7 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types. curshader = state.frag state.curshader = curshader - out_basecol, out_roughness, out_metallic, out_occlusion, out_specular, out_opacity, out_rior, out_emission_col = parse_shader_input(node.inputs[0]) + out_basecol, out_roughness, out_metallic, out_occlusion, out_specular, out_opacity, out_ior, out_emission_col = parse_shader_input(node.inputs[0]) if parse_surface: curshader.write(f'basecol = {out_basecol};') curshader.write(f'roughness = {out_roughness};') @@ -132,7 +132,7 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types. if parse_opacity: curshader.write('opacity = {0};'.format(out_opacity)) - curshader.write('rior = {0};'.format(out_rior)) + curshader.write('ior = {0};'.format(out_ior)) # Volume # parse_volume_input(node.inputs[1]) diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index 4b95449533..e83274dc7d 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -35,11 +35,11 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var)) mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc1, rough1, met1, occ1, spec1, opac1, rior1, emi1 = c.parse_shader_input(node.inputs[0]) + bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[0]) ek1 = mat_state.emission_type mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc2, rough2, met2, occ2, spec2, opac2, rior2, emi2 = c.parse_shader_input(node.inputs[1]) + bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[1]) ek2 = mat_state.emission_type if state.parse_surface: @@ -52,15 +52,15 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2) if state.parse_opacity: state.out_opacity = '({0} * {3} + {1} * {2})'.format(opac1, opac2, fac_var, fac_inv_var) - state.out_rior = '({0} * {3} + {1} * {2})'.format(rior1, rior2, fac_var, fac_inv_var) + state.out_ior = '({0} * {3} + {1} * {2})'.format(ior1, ior2, fac_var, fac_inv_var) def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state: ParserState) -> None: mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc1, rough1, met1, occ1, spec1, opac1, rior1, emi1 = c.parse_shader_input(node.inputs[0]) + bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[0]) ek1 = mat_state.emission_type mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc2, rough2, met2, occ2, spec2, opac2, rior2, emi2 = c.parse_shader_input(node.inputs[1]) + bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[1]) ek2 = mat_state.emission_type if state.parse_surface: @@ -73,7 +73,7 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2) if state.parse_opacity: state.out_opacity = '({0} * 0.5 + {1} * 0.5)'.format(opac1, opac2) - state.out_rior = '({0} * 0.5 + {1} * 0.5)'.format(rior1, rior2) + state.out_ior = '({0} * 0.5 + {1} * 0.5)'.format(ior1, ior2) def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: @@ -106,7 +106,7 @@ def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: N # clearcoar_normal = c.parse_vector_input(node.inputs[21]) # tangent = c.parse_vector_input(node.inputs[22]) if state.parse_opacity: - state.out_rior = c.parse_value_input(node.inputs[16]) + state.out_ior = c.parse_value_input(node.inputs[16]) if len(node.inputs) >= 21: state.out_opacity = c.parse_value_input(node.inputs[21]) @@ -159,7 +159,7 @@ def parse_bsdfglass(node: bpy.types.ShaderNodeBsdfGlass, out_socket: NodeSocket, state.out_roughness = c.parse_value_input(node.inputs[1]) if state.parse_opacity: state.out_opacity = '0.0' - state.out_rior = c.parse_value_input(node.inputs[2]) + state.out_ior = c.parse_value_input(node.inputs[2]) def parse_bsdfhair(node: bpy.types.ShaderNodeBsdfHair, out_socket: NodeSocket, state: ParserState) -> None: @@ -179,7 +179,7 @@ def parse_bsdfrefraction(node: bpy.types.ShaderNodeBsdfRefraction, out_socket: N state.out_roughness = c.parse_value_input(node.inputs[1]) if state.parse_opacity: state.out_opacity = '0.0' - state.out_rior = c.parse_value_input(node.inputs[2]) + state.out_ior = c.parse_value_input(node.inputs[2]) def parse_subsurfacescattering(node: bpy.types.ShaderNodeSubsurfaceScattering, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: diff --git a/blender/arm/material/make_depth.py b/blender/arm/material/make_depth.py index e259c5d340..97679fbe33 100644 --- a/blender/arm/material/make_depth.py +++ b/blender/arm/material/make_depth.py @@ -52,7 +52,7 @@ def make(context_id, rpasses, shadowmap=False): if parse_opacity: frag.write('float opacity;') - frag.write('float rior;') + frag.write('float ior;') if(con_depth).is_elem('morph'): make_morph_target.morph_pos(vert) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index efb81c67cc..3e2ac279bf 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -279,7 +279,7 @@ def make_deferred(con_mesh, rpasses): if '_SSRefraction' in wrd.world_defs: if 'refraction' in rpasses: - frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(rior, opacity, 0.0, 0.0);') + frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(ior, opacity, 0.0, 0.0);') else: frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 0.0);') @@ -544,22 +544,25 @@ def make_forward(con_mesh): if not blend: mrt = 0 # mrt: multiple render targets if rpdat.rp_ssr: - mrt = 2 + mrt += 1 if rpdat.rp_ss_refraction: mrt += 1 if mrt != 0: + index = 1 # Store light gbuffer for post-processing - frag.add_out(f'vec4 fragColor[{mrt}]') + frag.add_out(f'vec4 fragColor[{mrt}+1]') frag.add_include('std/gbuffer.glsl') frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') - frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') - if '_SSRefraction' in wrd.world_defs: + if rpdat.rp_ssr: + frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') + index += 1 + if rpdat.rp_ss_refraction: if parse_opacity: - frag.write('fragColor[2] = vec4(rior, opacity, 0.0, 0.0);') + frag.write(f'fragColor[{index}] = vec4(ior, opacity, 0.0, 0.0);') else: - frag.write('fragColor[2] = vec4(1.0, 1.0, 0.0, 0.0);') + frag.write(f'fragColor[{index}] = vec4(1.0, 1.0, 0.0, 0.0);') else: frag.add_out('vec4 fragColor[1]') @@ -759,4 +762,4 @@ def _write_material_attribs_default(frag: shader.Shader, parse_opacity: bool): frag.write('vec3 emissionCol;') if parse_opacity: frag.write('float opacity;') - frag.write('float rior = 1.450;')#case shader is arm we don't get an ior + frag.write('float ior = 1.450;')#case shader is arm we don't get an ior diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py index c1ce9be998..f660ce4d8b 100644 --- a/blender/arm/material/mat_utils.py +++ b/blender/arm/material/mat_utils.py @@ -82,7 +82,7 @@ def is_transluc_traverse(node): def is_transluc_type(node: bpy.types.ShaderNode) -> bool: - return node.type in ('BSDF_GLASS', 'BSDF_TRANSPARENT', 'BSDF_TRANSLUCENT') \ + return node.type in ('BSDF_GLASS', 'BSDF_TRANSPARENT', 'BSDF_TRANSLUCENT', 'BSDF_REFRACTION') \ or (is_armory_pbr_node(node) and (node.inputs['Opacity'].is_linked or node.inputs['Opacity'].default_value != 1.0)) \ or (node.type == 'BSDF_PRINCIPLED' and (node.inputs['Alpha'].is_linked or node.inputs['Alpha'].default_value != 1.0)) diff --git a/blender/arm/material/parser_state.py b/blender/arm/material/parser_state.py index 2938121461..bd953b7f5e 100644 --- a/blender/arm/material/parser_state.py +++ b/blender/arm/material/parser_state.py @@ -97,7 +97,7 @@ def __init__(self, context: ParserContext, tree_name: str, world: Optional[bpy.t self.out_occlusion: floatstr = '1.0' self.out_specular: floatstr = '1.0' self.out_opacity: floatstr = '1.0' - self.out_rior: floatstr = '1.450' + self.out_ior: floatstr = '1.450' self.out_emission_col: vec3str = 'vec3(0.0)' def reset_outs(self): @@ -108,13 +108,13 @@ def reset_outs(self): self.out_occlusion = '1.0' self.out_specular = '1.0' self.out_opacity = '1.0' - self.out_rior = '1.450' + self.out_ior = '1.450' self.out_emission_col = 'vec3(0.0)' def get_outs(self) -> Tuple[vec3str, floatstr, floatstr, floatstr, floatstr, floatstr, floatstr, vec3str]: """Return the shader output values as a tuple.""" return (self.out_basecol, self.out_roughness, self.out_metallic, self.out_occlusion, self.out_specular, - self.out_opacity, self.out_rior, self.out_emission_col) + self.out_opacity, self.out_ior, self.out_emission_col) def get_parser_pass_suffix(self) -> str: From a60f4584ab3c404f6021fb5ec68c3ce684ff0940 Mon Sep 17 00:00:00 2001 From: e2002e Date: Thu, 16 Nov 2023 03:33:21 +0100 Subject: [PATCH 118/175] bind lbuffer1 for refraction in forward rp; --- Sources/armory/renderpath/RenderPathForward.hx | 4 ++-- blender/arm/material/make_mesh.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index 64b6a61a33..c07a1509c5 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -23,7 +23,7 @@ class RenderPathForward { #if rp_render_to_texture { path.setTarget("lbuffer0", [ - #if rp_ssr "lbuffer1", #end + #if (rp_ssr || rp_ssrefr) "lbuffer1", #end #if rp_ssrefr "gbuffer_refraction" #end] ); } @@ -105,7 +105,7 @@ class RenderPathForward { t.depth_buffer = "main"; path.createRenderTarget(t); - #if rp_ssr + #if (rp_ssr || rp_ssrefr) { var t = new RenderTargetRaw(); t.name = "lbuffer1"; diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 3e2ac279bf..7a436ef45e 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -548,7 +548,7 @@ def make_forward(con_mesh): if rpdat.rp_ss_refraction: mrt += 1 if mrt != 0: - index = 1 + idx_refraction = 1 # Store light gbuffer for post-processing frag.add_out(f'vec4 fragColor[{mrt}+1]') frag.add_include('std/gbuffer.glsl') @@ -557,12 +557,12 @@ def make_forward(con_mesh): frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') if rpdat.rp_ssr: frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') - index += 1 + idx_refraction += 1 if rpdat.rp_ss_refraction: if parse_opacity: - frag.write(f'fragColor[{index}] = vec4(ior, opacity, 0.0, 0.0);') + frag.write(f'fragColor[{idx_refraction}] = vec4(ior, opacity, 0.0, 0.0);') else: - frag.write(f'fragColor[{index}] = vec4(1.0, 1.0, 0.0, 0.0);') + frag.write(f'fragColor[{idx_refraction}] = vec4(1.0, 1.0, 0.0, 0.0);') else: frag.add_out('vec4 fragColor[1]') From c7430d990ecec2a11d72ee888fb1b1e9f8e011ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:23:31 +0100 Subject: [PATCH 119/175] Show event names in event node headers --- blender/arm/logicnode/event/LN_on_event.py | 7 +++++-- blender/arm/logicnode/event/LN_send_event_to_object.py | 6 ++++++ blender/arm/logicnode/event/LN_send_global_event.py | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/blender/arm/logicnode/event/LN_on_event.py b/blender/arm/logicnode/event/LN_on_event.py index 10c7d05562..11e6b4541c 100644 --- a/blender/arm/logicnode/event/LN_on_event.py +++ b/blender/arm/logicnode/event/LN_on_event.py @@ -1,5 +1,6 @@ from arm.logicnode.arm_nodes import * + class OnEventNode(ArmLogicTreeNode): """Activates the output when the given event is received. @@ -51,14 +52,16 @@ def draw_buttons(self, context, layout): layout.prop(self, 'property1', text='') def draw_label(self) -> str: - return f'{self.bl_label}: {self.operators[self.property1]}' + if self.inputs[0].is_linked: + return self.bl_label + return f'{self.bl_label}: {self.inputs[0].get_default_value()}' 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('LNOnEventNode') - + try: newnode.inputs[0].default_value_raw = self["property0"] except: diff --git a/blender/arm/logicnode/event/LN_send_event_to_object.py b/blender/arm/logicnode/event/LN_send_event_to_object.py index 8470a7049e..4c6bde98ad 100644 --- a/blender/arm/logicnode/event/LN_send_event_to_object.py +++ b/blender/arm/logicnode/event/LN_send_event_to_object.py @@ -1,5 +1,6 @@ from arm.logicnode.arm_nodes import * + class SendEventNode(ArmLogicTreeNode): """Sends a event to the given object. @@ -19,3 +20,8 @@ def arm_init(self, context): self.add_input('ArmNodeSocketObject', 'Object') self.add_output('ArmNodeSocketAction', 'Out') + + def draw_label(self) -> str: + if self.inputs[1].is_linked: + return self.bl_label + return f'{self.bl_label}: {self.inputs[1].get_default_value()}' diff --git a/blender/arm/logicnode/event/LN_send_global_event.py b/blender/arm/logicnode/event/LN_send_global_event.py index aba0ac0ef6..3d4f03838b 100644 --- a/blender/arm/logicnode/event/LN_send_global_event.py +++ b/blender/arm/logicnode/event/LN_send_global_event.py @@ -1,5 +1,6 @@ from arm.logicnode.arm_nodes import * + class SendGlobalEventNode(ArmLogicTreeNode): """Sends the given event to all objects in the scene. @@ -17,3 +18,8 @@ def arm_init(self, context): self.add_input('ArmStringSocket', 'Event') self.add_output('ArmNodeSocketAction', 'Out') + + def draw_label(self) -> str: + if self.inputs[1].is_linked: + return self.bl_label + return f'{self.bl_label}: {self.inputs[1].get_default_value()}' From dcd56ac6a6f8a9c33c700f21ccece5850c2f9d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:29:09 +0100 Subject: [PATCH 120/175] Make node groups available from "Add node" menu --- blender/arm/logicnode/arm_node_group.py | 16 +++++- blender/arm/nodes_logic.py | 66 ++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/blender/arm/logicnode/arm_node_group.py b/blender/arm/logicnode/arm_node_group.py index 51728ad7c1..fa9c472bef 100644 --- a/blender/arm/logicnode/arm_node_group.py +++ b/blender/arm/logicnode/arm_node_group.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE from functools import reduce -from typing import List, Set, Dict +from typing import Iterator, List, Set, Dict import bpy from bpy.props import * @@ -62,7 +62,7 @@ def upstream_trees(self) -> List['ArmGroupTree']: raise RecursionError(f'Looks like group tree "{self}" has links to itself from other groups') return trees - def can_be_linked(self): + def can_be_linked(self) -> bool: """Try to avoid creating loops of group trees with each other""" # upstream trees of tested treed should nad share trees with downstream trees of current tree tested_tree_upstream_trees = {t.name for t in self.upstream_trees()} @@ -73,6 +73,18 @@ def can_be_linked(self): def update(self): pass + @classmethod + def get_linkable_group_trees(cls) -> Iterator['ArmGroupTree']: + return filter(lambda tree: isinstance(tree, ArmGroupTree) and tree.can_be_linked(), bpy.data.node_groups) + + @classmethod + def has_linkable_group_trees(cls) -> bool: + try: + _ = next(cls.get_linkable_group_trees()) + except StopIteration: + return False + return True + class ArmEditGroupTree(bpy.types.Operator): """Go into subtree to edit""" diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 998469e957..8d747ecf74 100644 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -1,9 +1,10 @@ from typing import Any, Callable import webbrowser +import bl_operators import bpy import blf -from bpy.props import BoolProperty, StringProperty +from bpy.props import BoolProperty, CollectionProperty, StringProperty import arm.logicnode.arm_nodes as arm_nodes import arm.logicnode.replacement @@ -25,6 +26,9 @@ else: arm.enable_reload(__name__) +INTERNAL_GROUPS_MENU_ID = 'ARM_INTERNAL_GROUPS' +internal_groups_menu_class: bpy.types.Menu + registered_nodes = [] registered_categories = [] @@ -69,6 +73,10 @@ def draw(self, context): safe_category_name = arm.utils.safesrc(category.name.lower()) layout.menu(f'ARM_MT_{safe_category_name}_menu', text=category.name, icon=category.icon) + if arm.logicnode.arm_node_group.ArmGroupTree.has_linkable_group_trees(): + layout.separator() + layout.menu(f'ARM_MT_{INTERNAL_GROUPS_MENU_ID}_menu', text=internal_groups_menu_class.bl_label, icon='OUTLINER_OB_GROUP_INSTANCE') + else: ARM_MT_NodeAddOverride.overridden_draw(self, context) @@ -81,9 +89,26 @@ class ARM_OT_AddNodeOverride(bpy.types.Operator): type: StringProperty(name="NodeItem type") use_transform: BoolProperty(name="Use Transform") + settings: CollectionProperty( + name="Settings", + description="Settings to be applied on the newly created node", + type=bl_operators.node.NodeSetting, + options={'SKIP_SAVE'}, + ) def invoke(self, context, event): - bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.type, use_transform=self.use_transform) + # Passing collection properties as operator parameters only + # works via raw sequences of dicts: + # https://blender.stackexchange.com/a/298977/58208 + # https://github.com/blender/blender/blob/cf1e1ed46b7ec80edb0f43cb514d3601a1696ec1/source/blender/python/intern/bpy_rna.c#L2033-L2043 + setting_dicts = [] + for setting in self.settings.values(): + setting_dicts.append({ + "name": setting.name, + "value": setting.value + }) + + bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.type, use_transform=self.use_transform, settings=setting_dicts) return {'FINISHED'} @classmethod @@ -122,7 +147,7 @@ def draw_category_menu(self, context): def register_nodes(): - global registered_nodes + global registered_nodes, internal_groups_menu_class # Re-register all nodes for now.. if len(registered_nodes) > 0 or len(registered_categories) > 0: @@ -145,6 +170,7 @@ def register_nodes(): for category in category_section: category.sort_nodes() safe_category_name = arm.utils.safesrc(category.name.lower()) + assert(safe_category_name != INTERNAL_GROUPS_MENU_ID) # see below menu_class = type(f'ARM_MT_{safe_category_name}Menu', (bpy.types.Menu, ), { 'bl_space_type': 'NODE_EDITOR', 'bl_idname': f'ARM_MT_{safe_category_name}_menu', @@ -156,20 +182,48 @@ def register_nodes(): bpy.utils.register_class(menu_class) + # Generate and register group menu + def draw_nodegroups_menu(self, context): + layout = self.layout + + tree: arm.logicnode.arm_node_group.ArmGroupTree + for tree in arm.logicnode.arm_node_group.ArmGroupTree.get_linkable_group_trees(): + op = layout.operator('arm.add_node_override', text=tree.name) + op.type = 'LNCallGroupNode' + op.use_transform = True + item = op.settings.add() + item.name = "group_tree" + item.value = f'bpy.data.node_groups["{tree.name}"]' + + # Don't name categories like the content of the INTERNAL_GROUPS_MENU_ID variable! + menu_class = type(f'ARM_MT_{INTERNAL_GROUPS_MENU_ID}Menu', (bpy.types.Menu,), { + 'bl_space_type': 'NODE_EDITOR', + 'bl_idname': f'ARM_MT_{INTERNAL_GROUPS_MENU_ID}_menu', + 'bl_label': 'Node Groups', + 'bl_description': 'List of node groups that can be added to the current tree', + 'draw': draw_nodegroups_menu + }) + internal_groups_menu_class = menu_class + bpy.utils.register_class(menu_class) + def unregister_nodes(): - global registered_nodes, registered_categories + global registered_nodes, registered_categories, internal_groups_menu_class for n in registered_nodes: if issubclass(n, arm_nodes.ArmLogicTreeNode): n.on_unregister() bpy.utils.unregister_class(n) + registered_nodes = [] + for c in registered_categories: bpy.utils.unregister_class(c) - - registered_nodes = [] registered_categories = [] + if internal_groups_menu_class is not None: + bpy.utils.unregister_class(internal_groups_menu_class) + internal_groups_menu_class = None + class ARM_PT_LogicNodePanel(bpy.types.Panel): bl_label = 'Armory Logic Node' From 44370f08d52bd02fd33bf1fd02a6a27ef308a7e9 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:23:09 -0300 Subject: [PATCH 121/175] 1 new function 1 modified --- blender/arm/props_traits.py | 20 ++++++++++++++++++++ blender/arm/props_ui.py | 15 ++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 3734209c86..5d49e549d7 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -734,6 +734,24 @@ def draw(self, context): obj = bpy.context.scene draw_traits_panel(self.layout, obj, is_object=False) +class ARM_OT_RemoveTraitsFromActiveObjects(bpy.types.Operator): + bl_label = 'Remove Traits From Active Objects' + bl_idname = 'arm.remove_traits_from_active_objects' + bl_description = 'Removes all traits from all active objects' + + def execute(self, context): + for obj in bpy.context.selected_objects: + lst = obj.arm_traitlist + while(len(lst) > 0): + lst.remove(0) + obj.arm_traitlist_index = 0 + + for obj in bpy.context.selected_objects: + print(obj.name, obj.arm_traitlist_index) + + return {"FINISHED"} + + class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): bl_label = 'Copy Traits from Active Object' bl_idname = 'arm.copy_traits_to_active' @@ -814,6 +832,7 @@ def draw(self, _context): layout = self.layout layout.operator("arm.copy_traits_to_active", icon='PASTEDOWN') + layout.operator("arm.remove_traits_from_active_objects", icon='REMOVE') layout.operator("arm.print_traits", icon='CONSOLE') def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, bpy.types.Scene], is_object: bool) -> None: @@ -977,6 +996,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b ARM_PT_SceneTraitPanel, ARM_OT_CopyTraitsFromActive, ARM_MT_context_menu, + ARM_OT_RemoveTraitsFromActiveObjects ) __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index fed7645140..1c5b6891b2 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -2284,6 +2284,19 @@ class ArmPrintTraitsButton(bpy.types.Operator): def execute(self, context): for s in bpy.data.scenes: print('Scene: {0}'.format(s.name)) + for t in s.arm_traitlist: + if not t.enabled_prop: + continue + tname = "undefined" + if t.type_prop == 'Haxe Script' or "Bundled": + tname = t.class_name_prop + if t.type_prop == 'Logic Nodes': + tname = t.node_tree_prop.name + if t.type_prop == 'UI Canvas': + tname = t.canvas_name_prop + if t.type_prop == 'WebAssembly': + tname = t.webassembly_prop + print('Scene Trait: {0} ("{1}")'.format(s.name, tname)) for o in s.objects: for t in o.arm_traitlist: if not t.enabled_prop: @@ -2297,7 +2310,7 @@ def execute(self, context): tname = t.canvas_name_prop if t.type_prop == 'WebAssembly': tname = t.webassembly_prop - print('Trait: {0} ("{1}")'.format(o.name, tname)) + print('Object Trait: {0} ("{1}")'.format(o.name, tname)) return{'FINISHED'} class ARM_PT_MaterialNodePanel(bpy.types.Panel): From 54d0e333c88d6670616e859ae7f17b80a81d6a94 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:39:51 -0300 Subject: [PATCH 122/175] erase printing info --- blender/arm/props_traits.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 5d49e549d7..f81d5d4db3 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -746,9 +746,6 @@ def execute(self, context): lst.remove(0) obj.arm_traitlist_index = 0 - for obj in bpy.context.selected_objects: - print(obj.name, obj.arm_traitlist_index) - return {"FINISHED"} From 29ace198c546e591d7ec6264cce223b821a140c5 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sat, 18 Nov 2023 15:15:50 -0300 Subject: [PATCH 123/175] comments updates --- blender/arm/props_traits.py | 8 +++----- blender/arm/props_ui.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index f81d5d4db3..f13e8019ce 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -735,15 +735,13 @@ def draw(self, context): draw_traits_panel(self.layout, obj, is_object=False) class ARM_OT_RemoveTraitsFromActiveObjects(bpy.types.Operator): - bl_label = 'Remove Traits From Active Objects' + bl_label = 'Remove Traits From Selected Objects' bl_idname = 'arm.remove_traits_from_active_objects' - bl_description = 'Removes all traits from all active objects' + bl_description = 'Removes all traits from all selected objects' def execute(self, context): for obj in bpy.context.selected_objects: - lst = obj.arm_traitlist - while(len(lst) > 0): - lst.remove(0) + obj.arm_traitlist.clear() obj.arm_traitlist_index = 0 return {"FINISHED"} diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 1c5b6891b2..a7ddc9da88 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -2278,7 +2278,7 @@ def draw(self, context): class ArmPrintTraitsButton(bpy.types.Operator): bl_idname = 'arm.print_traits' - bl_label = 'Print All Scenes Traits' + bl_label = 'Print All Traits' bl_description = 'Returns all traits in current blend' def execute(self, context): @@ -2310,7 +2310,7 @@ def execute(self, context): tname = t.canvas_name_prop if t.type_prop == 'WebAssembly': tname = t.webassembly_prop - print('Object Trait: {0} ("{1}")'.format(o.name, tname)) + print(' Object Trait: {0} ("{1}")'.format(o.name, tname)) return{'FINISHED'} class ARM_PT_MaterialNodePanel(bpy.types.Panel): From 5b4ab1e464ddbaee49ce55a4844e04322d375def Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:37:17 -0300 Subject: [PATCH 124/175] add poll function From dbf7ed0a187832dd3c4de665c830d9d9c39fec2f Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:39:39 -0300 Subject: [PATCH 125/175] add poll function --- blender/arm/props_traits.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index f13e8019ce..e6515d1437 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -739,6 +739,10 @@ class ARM_OT_RemoveTraitsFromActiveObjects(bpy.types.Operator): bl_idname = 'arm.remove_traits_from_active_objects' bl_description = 'Removes all traits from all selected objects' + @classmethod + def poll(cls, context): + return len(context.selected_objects) > 0 + def execute(self, context): for obj in bpy.context.selected_objects: obj.arm_traitlist.clear() From a7cdc15764badbec187a9639580c3b96b26409fd Mon Sep 17 00:00:00 2001 From: 1k8 Date: Wed, 22 Nov 2023 13:05:24 -0500 Subject: [PATCH 126/175] Update WebSocket and replace Regex Recent changes to PCRE need to be investigated to find out the cause of matched index outside bounds caused by the regex matches. --- Sources/armory/network/WebSocket.hx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/armory/network/WebSocket.hx b/Sources/armory/network/WebSocket.hx index f1aa64938a..1946e03123 100644 --- a/Sources/armory/network/WebSocket.hx +++ b/Sources/armory/network/WebSocket.hx @@ -190,7 +190,9 @@ class WebSocket extends WebSocketCommon { } inline private function parseUrl(url) - { + { + /** TO DO - FIND OUT WHAT IS BREAKING REGEX IN THE NEW PCRE2 FOR HL C + var urlRegExp = ~/^(\w+?):\/\/([\w\.-]+)(:(\d+))?(\/.*)?$/; if ( ! urlRegExp.match(url)) { @@ -209,9 +211,24 @@ class WebSocket extends WebSocketCommon { if (_path == null || _path.length == 0) { _path = "/"; } - + **/ + var urlArr = url.split(":"); + if ( urlArr.length < 3) { + throw 'Uri not matching websocket URL "${url}"'; + } + _protocol = urlArr[0]; + _host = urlArr[1].substr(2, urlArr[1].length); + var parsedPort = Std.parseInt(urlArr[2].split("/")[0]); + if (parsedPort > 0 ) { + _port = parsedPort; + } + _path = urlArr[2].substr(urlArr[2].split("/")[0].length, urlArr[2].length); + if (_path == null || _path.length == 0) { + _path = "/"; + } } + private function createSocket():SocketImpl { if (_protocol == "wss") { From ce94101ff03de5070accf15c816f3acd911a98e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:38:59 +0100 Subject: [PATCH 127/175] Print exception if player cannot be started --- blender/arm/make.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index ad7efe1dba..19c2195638 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -11,6 +11,7 @@ import subprocess import threading import time +import traceback from typing import Callable import webbrowser @@ -677,8 +678,9 @@ def build_success(): cmd.append('--nosound') try: state.proc_play = run_proc(cmd, play_done) - except: - log.warn('Failed to start player') + except Exception: + traceback.print_exc() + log.warn('Failed to start player, command and exception have been printed to console above') if wrd.arm_runtime == 'Browser': webbrowser.open(url) From 75b3e9ce5e75d2b8b46cc39d9c08ea99e9a77497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:39:08 +0100 Subject: [PATCH 128/175] Fix whitespace --- blender/arm/make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index 19c2195638..b633bdc34b 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -629,7 +629,7 @@ def build_success(): else: tplstr = Template(envcmd).safe_substitute({ 'host': host, - 'port': prefs.html5_server_port, + 'port': prefs.html5_server_port, 'width': width, 'height': height, 'url': url, From d23546180d2c1e0ed24a269db09133816a890a2e Mon Sep 17 00:00:00 2001 From: 1k8 Date: Wed, 29 Nov 2023 13:12:22 -0500 Subject: [PATCH 129/175] Include ZUI when Debug Console is activated Resolves when ZUI module is disabled and debugging is enabled --- blender/arm/write_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/write_data.py b/blender/arm/write_data.py index 60146ddd53..5c1a2dfd22 100644 --- a/blender/arm/write_data.py +++ b/blender/arm/write_data.py @@ -297,7 +297,7 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo if arm.utils.get_pref_or_default('haxe_times', False): khafile.write("project.addParameter('--times');\n") - if export_ui: + if export_ui or wrd.arm_debug_console: if not os.path.exists('Libraries/zui'): khafile.write(add_armory_library(sdk_path, 'lib/zui', rel_path=do_relpath_sdk)) p = sdk_path + '/armory/Assets/font_default.ttf' From 892a96c5a5ba05d713c3afaf0aad23623f679302 Mon Sep 17 00:00:00 2001 From: RPaladin <69180012+rpaladin@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:31:46 -0800 Subject: [PATCH 130/175] Fix context for remove traits operator --- blender/arm/props_traits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index e6515d1437..3ecaa002ec 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -741,7 +741,7 @@ class ARM_OT_RemoveTraitsFromActiveObjects(bpy.types.Operator): @classmethod def poll(cls, context): - return len(context.selected_objects) > 0 + return context.mode != 'SCENE' and len(context.selected_objects) > 0 def execute(self, context): for obj in bpy.context.selected_objects: From 042f6ebcef0ff5629ee518b568c837ae20e762b1 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:19:44 -0300 Subject: [PATCH 131/175] set name for armature with actions --- Sources/armory/logicnode/SetNameNode.hx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/armory/logicnode/SetNameNode.hx b/Sources/armory/logicnode/SetNameNode.hx index 630baa7a67..22f2bbfc42 100644 --- a/Sources/armory/logicnode/SetNameNode.hx +++ b/Sources/armory/logicnode/SetNameNode.hx @@ -14,6 +14,12 @@ class SetNameNode extends LogicNode { if (object == null) return; + #if arm_skin + for(a in iron.Scene.active.animations) + if(a.armature != null && a.armature.name == object.name) + a.armature.name = name; + #end + object.name = name; runOutput(0); From 62312c4fc88fe00a4f6f11e995f236febbb73ff2 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sat, 2 Dec 2023 14:38:36 +0100 Subject: [PATCH 132/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index 605f7fc411..a37ffcb4d9 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.11' +arm_version = '2023.12' arm_commit = '$Id$' def get_project_html5_copy(self): From 81c84d1739c849bc9e7d907f44953402b7c445a0 Mon Sep 17 00:00:00 2001 From: e2002e Date: Tue, 5 Dec 2023 14:37:16 +0100 Subject: [PATCH 133/175] fix for refraction, set gbufferD1 to DEPTH16 --- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 4 ++-- Shaders/water_pass/water_pass.frag.glsl | 24 +++++++++---------- .../armory/renderpath/RenderPathDeferred.hx | 2 +- .../armory/renderpath/RenderPathForward.hx | 2 +- .../arm/material/cycles_nodes/nodes_shader.py | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 2ed4d4e314..aa57479182 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -101,7 +101,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, depth, cameraProj); vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); - hitCoord = -viewPos; + hitCoord = viewPos; #ifdef _CPostprocess vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; @@ -125,5 +125,5 @@ void main() { vec3 refractionCol = textureLod(tex1, coords.xy, 0.0).rgb; refractionCol = clamp(refractionCol, 0.0, 1.0); vec3 color = textureLod(tex, texCoord.xy, 0.0).rgb; - fragColor.rgb = mix(refractionCol * intensity + color, color, opac); + fragColor.rgb = mix(refractionCol * intensity, color, opac); } diff --git a/Shaders/water_pass/water_pass.frag.glsl b/Shaders/water_pass/water_pass.frag.glsl index 0e34e92c87..65451c35dc 100644 --- a/Shaders/water_pass/water_pass.frag.glsl +++ b/Shaders/water_pass/water_pass.frag.glsl @@ -53,27 +53,25 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); return viewPos.z - hit.z; } vec4 binarySearch(vec3 dir) { float ddepth; - vec3 start = hitCoord; - for (int i = 0; i < numBinarySearchSteps; i++) - { + for (int i = 0; i < 7; i++) { dir *= 0.5; - start -= dir; - ddepth = getDeltaDepth(start); + hitCoord -= dir; + ddepth = getDeltaDepth(hitCoord); if (ddepth < 0.0) hitCoord += dir; } // Ugly discard of hits too far away - #ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); - #else - if (abs(ddepth) > ssrSearchDist / 500) return vec4(0.0); - #endif - return vec4(getProjectedCoord(start), 0.0, 1.0); +#ifdef _CPostprocess + if (abs(ddepth) > PPComp9.z) return vec4(0.0); +#else + if (abs(ddepth) > ssrSearchDist) return vec4(0.0); +#endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } vec4 rayCast(vec3 dir) { @@ -152,7 +150,7 @@ void main() { vec3 viewNormal = n2; vec3 viewPos = getPosView(viewRay, gdepth, cameraProj); - vec3 reflected = reflect(viewPos, viewNormal); + vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index afa5ea29dd..afa234427e 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -361,7 +361,7 @@ class RenderPathDeferred { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = "R32"; + t.format = "DEPTH16"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); } diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index c07a1509c5..13757b9538 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -147,7 +147,7 @@ class RenderPathForward { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = "R32"; + t.format = "DEPTH16"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); } diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index e83274dc7d..2a403ab5b9 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -158,7 +158,7 @@ def parse_bsdfglass(node: bpy.types.ShaderNodeBsdfGlass, out_socket: NodeSocket, c.write_normal(node.inputs[3]) state.out_roughness = c.parse_value_input(node.inputs[1]) if state.parse_opacity: - state.out_opacity = '0.0' + state.out_opacity = '0.5' # for refraction mix(refraction, color) state.out_ior = c.parse_value_input(node.inputs[2]) From b69ad3f97b9afdad4cf92f84a237cfe82755f645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:31:19 +0100 Subject: [PATCH 134/175] Enable GPU for environment rendering also for non-CUDA compute devices The now replaced code was written when Cycles only supported CUDA or no GPU rendering at all, but now there are more options available (CUDA, OptiX, HIP, oneAPI) --- blender/arm/write_probes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 36001e5491..4f3bb8e13b 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -68,10 +68,10 @@ def setup_envmap_render(): scene.render.resolution_y = radiance_size // 2 # Set GPU as rendering device if the user enabled it - if bpy.context.preferences.addons["cycles"].preferences.compute_device_type == "CUDA": + if bpy.context.preferences.addons["cycles"].preferences.compute_device_type != "NONE": scene.cycles.device = "GPU" else: - log.info('Using CPU for environment render (might be slow). Enable CUDA if possible.') + log.info('Using CPU for environment render (might be slow). If possible, configure GPU rendering in Blender preferences (System > Cycles Render Devices).') # Those settings are sufficient for rendering only the world background scene.cycles.samples = 1 From 12562d86ab82d42cf42edf47879811716779d982 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:25:53 +0000 Subject: [PATCH 135/175] add sound string option --- Sources/armory/logicnode/PlaySoundRawNode.hx | 4 +++- blender/arm/logicnode/sound/LN_play_sound.py | 25 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index 2f5325db0f..c5a4df99c5 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -15,6 +15,8 @@ class PlaySoundRawNode extends LogicNode { /** Whether to stream the sound from disk **/ public var property5: Bool; + public var property6: String; + var sound: kha.Sound = null; var channel: kha.audio1.AudioChannel = null; @@ -26,7 +28,7 @@ class PlaySoundRawNode extends LogicNode { switch (from) { case Play: if (sound == null) { - iron.data.Data.getSound(property0, function(s: kha.Sound) { + iron.data.Data.getSound(property6 == 'Sound' ? property0 : inputs[5].get(), function(s: kha.Sound) { this.sound = s; }); } diff --git a/blender/arm/logicnode/sound/LN_play_sound.py b/blender/arm/logicnode/sound/LN_play_sound.py index 90b51080ea..6a8ae19078 100644 --- a/blender/arm/logicnode/sound/LN_play_sound.py +++ b/blender/arm/logicnode/sound/LN_play_sound.py @@ -20,7 +20,9 @@ class PlaySoundNode(ArmLogicTreeNode): @output Done: activated when the playback has finished or was stopped manually. + @option Sound/Sound Name: specify a sound by a resource or a string name @option Sound: The sound that will be played. + @option Stream: Stream the sound from disk. @option Loop: Whether to loop the playback. @option Retrigger: If true, the playback position will be reset to the beginning on each activation of Play. If false, the playback @@ -31,9 +33,16 @@ class PlaySoundNode(ArmLogicTreeNode): bl_idname = 'LNPlaySoundRawNode' bl_label = 'Play Sound' bl_width_default = 200 - arm_version = 2 + arm_version = 3 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 5: + self.inputs.remove(self.inputs[-1]) + if self.property6 == 'Sound Name': + self.add_input('ArmStringSocket', 'Sound Name') property0: HaxePointerProperty('property0', name='', type=bpy.types.Sound) + property1: HaxeBoolProperty( 'property1', name='Loop', @@ -62,7 +71,14 @@ class PlaySoundNode(ArmLogicTreeNode): default=False ) + property6: HaxeEnumProperty( + 'property6', + items = [('Sound', 'Sound', 'Sound'), + ('Sound Name', 'Sound Name', 'Sound Name')], + name='', default='Sound', update=remove_extra_inputs) + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'Play') self.add_input('ArmNodeSocketAction', 'Pause') self.add_input('ArmNodeSocketAction', 'Stop') @@ -74,9 +90,14 @@ def arm_init(self, context): self.add_output('ArmNodeSocketAction', 'Done') def draw_buttons(self, context, layout): - layout.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') + + layout.prop(self, 'property6') col = layout.column(align=True) + + if self.property6 == 'Sound': + col.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') + col.prop(self, 'property5') col.prop(self, 'property1') col.prop(self, 'property2') From 2a1d86b1019584d39a2cbcefdff3fff2cb0584f1 Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Tue, 19 Dec 2023 02:08:54 +0000 Subject: [PATCH 136/175] world logic nodes --- .../logicnode/GetHosekWilkiePropertiesNode.hx | 27 +++++++++++ .../logicnode/GetNishitaPropertiesNode.hx | 29 ++++++++++++ .../armory/logicnode/GetWorldStrengthNode.hx | 12 +++++ .../logicnode/SetHosekWilkiePropertiesNode.hx | 41 +++++++++++++++++ .../logicnode/SetNishitaPropertiesNode.hx | 45 +++++++++++++++++++ .../armory/logicnode/SetWorldStrengthNode.hx | 16 +++++++ blender/arm/logicnode/__init__.py | 1 + .../world/LN_get_hosekwilkie_properties.py | 12 +++++ .../world/LN_get_nishita_properties.py | 14 ++++++ .../logicnode/world/LN_get_world_strength.py | 10 +++++ .../world/LN_set_hosekwilkie_properties.py | 38 ++++++++++++++++ .../world/LN_set_nishita_properties.py | 43 ++++++++++++++++++ .../logicnode/world/LN_set_world_strength.py | 13 ++++++ blender/arm/logicnode/world/__init__.py | 3 ++ 14 files changed, 304 insertions(+) create mode 100644 Sources/armory/logicnode/GetHosekWilkiePropertiesNode.hx create mode 100644 Sources/armory/logicnode/GetNishitaPropertiesNode.hx create mode 100644 Sources/armory/logicnode/GetWorldStrengthNode.hx create mode 100644 Sources/armory/logicnode/SetHosekWilkiePropertiesNode.hx create mode 100644 Sources/armory/logicnode/SetNishitaPropertiesNode.hx create mode 100644 Sources/armory/logicnode/SetWorldStrengthNode.hx create mode 100644 blender/arm/logicnode/world/LN_get_hosekwilkie_properties.py create mode 100644 blender/arm/logicnode/world/LN_get_nishita_properties.py create mode 100644 blender/arm/logicnode/world/LN_get_world_strength.py create mode 100644 blender/arm/logicnode/world/LN_set_hosekwilkie_properties.py create mode 100644 blender/arm/logicnode/world/LN_set_nishita_properties.py create mode 100644 blender/arm/logicnode/world/LN_set_world_strength.py create mode 100644 blender/arm/logicnode/world/__init__.py diff --git a/Sources/armory/logicnode/GetHosekWilkiePropertiesNode.hx b/Sources/armory/logicnode/GetHosekWilkiePropertiesNode.hx new file mode 100644 index 0000000000..2f9a1d6cac --- /dev/null +++ b/Sources/armory/logicnode/GetHosekWilkiePropertiesNode.hx @@ -0,0 +1,27 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GetHosekWilkiePropertiesNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var world = iron.Scene.active.world.raw; + + return switch (from) { + case 0: + world.turbidity; + case 1: + world.ground_albedo; + case 2: + new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]); + default: + null; + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/GetNishitaPropertiesNode.hx b/Sources/armory/logicnode/GetNishitaPropertiesNode.hx new file mode 100644 index 0000000000..41c784143c --- /dev/null +++ b/Sources/armory/logicnode/GetNishitaPropertiesNode.hx @@ -0,0 +1,29 @@ +package armory.logicnode; + +import iron.math.Vec4; + +class GetNishitaPropertiesNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var world = iron.Scene.active.world.raw; + + return switch (from) { + case 0: + world.nishita_density[0]; + case 1: + world.nishita_density[1]; + case 2: + world.nishita_density[2]; + case 3: + new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]); + default: + null; + } + return null; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/GetWorldStrengthNode.hx b/Sources/armory/logicnode/GetWorldStrengthNode.hx new file mode 100644 index 0000000000..c97c7bf966 --- /dev/null +++ b/Sources/armory/logicnode/GetWorldStrengthNode.hx @@ -0,0 +1,12 @@ +package armory.logicnode; + +class GetWorldStrengthNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + return iron.Scene.active.world.raw.probe.strength; + } +} \ No newline at end of file diff --git a/Sources/armory/logicnode/SetHosekWilkiePropertiesNode.hx b/Sources/armory/logicnode/SetHosekWilkiePropertiesNode.hx new file mode 100644 index 0000000000..905f1f9a8f --- /dev/null +++ b/Sources/armory/logicnode/SetHosekWilkiePropertiesNode.hx @@ -0,0 +1,41 @@ +package armory.logicnode; + +import armory.renderpath.HosekWilkie; +import iron.math.Vec4; + +class SetHosekWilkiePropertiesNode extends LogicNode { + + public var property0:String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var world = iron.Scene.active.world; + + if(property0 == 'Turbidity/Ground Albedo'){ + world.raw.turbidity = inputs[1].get(); + world.raw.ground_albedo = inputs[2].get(); + } + + if(property0 == 'Turbidity') + world.raw.turbidity = inputs[1].get(); + + if(property0 == 'Ground Albedo') + world.raw.ground_albedo = inputs[1].get(); + + if(property0 == 'Sun Direction'){ + var vec:Vec4 = inputs[1].get(); + world.raw.sun_direction[0] = vec.x; + world.raw.sun_direction[1] = vec.y; + world.raw.sun_direction[2] = vec.z; + } + + HosekWilkie.recompute(world); + + runOutput(0); + + } +} diff --git a/Sources/armory/logicnode/SetNishitaPropertiesNode.hx b/Sources/armory/logicnode/SetNishitaPropertiesNode.hx new file mode 100644 index 0000000000..c7b44a8c9d --- /dev/null +++ b/Sources/armory/logicnode/SetNishitaPropertiesNode.hx @@ -0,0 +1,45 @@ +package armory.logicnode; + +import armory.renderpath.Nishita; +import iron.math.Vec4; + +class SetNishitaPropertiesNode extends LogicNode { + + public var property0:String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + var world = iron.Scene.active.world; + + if(property0 == 'Density'){ + world.raw.nishita_density[0] = inputs[1].get(); + world.raw.nishita_density[1] = inputs[2].get(); + world.raw.nishita_density[2] = inputs[3].get(); + } + + if(property0 == 'Air') + world.raw.nishita_density[0] = inputs[1].get(); + + if(property0 == 'Dust') + world.raw.nishita_density[1] = inputs[1].get(); + + if(property0 == 'Ozone') + world.raw.nishita_density[2] = inputs[1].get(); + + if(property0 == 'Sun Direction'){ + var vec:Vec4 = inputs[1].get(); + world.raw.sun_direction[0] = vec.x; + world.raw.sun_direction[1] = vec.y; + world.raw.sun_direction[2] = vec.z; + } + + Nishita.recompute(world); + + runOutput(0); + + } +} diff --git a/Sources/armory/logicnode/SetWorldStrengthNode.hx b/Sources/armory/logicnode/SetWorldStrengthNode.hx new file mode 100644 index 0000000000..f4adad6f6b --- /dev/null +++ b/Sources/armory/logicnode/SetWorldStrengthNode.hx @@ -0,0 +1,16 @@ +package armory.logicnode; + +class SetWorldStrengthNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + + iron.Scene.active.world.raw.probe.strength = inputs[1].get(); + + runOutput(0); + + } +} diff --git a/blender/arm/logicnode/__init__.py b/blender/arm/logicnode/__init__.py index 9ba4250f9b..0dae5e7d0f 100644 --- a/blender/arm/logicnode/__init__.py +++ b/blender/arm/logicnode/__init__.py @@ -34,6 +34,7 @@ def init_categories(): arm_nodes.add_category('Camera', icon='OUTLINER_OB_CAMERA', section="data") arm_nodes.add_category('Material', icon='MATERIAL', section="data") arm_nodes.add_category('Light', icon='LIGHT', section="data") + arm_nodes.add_category('World', icon='WORLD', section="data") arm_nodes.add_category('Object', icon='OBJECT_DATA', section="data") arm_nodes.add_category('Scene', icon='SCENE_DATA', section="data") arm_nodes.add_category('Trait', icon='NODETREE', section="data") diff --git a/blender/arm/logicnode/world/LN_get_hosekwilkie_properties.py b/blender/arm/logicnode/world/LN_get_hosekwilkie_properties.py new file mode 100644 index 0000000000..353f43938e --- /dev/null +++ b/blender/arm/logicnode/world/LN_get_hosekwilkie_properties.py @@ -0,0 +1,12 @@ +from arm.logicnode.arm_nodes import * + +class GetHosekWilkiePropertiesNode(ArmLogicTreeNode): + """Gets the HosekWilkie properties.""" + bl_idname = 'LNGetHosekWilkiePropertiesNode' + bl_label = 'Get HosekWilkie Properties' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Turbidity') + self.add_output('ArmFloatSocket', 'Ground Albedo') + self.add_output('ArmVectorSocket', 'Sun Direction') diff --git a/blender/arm/logicnode/world/LN_get_nishita_properties.py b/blender/arm/logicnode/world/LN_get_nishita_properties.py new file mode 100644 index 0000000000..458777fada --- /dev/null +++ b/blender/arm/logicnode/world/LN_get_nishita_properties.py @@ -0,0 +1,14 @@ +from arm.logicnode.arm_nodes import * + +class GetNishitaPropertiesNode(ArmLogicTreeNode): + """Gets the Nishita properties.""" + bl_idname = 'LNGetNishitaPropertiesNode' + bl_label = 'Get Nishita Properties' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Air') + self.add_output('ArmFloatSocket', 'Dust') + self.add_output('ArmFloatSocket', 'Ozone') + self.add_output('ArmVectorSocket', 'Sun Direction') + diff --git a/blender/arm/logicnode/world/LN_get_world_strength.py b/blender/arm/logicnode/world/LN_get_world_strength.py new file mode 100644 index 0000000000..bcc37811b1 --- /dev/null +++ b/blender/arm/logicnode/world/LN_get_world_strength.py @@ -0,0 +1,10 @@ +from arm.logicnode.arm_nodes import * + +class GetWorldStrengthNode(ArmLogicTreeNode): + """Gets the strength of the given World.""" + bl_idname = 'LNGetWorldStrengthNode' + bl_label = 'Get World Strength' + arm_version = 1 + + def arm_init(self, context): + self.add_output('ArmFloatSocket', 'Strength') diff --git a/blender/arm/logicnode/world/LN_set_hosekwilkie_properties.py b/blender/arm/logicnode/world/LN_set_hosekwilkie_properties.py new file mode 100644 index 0000000000..1e3e3d0768 --- /dev/null +++ b/blender/arm/logicnode/world/LN_set_hosekwilkie_properties.py @@ -0,0 +1,38 @@ +from arm.logicnode.arm_nodes import * + +class SetHosekWilkiePropertiesNode(ArmLogicTreeNode): + """Sets the HosekWilkie properties.""" + bl_idname = 'LNSetHosekWilkiePropertiesNode' + bl_label = 'Set HosekWilkie Properties' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Turbidity/Ground Albedo': + self.add_input('ArmFloatSocket', 'Turbidity') + self.add_input('ArmFloatSocket', 'Ground Albedo') + if self.property0 == 'Turbidity': + self.add_input('ArmFloatSocket', 'Turbidity') + if self.property0 == 'Ground Albedo': + self.add_input('ArmFloatSocket', 'Ground Albedo') + if self.property0 == 'Sun Direction': + self.add_input('ArmVectorSocket', 'Sun Direction') + + property0: HaxeEnumProperty( + 'property0', + items = [('Turbidity/Ground Albedo', 'Turbidity/Ground Albedo', 'Turbidity, Ground Albedo'), + ('Turbidity', 'Turbidity', 'Turbidity'), + ('Ground Albedo', 'Ground Albedo', 'Ground Albedo'), + ('Sun Direction', 'Sun Direction', 'Sun Direction')], + name='', default='Turbidity/Ground Albedo', update=remove_extra_inputs) + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Turbidity') + self.add_input('ArmFloatSocket', 'Ground_Albedo') + + 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/world/LN_set_nishita_properties.py b/blender/arm/logicnode/world/LN_set_nishita_properties.py new file mode 100644 index 0000000000..9e693bc3f0 --- /dev/null +++ b/blender/arm/logicnode/world/LN_set_nishita_properties.py @@ -0,0 +1,43 @@ +from arm.logicnode.arm_nodes import * + +class SetNishitaPropertiesNode(ArmLogicTreeNode): + """Sets the Nishita properties""" + bl_idname = 'LNSetNishitaPropertiesNode' + bl_label = 'Set Nishita Properties' + arm_version = 1 + + def remove_extra_inputs(self, context): + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'Density': + self.add_input('ArmFloatSocket', 'Air') + self.add_input('ArmFloatSocket', 'Dust') + self.add_input('ArmFloatSocket', 'Ozone') + if self.property0 == 'Air': + self.add_input('ArmFloatSocket', 'Air') + if self.property0 == 'Dust': + self.add_input('ArmFloatSocket', 'Dust') + if self.property0 == 'Ozone': + self.add_input('ArmFloatSocket', 'Ozone') + if self.property0 == 'Sun Direction': + self.add_input('ArmVectorSocket', 'Sun Direction') + + property0: HaxeEnumProperty( + 'property0', + items = [('Density', 'Density', 'Air, Dust, Ozone'), + ('Air', 'Air', 'Air'), + ('Dust', 'Dust', 'Dust'), + ('Ozone', 'Ozone', 'Ozone'), + ('Sun Direction', 'Sun Direction', 'Sun Direction')], + name='', default='Density', update=remove_extra_inputs) + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Air') + self.add_input('ArmFloatSocket', 'Dust') + self.add_input('ArmFloatSocket', 'Ozone') + + 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/world/LN_set_world_strength.py b/blender/arm/logicnode/world/LN_set_world_strength.py new file mode 100644 index 0000000000..3e7fedf6ff --- /dev/null +++ b/blender/arm/logicnode/world/LN_set_world_strength.py @@ -0,0 +1,13 @@ +from arm.logicnode.arm_nodes import * + +class SetWorldStrengthNode(ArmLogicTreeNode): + """Sets the strength of the given World.""" + bl_idname = 'LNSetWorldStrengthNode' + bl_label = 'Set World Strength' + arm_version = 1 + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmFloatSocket', 'Strength') + + self.add_output('ArmNodeSocketAction', 'Out') diff --git a/blender/arm/logicnode/world/__init__.py b/blender/arm/logicnode/world/__init__.py new file mode 100644 index 0000000000..f7b34e3c10 --- /dev/null +++ b/blender/arm/logicnode/world/__init__.py @@ -0,0 +1,3 @@ +from arm.logicnode.arm_nodes import add_node_section + +add_node_section(name='default', category='world') \ No newline at end of file From 97a99d25ad908677fa054caa8de67d6894c90b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:41:44 +0100 Subject: [PATCH 137/175] Add DebugConsole.isDebugConsoleHovered (+ node integration) --- .../logicnode/GetDebugConsoleSettings.hx | 26 ++++++++++------ .../armory/logicnode/GetMouseStartedNode.hx | 10 ++++++ Sources/armory/logicnode/MergedMouseNode.hx | 13 ++++++++ Sources/armory/trait/internal/DebugConsole.hx | 26 ++++++++++++++++ .../logicnode/input/LN_get_mouse_started.py | 25 ++++++++++++++- blender/arm/logicnode/input/LN_mouse.py | 31 +++++++++++++++---- .../LN_get_debug_console_settings.py | 30 ++++++++++++++++-- 7 files changed, 141 insertions(+), 20 deletions(-) diff --git a/Sources/armory/logicnode/GetDebugConsoleSettings.hx b/Sources/armory/logicnode/GetDebugConsoleSettings.hx index ea6778289a..b38b83c604 100644 --- a/Sources/armory/logicnode/GetDebugConsoleSettings.hx +++ b/Sources/armory/logicnode/GetDebugConsoleSettings.hx @@ -1,5 +1,8 @@ package armory.logicnode; + +#if arm_debug import armory.trait.internal.DebugConsole; +#end class GetDebugConsoleSettings extends LogicNode { @@ -8,19 +11,22 @@ class GetDebugConsoleSettings extends LogicNode { } override function get(from: Int): Dynamic { - #if arm_debug switch(from) { - case 0: return armory.trait.internal.DebugConsole.getVisible(); - case 1: return armory.trait.internal.DebugConsole.getScale(); - case 2: { - switch (armory.trait.internal.DebugConsole.getPosition()) { - case PositionStateEnum.Left: return "Left"; - case PositionStateEnum.Center: return "Center"; - case PositionStateEnum.Right: return "Right"; + case 0: return #if arm_debug true #else false #end; + case 1: return #if arm_debug DebugConsole.getVisible() #else false #end; + case 2: return #if arm_debug DebugConsole.isDebugConsoleHovered #else false #end; + case 3: return #if arm_debug DebugConsole.getScale() #else 1.0 #end; + case 4: + #if arm_debug + switch (DebugConsole.getPosition()) { + case PositionStateEnum.Left: return "Left"; + case PositionStateEnum.Center: return "Center"; + case PositionStateEnum.Right: return "Right"; } - } + #else + return ""; + #end } - #end return null; } } diff --git a/Sources/armory/logicnode/GetMouseStartedNode.hx b/Sources/armory/logicnode/GetMouseStartedNode.hx index 8f2d5f3219..67b5080726 100644 --- a/Sources/armory/logicnode/GetMouseStartedNode.hx +++ b/Sources/armory/logicnode/GetMouseStartedNode.hx @@ -2,8 +2,14 @@ package armory.logicnode; import iron.system.Input; +#if arm_debug +import armory.trait.internal.DebugConsole; +#end + class GetMouseStartedNode extends LogicNode { + public var property0: Bool; + var m = Input.getMouse(); var buttonStarted: Null; @@ -14,6 +20,10 @@ class GetMouseStartedNode extends LogicNode { override function run(from: Int) { buttonStarted = null; + #if arm_debug + if (!property0 && DebugConsole.isDebugConsoleHovered) return; + #end + for (b in Mouse.buttons) { if (m.started(b)) { buttonStarted = b; diff --git a/Sources/armory/logicnode/MergedMouseNode.hx b/Sources/armory/logicnode/MergedMouseNode.hx index 40ed2fec34..b78108fdbf 100644 --- a/Sources/armory/logicnode/MergedMouseNode.hx +++ b/Sources/armory/logicnode/MergedMouseNode.hx @@ -1,9 +1,14 @@ package armory.logicnode; +#if arm_debug +import armory.trait.internal.DebugConsole; +#end + class MergedMouseNode extends LogicNode { public var property0: String; public var property1: String; + public var property2: Bool; public function new(tree: LogicTree) { super(tree); @@ -12,6 +17,10 @@ class MergedMouseNode extends LogicNode { } function update() { + #if arm_debug + if (!property2 && DebugConsole.isDebugConsoleHovered && property0 != "moved") return; + #end + var mouse = iron.system.Input.getMouse(); var b = false; switch (property0) { @@ -28,6 +37,10 @@ class MergedMouseNode extends LogicNode { } override function get(from: Int): Dynamic { + #if arm_debug + if (!property2 && DebugConsole.isDebugConsoleHovered && property0 != "moved") return false; + #end + var mouse = iron.system.Input.getMouse(); switch (property0) { case "started": diff --git a/Sources/armory/trait/internal/DebugConsole.hx b/Sources/armory/trait/internal/DebugConsole.hx index 442baba536..9b0a41a4e1 100644 --- a/Sources/armory/trait/internal/DebugConsole.hx +++ b/Sources/armory/trait/internal/DebugConsole.hx @@ -29,6 +29,14 @@ class DebugConsole extends Trait { public static var traceWithPosition = true; public static var fpsAvg = 0.0; + /** + Whether any window of the debug console was hovered in the last drawn frame. + If `visible` is `false`, the value of this variable is also `false`. + **/ + // NOTE If there is more than one debug console for whatever reason + // (technically possible but stupid) this will only work for the last drawn debug console + public static var isDebugConsoleHovered(default, null) = false; + static var ui: Zui; var scaleFactor = 1.0; @@ -69,6 +77,8 @@ class DebugConsole extends Trait { var shortcutScaleIn = kha.input.KeyCode.OpenBracket; var shortcutScaleOut = kha.input.KeyCode.CloseBracket; + var mouse: iron.system.Input.Mouse; + #if arm_shadowmap_atlas var lightColorMap: Map = new Map(); var lightColorMapCount = 0; @@ -83,6 +93,7 @@ class DebugConsole extends Trait { keyCodeScaleOut = kha.input.KeyCode.CloseBracket) { super(); this.scaleFactor = scaleFactor; + this.mouse = iron.system.Input.getMouse(); DebugConsole.traceWithPosition = traceWithPosition == 1; iron.data.Data.getFont(Canvas.defaultFontName, function(font: kha.Font) { @@ -233,7 +244,9 @@ class DebugConsole extends Trait { frameTime = Scheduler.realTime() - lastTime; lastTime = Scheduler.realTime(); + isDebugConsoleHovered = false; if (!visible) return; + var ww = Std.int(280 * scaleFactor * getScale()); // RIGHT var wx = iron.App.w() - ww; @@ -251,6 +264,7 @@ class DebugConsole extends Trait { if (bindG) g.end(); ui.begin(g); + if (ui.window(hwin, wx, wy, ww, wh, true)) { if (ui.tab(htab, "")) {} @@ -908,6 +922,7 @@ class DebugConsole extends Trait { ui.separator(); } + isDebugConsoleHovered = isDebugConsoleHovered || isZuiWindowHovered(hwin, wx, wy); // Draw trait debug windows var handleWinTrait = Id.handle(); @@ -955,6 +970,7 @@ class DebugConsole extends Trait { ui.unindent(); } + isDebugConsoleHovered = isDebugConsoleHovered || isZuiWindowHovered(handleWindow, wx, wy); } ui.end(bindG); @@ -990,6 +1006,16 @@ class DebugConsole extends Trait { #end } + function isZuiWindowHovered(hwin: zui.Zui.Handle, wx: Int, wy: Int): Bool { + var mouseWindowSpaceX = mouse.x - wx - hwin.dragX; + var mouseWindowSpaceY = mouse.y - wy - hwin.dragY; + + return ( + mouseWindowSpaceX >= 0 && mouseWindowSpaceX < hwin.lastMaxX + && mouseWindowSpaceY >= 0 && mouseWindowSpaceY < hwin.lastMaxY + ); + } + static function roundfp(f: Float, precision = 2): Float { f *= Math.pow(10, precision); return Math.round(f) / Math.pow(10, precision); diff --git a/blender/arm/logicnode/input/LN_get_mouse_started.py b/blender/arm/logicnode/input/LN_get_mouse_started.py index 9a6fce1f3f..525b22143b 100644 --- a/blender/arm/logicnode/input/LN_get_mouse_started.py +++ b/blender/arm/logicnode/input/LN_get_mouse_started.py @@ -1,13 +1,36 @@ from arm.logicnode.arm_nodes import * + class GetMouseStartedNode(ArmLogicTreeNode): """.""" bl_idname = 'LNGetMouseStartedNode' bl_label = 'Get Mouse Started' - arm_version = 1 + arm_version = 2 + + property0: HaxeBoolProperty( + 'property0', + name='Include Debug Console', + description=( + 'If disabled, this node does not react to mouse press events' + ' over the debug console area. Enable this option to catch those events' + ) + ) def arm_init(self, context): self.add_input('ArmNodeSocketAction', 'In') self.add_output('ArmNodeSocketAction', 'Out') self.add_output('ArmStringSocket', 'Button') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNGetMouseStartedNode', self.arm_version, 'LNGetMouseStartedNode', 2, + in_socket_mapping={0: 0}, out_socket_mapping={0: 0, 1: 1}, + property_defaults={'property0': True} + ) diff --git a/blender/arm/logicnode/input/LN_mouse.py b/blender/arm/logicnode/input/LN_mouse.py index 1b4953cb04..17ba533f1c 100644 --- a/blender/arm/logicnode/input/LN_mouse.py +++ b/blender/arm/logicnode/input/LN_mouse.py @@ -6,7 +6,7 @@ class MouseNode(ArmLogicTreeNode): bl_idname = 'LNMergedMouseNode' bl_label = 'Mouse' arm_section = 'mouse' - arm_version = 2 + arm_version = 3 property0: HaxeEnumProperty( 'property0', @@ -23,6 +23,14 @@ class MouseNode(ArmLogicTreeNode): ('side1', 'Side 1', 'Side 1 mouse button'), ('side2', 'Side 2', 'Side 2 mouse button')], name='', default='left') + property2: HaxeBoolProperty( + 'property2', + name='Include Debug Console', + description=( + 'If disabled, this node does not react to mouse press events' + ' over the debug console area. Enable this option to catch those events' + ) + ) def arm_init(self, context): self.add_output('ArmNodeSocketAction', 'Out') @@ -30,13 +38,24 @@ def arm_init(self, context): def draw_buttons(self, context, layout): layout.prop(self, 'property0') - layout.prop(self, 'property1') + + if self.property0 != 'moved': + layout.prop(self, 'property1') + layout.prop(self, 'property2') def draw_label(self) -> str: return f'{self.bl_label}: {self.property1}' def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 1): - raise LookupError() - - return NodeReplacement.Identity(self) + if 0 <= self.arm_version < 2: + return NodeReplacement.Identity(self) + + elif self.arm_version == 2: + return NodeReplacement( + 'LNMergedMouseNode', self.arm_version, 'LNMergedMouseNode', 3, + in_socket_mapping={}, out_socket_mapping={0: 0, 1: 1}, + property_mapping={'property0': 'property0', 'property1': 'property1'}, + property_defaults={'property2': True} + ) + + raise LookupError() diff --git a/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py b/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py index e6e04394b3..789e80f0dc 100644 --- a/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py +++ b/blender/arm/logicnode/miscellaneous/LN_get_debug_console_settings.py @@ -1,12 +1,36 @@ from arm.logicnode.arm_nodes import * + class GetDebugConsoleSettings(ArmLogicTreeNode): - """Returns the debug console settings.""" + """Return properties of the debug console. + + @output Enabled: Whether the debug console is enabled. + @output Visible: Whether the debug console is visible, + or `false` if the debug console is disabled. + @output Hovered: Whether the debug console is hovered by the mouse cursor, + or `false` if the debug console is disabled. + @output UI Scale: The scaling factor of the debug console user interface, + or `1.0` if the debug console is disabled. + @output Position: The initial position of the debug console. + Possible values if the debug console is enabled: `"Left"`, `"Center"`, `"Right"`. + If the debug console is disabled, the returned value is an empty string `""`. + """ bl_idname = 'LNGetDebugConsoleSettings' bl_label = 'Get Debug Console Settings' - arm_version = 1 + arm_version = 2 def arm_init(self, context): + self.add_output('ArmBoolSocket', 'Enabled') self.add_output('ArmBoolSocket', 'Visible') - self.add_output('ArmFloatSocket', 'Scale') + self.add_output('ArmBoolSocket', 'Hovered') + self.add_output('ArmFloatSocket', 'UI Scale') self.add_output('ArmStringSocket', 'Position') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNGetDebugConsoleSettings', self.arm_version, 'LNGetDebugConsoleSettings', 2, + in_socket_mapping={}, out_socket_mapping={0: 1, 1: 3, 2: 4} + ) From efb9d75c8f28d7cc23bd651fb0f7a7d012480670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:14:21 +0100 Subject: [PATCH 138/175] Cleanup whitespace --- Sources/armory/logicnode/GamepadSticksNode.hx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/armory/logicnode/GamepadSticksNode.hx b/Sources/armory/logicnode/GamepadSticksNode.hx index 8d2bad8dbd..84834c09f6 100644 --- a/Sources/armory/logicnode/GamepadSticksNode.hx +++ b/Sources/armory/logicnode/GamepadSticksNode.hx @@ -7,7 +7,7 @@ class GamepadSticksNode extends LogicNode { public var property2: String; var started = false; var previousb = false; - + var gstarted = false; var gpreviousb = false; @@ -64,17 +64,17 @@ class GamepadSticksNode extends LogicNode { if (b) previousb = b; if (b != previousb) started = false; - + if (property0 == 'Started' && b && !started){started = true; runOutput(0);} else if (property0 == 'Down' && b){previousb = b; runOutput(0);} else if (property0 == 'Released' && b != previousb){previousb = b; runOutput(0);} - + } override function get(from: Int): Dynamic { var num: Int = inputs[0].get(); var gamepad = iron.system.Input.getGamepad(num); - + if (gamepad == null) return false; var b = false; @@ -116,15 +116,15 @@ class GamepadSticksNode extends LogicNode { case 'down-right': b = gamepad.rightStick.y == 1 && gamepad.rightStick.x == 1; } - + if (b) gpreviousb = b; if (b != gpreviousb) gstarted = false; - + if (property0 == 'Started' && b && !gstarted){gstarted = true; return true;} else if (property0 == 'Down' && b){gpreviousb = b; return true;} else if (property0 == 'Released' && b != gpreviousb){gpreviousb = b; return true;} - + return false; - + } } From 62ebcdd7db0b854569b3d4dd9f382d24aba8474a Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:25:07 -0300 Subject: [PATCH 139/175] Update LN_play_sound.py fix version --- blender/arm/logicnode/sound/LN_play_sound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/sound/LN_play_sound.py b/blender/arm/logicnode/sound/LN_play_sound.py index 6a8ae19078..6369aeb900 100644 --- a/blender/arm/logicnode/sound/LN_play_sound.py +++ b/blender/arm/logicnode/sound/LN_play_sound.py @@ -112,7 +112,7 @@ def draw_buttons(self, context, layout): row.prop(self, 'property4') def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 1): + if self.arm_version not in (0, 2): raise LookupError() return NodeReplacement.Identity(self) From a67b5f5222007bf527a02544da3b78c3d249bfa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:23:09 +0100 Subject: [PATCH 140/175] Play Sound node: fix version check once more --- blender/arm/logicnode/sound/LN_play_sound.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/logicnode/sound/LN_play_sound.py b/blender/arm/logicnode/sound/LN_play_sound.py index 6369aeb900..10496526c6 100644 --- a/blender/arm/logicnode/sound/LN_play_sound.py +++ b/blender/arm/logicnode/sound/LN_play_sound.py @@ -112,7 +112,7 @@ def draw_buttons(self, context, layout): row.prop(self, 'property4') def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.arm_version not in (0, 2): - raise LookupError() + if 0 <= self.arm_version <= 2: + return NodeReplacement.Identity(self) - return NodeReplacement.Identity(self) + raise LookupError() From 6c09dbbe951180ece756c28d0c3cb600bd35f728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:23:38 +0100 Subject: [PATCH 141/175] Whitespace cleanup --- blender/arm/logicnode/sound/LN_play_sound.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/blender/arm/logicnode/sound/LN_play_sound.py b/blender/arm/logicnode/sound/LN_play_sound.py index 10496526c6..a7cd5157b0 100644 --- a/blender/arm/logicnode/sound/LN_play_sound.py +++ b/blender/arm/logicnode/sound/LN_play_sound.py @@ -42,7 +42,7 @@ def remove_extra_inputs(self, context): self.add_input('ArmStringSocket', 'Sound Name') property0: HaxePointerProperty('property0', name='', type=bpy.types.Sound) - + property1: HaxeBoolProperty( 'property1', name='Loop', @@ -78,7 +78,6 @@ def remove_extra_inputs(self, context): name='', default='Sound', update=remove_extra_inputs) def arm_init(self, context): - self.add_input('ArmNodeSocketAction', 'Play') self.add_input('ArmNodeSocketAction', 'Pause') self.add_input('ArmNodeSocketAction', 'Stop') @@ -90,14 +89,13 @@ def arm_init(self, context): self.add_output('ArmNodeSocketAction', 'Done') def draw_buttons(self, context, layout): - layout.prop(self, 'property6') col = layout.column(align=True) - + if self.property6 == 'Sound': col.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='') - + col.prop(self, 'property5') col.prop(self, 'property1') col.prop(self, 'property2') From 860e67cae93acf8a7598f45a916f75ef91786deb Mon Sep 17 00:00:00 2001 From: t3du <32546729+t3du@users.noreply.github.com> Date: Sun, 24 Dec 2023 11:34:32 -0300 Subject: [PATCH 142/175] fix sound load and retrigger --- Sources/armory/logicnode/PlaySoundRawNode.hx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/armory/logicnode/PlaySoundRawNode.hx b/Sources/armory/logicnode/PlaySoundRawNode.hx index c5a4df99c5..780fefddaf 100644 --- a/Sources/armory/logicnode/PlaySoundRawNode.hx +++ b/Sources/armory/logicnode/PlaySoundRawNode.hx @@ -27,7 +27,7 @@ class PlaySoundRawNode extends LogicNode { override function run(from: Int) { switch (from) { case Play: - if (sound == null) { + if (property6 == 'Sound' ? sound == null : true) { iron.data.Data.getSound(property6 == 'Sound' ? property0 : inputs[5].get(), function(s: kha.Sound) { this.sound = s; }); @@ -36,6 +36,8 @@ class PlaySoundRawNode extends LogicNode { // Resume if (channel != null) { if (property2) channel.stop(); + if (property6 == 'Sound Name') + channel = iron.system.Audio.play(sound, property1, property5); channel.play(); channel.volume = inputs[4].get(); } From d804eba16a09e04a4c22bcb5428ac4720ec0ab24 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Wed, 3 Jan 2024 17:38:40 +0100 Subject: [PATCH 143/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index a37ffcb4d9..d246d408f1 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2023.12' +arm_version = '2024.1' arm_commit = '$Id$' def get_project_html5_copy(self): From e96dc520560b91ee86d4b41a32a95197b1d7e501 Mon Sep 17 00:00:00 2001 From: Yvain Date: Thu, 4 Jan 2024 23:37:18 +0100 Subject: [PATCH 144/175] normalize viewPos, rid of refractive object's reflection's artifacts. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 57 +++++++-------- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 85 +++++++++++------------ Shaders/water_pass/water_pass.frag.glsl | 25 ++++--- 3 files changed, 83 insertions(+), 84 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 4037e7420c..138056afdd 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -27,14 +27,13 @@ float depth; const int numBinarySearchSteps = 7; const int maxSteps = int(ceil(1.0 / ssrRayStep) * ssrSearchDist); - vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); projectedCoord.xy /= projectedCoord.w; projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; -#ifdef _InvY + #ifdef _InvY projectedCoord.y = 1.0 - projectedCoord.y; -#endif + #endif return projectedCoord.xy; } @@ -46,27 +45,27 @@ float getDeltaDepth(const vec3 hit) { vec4 binarySearch(vec3 dir) { float ddepth; - for (int i = 0; i < 7; i++) { + for (int i = 0; i < numBinarySearchSteps; i++) { dir *= 0.5; hitCoord -= dir; ddepth = getDeltaDepth(hitCoord); if (ddepth < 0.0) hitCoord += dir; } // Ugly discard of hits too far away -#ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z) return vec4(0.0); -#else - if (abs(ddepth) > ssrSearchDist) return vec4(0.0); -#endif + #ifdef _CPostprocess + if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); + #else + if (abs(ddepth) > ssrSearchDist / 500) return vec4(0.0); + #endif return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } vec4 rayCast(vec3 dir) { -#ifdef _CPostprocess - dir *= PPComp9.x; -#else - dir *= ssrRayStep; -#endif + #ifdef _CPostprocess + dir *= PPComp9.x; + #else + dir *= ssrRayStep; + #endif for (int i = 0; i < maxSteps; i++) { hitCoord += dir; if (getDeltaDepth(hitCoord) > 0.0) return binarySearch(dir); @@ -82,8 +81,8 @@ void main() { float spec = fract(textureLod(gbuffer1, texCoord, 0.0).a); if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } - depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; - if (depth == 1.0) { fragColor.rgb = vec3(0.0); return; } + float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + if (d == 1.0) { fragColor.rgb = vec3(0.0); return; } vec2 enc = g0.rg; vec3 n; @@ -92,26 +91,28 @@ void main() { n = normalize(n); vec3 viewNormal = V3 * n; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 viewPos = getPosView(viewRay, d, cameraProj); vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; -#ifdef _CPostprocess - vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; -#else - vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; -#endif + #ifdef _CPostprocess + vec3 dir = reflected * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; + #else + vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; + #endif + // * max(ssrMinRayStep, -viewPos.z) vec4 coords = rayCast(dir); + vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); float screenEdgeFactor = clamp(1.0 - (deltaCoords.x + deltaCoords.y), 0.0, 1.0); - float reflectivity = 1.0 - roughness; -#ifdef _CPostprocess - float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; -#else - float intensity = pow(reflectivity, ssrFalloffExp) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((ssrSearchDist - length(viewPos - hitCoord)) * (1.0 / ssrSearchDist), 0.0, 1.0) * coords.w; -#endif + float reflectivity = 1.0 - roughness; + #ifdef _CPostprocess + float intensity = pow(reflectivity, PPComp10.x) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; + #else + float intensity = pow(reflectivity, ssrFalloffExp) * screenEdgeFactor * clamp(-reflected.z, 0.0, 1.0) * clamp((ssrSearchDist - length(viewPos - hitCoord)) * (1.0 / ssrSearchDist), 0.0, 1.0) * coords.w; + #endif intensity = clamp(intensity, 0.0, 1.0); vec3 reflCol = textureLod(tex, coords.xy, 0.0).rgb; diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index aa57479182..b0cb35f5ec 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -4,9 +4,6 @@ #include "std/math.glsl" #include "std/gbuffer.glsl" -in vec2 texCoord; -out vec4 fragColor; - uniform sampler2D tex; uniform sampler2D tex1; uniform sampler2D gbufferD; @@ -23,59 +20,61 @@ uniform vec3 PPComp10; #endif in vec3 viewRay; +in vec2 texCoord; +out vec4 fragColor; + vec3 hitCoord; float depth; -vec3 viewPos; +const int numBinarySearchSteps = 7; const int maxSteps = int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist); vec2 getProjectedCoord(const vec3 hit) { - vec4 projectedCoord = P * vec4(hit, 1.0); - projectedCoord.xy /= projectedCoord.w; - projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; + vec4 projectedCoord = P * vec4(hit, 1.0); + projectedCoord.xy /= projectedCoord.w; + projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; #ifdef _InvY - projectedCoord.y = 1.0 - projectedCoord.y; + projectedCoord.y = 1.0 - projectedCoord.y; #endif - return projectedCoord.xy; + return projectedCoord.xy; } float getDeltaDepth(const vec3 hit) { - depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); - return viewPos.z - hit.z; + depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); + return viewPos.z - hit.z; } +/* vec4 binarySearch(vec3 dir) { - float d; - for (int i = 0; i < 7; i++) { - dir *= 0.5; - hitCoord -= dir; - d = getDeltaDepth(hitCoord); - if (d < 0.0) - hitCoord += dir; - } - // Ugly discard of hits too far away -#ifdef _CPostprocess - if (abs(d) > PPComp9.z) return vec4(texCoord, 0.0, 1.0); -#else - if (abs(d) > ss_refractionSearchDist) return vec4(texCoord, 0.0, 1.0); -#endif - return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); + float ddepth; + for (int i = 0; i < numBinarySearchSteps; i++) { + dir *= 0.5; + hitCoord -= dir; + ddepth = getDeltaDepth(hitCoord); + if (ddepth < 0.0) hitCoord += dir; + } + // Ugly discard of hits too far away + #ifdef _CPostprocess + if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); + #else + if (abs(ddepth) > ss_refractionSearchDist / 500) return vec4(0.0); + #endif + return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } +*/ vec4 rayCast(vec3 dir) { - float d; -#ifdef _CPostprocess - dir *= PPComp9.x; -#else - dir *= ss_refractionRayStep; -#endif - for (int i = 0; i < maxSteps; i++) { - hitCoord += dir; - d = getDeltaDepth(hitCoord); - if (d > 0.0) return vec4(getProjectedCoord(dir), 0.0, 1.0); - } - return vec4(texCoord, 0.0, 1.0); + #ifdef _CPostprocess + dir *= PPComp9.x; + #else + dir *= ss_refractionRayStep; + #endif + for (int i = 0; i < maxSteps; i++) { + hitCoord += dir; + if (getDeltaDepth(hitCoord) > 0.0) return vec4(getProjectedCoord(dir), 0.0, 1.0); + } + return vec4(0.0); } void main() { @@ -85,9 +84,9 @@ void main() { float ior = gr.x; float opac = gr.y; - depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; - if (depth == 1.0 || ior == 1.0 || opac == 1.0) { + if (d == 1.0 || ior == 1.0 || opac == 1.0) { fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; return; } @@ -99,7 +98,7 @@ void main() { n = normalize(n); vec3 viewNormal = V3 * n; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + vec3 viewPos = getPosView(viewRay, d, cameraProj); vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); hitCoord = viewPos; @@ -116,7 +115,7 @@ void main() { float refractivity = 1.0; #ifdef _CPostprocess - float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; + float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp(-refracted.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; #else float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((ss_refractionSearchDist - length(viewPos - hitCoord)) * (1.0 / ss_refractionSearchDist), 0.0, 1.0) * coords.w; #endif diff --git a/Shaders/water_pass/water_pass.frag.glsl b/Shaders/water_pass/water_pass.frag.glsl index 65451c35dc..2bf016bb0f 100644 --- a/Shaders/water_pass/water_pass.frag.glsl +++ b/Shaders/water_pass/water_pass.frag.glsl @@ -39,7 +39,7 @@ vec3 hitCoord; float depth; const int numBinarySearchSteps = 7; -#define maxSteps int(ceil(1.0 / ssrRayStep) * ssrSearchDist) +const int maxSteps = int(ceil(1.0 / ssrRayStep) * ssrSearchDist); vec2 getProjectedCoord(const vec3 hit) { vec4 projectedCoord = P * vec4(hit, 1.0); @@ -53,41 +53,40 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = normalize(getPosView(viewRay, depth, cameraProj)); + vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } vec4 binarySearch(vec3 dir) { float ddepth; - for (int i = 0; i < 7; i++) { + for (int i = 0; i < numBinarySearchSteps; i++) { dir *= 0.5; hitCoord -= dir; ddepth = getDeltaDepth(hitCoord); if (ddepth < 0.0) hitCoord += dir; } // Ugly discard of hits too far away -#ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z) return vec4(0.0); -#else - if (abs(ddepth) > ssrSearchDist) return vec4(0.0); -#endif + #ifdef _CPostprocess + if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); + #else + if (abs(ddepth) > ssrSearchDist / 500) return vec4(0.0); + #endif return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } vec4 rayCast(vec3 dir) { #ifdef _CPostprocess - dir *= PPComp9.x; + dir *= PPComp9.x; #else - dir *= ssrRayStep; + dir *= ssrRayStep; #endif - for (int i = 0; i < maxSteps; i++) - { + for (int i = 0; i < maxSteps; i++) { hitCoord += dir; if (getDeltaDepth(hitCoord) > 0.0) return binarySearch(dir); } return vec4(0.0); } -#endif +#endif //SSR void main() { float gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; From 8cfd1cc1758d201d682e41287e3ae338c6df53f0 Mon Sep 17 00:00:00 2001 From: Yvain Date: Sun, 7 Jan 2024 18:01:03 +0100 Subject: [PATCH 145/175] reimplement binary search. testing delta depth against fragment's depth solves the issue. --- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index b0cb35f5ec..cc0231f323 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -39,32 +39,35 @@ vec2 getProjectedCoord(const vec3 hit) { return projectedCoord.xy; } + float getDeltaDepth(const vec3 hit) { depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } -/* + vec4 binarySearch(vec3 dir) { float ddepth; for (int i = 0; i < numBinarySearchSteps; i++) { dir *= 0.5; hitCoord -= dir; ddepth = getDeltaDepth(hitCoord); - if (ddepth < 0.0) hitCoord += dir; + if (ddepth < depth) hitCoord += dir; } // Ugly discard of hits too far away + //using a divider of 500 doesn't work here unless the distance is set to at least 500 for a blender unit. #ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z / 500) return vec4(0.0); + if (abs(ddepth) > PPComp9.z) return vec4(texCoord.xy, 0.0, 0.0); #else - if (abs(ddepth) > ss_refractionSearchDist / 500) return vec4(0.0); + if (abs(ddepth) > ss_refractionSearchDist) return vec4(texCoord.xy, 0.0, 0.0); #endif return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } -*/ + vec4 rayCast(vec3 dir) { + float ddepth; #ifdef _CPostprocess dir *= PPComp9.x; #else @@ -72,7 +75,8 @@ vec4 rayCast(vec3 dir) { #endif for (int i = 0; i < maxSteps; i++) { hitCoord += dir; - if (getDeltaDepth(hitCoord) > 0.0) return vec4(getProjectedCoord(dir), 0.0, 1.0); + ddepth = getDeltaDepth(hitCoord); + if (ddepth > depth) return binarySearch(dir); } return vec4(0.0); } From cc35a809d1ab0bf8af892773a75df7f4cdcf63e5 Mon Sep 17 00:00:00 2001 From: Yvain Date: Sun, 7 Jan 2024 18:51:14 +0100 Subject: [PATCH 146/175] =?UTF-8?q?alles=20kl=C3=A4r.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index cc0231f323..a1e2567a28 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -24,7 +24,7 @@ in vec2 texCoord; out vec4 fragColor; vec3 hitCoord; -float depth; +float d; const int numBinarySearchSteps = 7; const int maxSteps = int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist); @@ -41,8 +41,8 @@ vec2 getProjectedCoord(const vec3 hit) { float getDeltaDepth(const vec3 hit) { - depth = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = getPosView(viewRay, depth, cameraProj); + d = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = normalize(getPosView(viewRay, d, cameraProj)); return viewPos.z - hit.z; } @@ -53,7 +53,7 @@ vec4 binarySearch(vec3 dir) { dir *= 0.5; hitCoord -= dir; ddepth = getDeltaDepth(hitCoord); - if (ddepth < depth) hitCoord += dir; + if (ddepth < 0.0) hitCoord += dir; } // Ugly discard of hits too far away //using a divider of 500 doesn't work here unless the distance is set to at least 500 for a blender unit. @@ -76,7 +76,7 @@ vec4 rayCast(vec3 dir) { for (int i = 0; i < maxSteps; i++) { hitCoord += dir; ddepth = getDeltaDepth(hitCoord); - if (ddepth > depth) return binarySearch(dir); + if (ddepth > d) return binarySearch(dir); } return vec4(0.0); } @@ -88,7 +88,7 @@ void main() { float ior = gr.x; float opac = gr.y; - float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; if (d == 1.0 || ior == 1.0 || opac == 1.0) { fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; @@ -102,8 +102,8 @@ void main() { n = normalize(n); vec3 viewNormal = V3 * n; - vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); + vec3 viewPos = normalize(getPosView(viewRay, d, cameraProj)); + vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); hitCoord = viewPos; #ifdef _CPostprocess From 7114d78769344c4240d171feeb489e16dc61706f Mon Sep 17 00:00:00 2001 From: Yvain Date: Sun, 14 Jan 2024 13:20:21 +0100 Subject: [PATCH 147/175] rollback index values for material mix. simplify make_mesh.py. --- blender/arm/material/cycles_nodes/nodes_shader.py | 4 ++-- blender/arm/material/make_mesh.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index 2a403ab5b9..449e679497 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -35,11 +35,11 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var)) mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[0]) + bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[1]) ek1 = mat_state.emission_type mat_state.emission_type = mat_state.EmissionType.NO_EMISSION - bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[1]) + bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[2]) ek2 = mat_state.emission_type if state.parse_surface: diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 7a436ef45e..50eb059a15 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -548,21 +548,18 @@ def make_forward(con_mesh): if rpdat.rp_ss_refraction: mrt += 1 if mrt != 0: - idx_refraction = 1 # Store light gbuffer for post-processing frag.add_out(f'vec4 fragColor[{mrt}+1]') frag.add_include('std/gbuffer.glsl') frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') - if rpdat.rp_ssr: - frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') - idx_refraction += 1 + frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') if rpdat.rp_ss_refraction: if parse_opacity: - frag.write(f'fragColor[{idx_refraction}] = vec4(ior, opacity, 0.0, 0.0);') + frag.write(f'fragColor[2] = vec4(ior, opacity, 0.0, 0.0);') else: - frag.write(f'fragColor[{idx_refraction}] = vec4(1.0, 1.0, 0.0, 0.0);') + frag.write(f'fragColor[2] = vec4(1.0, 1.0, 0.0, 0.0);') else: frag.add_out('vec4 fragColor[1]') From c73363819de643b6adfcf52ce04ec3f63f3431f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:59:25 +0100 Subject: [PATCH 148/175] Fix rendering of two sided materials in some render paths --- blender/arm/material/make_mesh.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 50eb059a15..ad7d0d0fc7 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -151,6 +151,9 @@ def make_base(con_mesh, parse_opacity): make_attrib.write_norpos(con_mesh, vert) frag.write_attrib('vec3 n = normalize(wnormal);') + if mat_state.material.arm_two_sided: + frag.write('if (!gl_FrontFacing) n *= -1;') # Flip normal when drawing back-face + if not is_displacement and not vattr_written: make_attrib.write_vertpos(vert) @@ -240,9 +243,6 @@ def make_deferred(con_mesh, rpasses): # Pack gbuffer frag.add_include('std/gbuffer.glsl') - if mat_state.material.arm_two_sided: - frag.write('if (!gl_FrontFacing) n *= -1;') # Flip normal when drawing back-face - frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') @@ -342,6 +342,9 @@ def make_forward_mobile(con_mesh): make_attrib.write_norpos(con_mesh, vert) frag.write_attrib('vec3 n = normalize(wnormal);') + if mat_state.material.arm_two_sided: + frag.write('if (!gl_FrontFacing) n *= -1;') # Flip normal when drawing back-face + make_attrib.write_vertpos(vert) frag.add_include('std/math.glsl') From 92af75497d578c39c7709398b1481b8c1b995cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= <17685000+MoritzBrueckner@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:33:44 +0100 Subject: [PATCH 149/175] Fix INF values in cloud shader (causing Bloom flickering) --- blender/arm/make_world.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index aedc1ce73e..c239d45122 100644 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -311,6 +311,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): frag.add_const('float', 'cloudsSteps', str(round(world.arm_clouds_steps * 100) / 100)) frag.add_function('''float remap(float old_val, float old_min, float old_max, float new_min, float new_max) { +\tif (old_max == old_min) return 0.0; \treturn new_min + (((old_val - old_min) / (old_max - old_min)) * (new_max - new_min)); }''') From 743903796cd9af2e61d79b9746e156c641f6cc39 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Sat, 3 Feb 2024 03:01:26 -0500 Subject: [PATCH 150/175] added linked file support --- blender/arm/exporter.py | 58 ++++++++++++++++++++++++++++++++++++++--- blender/arm/make.py | 33 ++++++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index bd73697535..c2952df108 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -29,6 +29,17 @@ from arm import assets, exporter_opt, log, make_renderpath from arm.material import cycles, make as make_material, mat_batch +# Global storage for the new file path +new_file_path = None +original_filename = None + +def get_exported_file_path(): + """Function to access the global new_file_path and original_filename.""" + global new_file_path, original_filename + print(f"Retrieving file path: {new_file_path}") + print(f"Retrieving original filename: {original_filename}") + return new_file_path, original_filename + if arm.is_reload(__name__): assets = arm.reload_module(assets) exporter_opt = arm.reload_module(exporter_opt) @@ -2344,15 +2355,54 @@ def export_objects(self, scene): def execute(self): """Exports the scene.""" + global new_file_path + global original_filename + profile_time = time.time() wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting ' + arm.utils.asset_name(self.scene)) if self.compress_enabled: print('Scene data will be compressed which might take a while.') - + current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe - + + # Save current file path + original_file_path = bpy.data.filepath + original_filename = original_file_path + + print(f'Saved filepath {original_file_path}') + + # Create new file path + savedcopy_file_path = original_file_path.replace('.blend', '-original.blend') + + print(f"Copying file to: {savedcopy_file_path}") + + # Set global variable to the new file so we can pass it to make.py. + new_file_path = savedcopy_file_path + + print(f"Setting new file path: {new_file_path}") + + # Save a copy of the current file + bpy.ops.wm.save_as_mainfile(filepath=savedcopy_file_path, copy=True) + + # Select all collections and make duplicates real + print("Making linked duplicates real - Start") + + # Code to make linked duplicates real + for obj in bpy.context.scene.objects: + if obj.instance_type == 'COLLECTION': + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.duplicates_make_real(use_hierarchy=True) + obj.select_set(False) + + # Debug line for execution completion + print("Making linked duplicates real - Complete") + + # Save changes to the duplicated file + bpy.ops.wm.save_mainfile() + scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() # bobject => blender object for bobject in scene_objects: @@ -2525,9 +2575,11 @@ def execute(self): # Restore frame if self.scene.frame_current != current_frame: self.scene.frame_set(current_frame, subframe=current_subframe) - + + if wrd.arm_verbose_output: print('Scene exported in {:0.3f}s'.format(time.time() - profile_time)) + def create_default_camera(self, is_viewport_camera=False): """Creates the default camera and adds a WalkNavigation trait to it.""" diff --git a/blender/arm/make.py b/blender/arm/make.py index b633bdc34b..0ce9dfa472 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -31,6 +31,9 @@ import arm.utils_vs import arm.write_data as write_data +#Used for scene duplication and restoration. +from arm.exporter import get_exported_file_path + if arm.is_reload(__name__): assets = arm.reload_module(assets) arm.exporter = arm.reload_module(arm.exporter) @@ -500,7 +503,35 @@ def play_done(): state.redraw_ui = True log.clear() live_patch.stop() - + + # Call the new function for handling file operations at the end of play_done + handle_reloading_originals() + + +def handle_reloading_originals(): + """Handles the file operations of opening, saving, and deleting files.""" + new_file_path, original_filename = get_exported_file_path() + if new_file_path: + bpy.ops.wm.open_mainfile(filepath=new_file_path) + print("Loaded original file copy.") + if original_filename and os.path.exists(original_filename): + try: + os.remove(original_filename) + print(f"Deleted original file at: {original_filename}") + bpy.ops.wm.save_as_mainfile(filepath=original_filename) + print(f"Saved the file to the original filename: {original_filename}") + except OSError as e: + print(f"Error deleting {original_filename}: {e.strerror}") + + if new_file_path and os.path.exists(new_file_path) and new_file_path != original_filename: + try: + os.remove(new_file_path) + print(f"Deleted temporary file at: {new_file_path}") + except OSError as e: + print(f"Error deleting {new_file_path}: {e.strerror}") + else: + print("Failed to retrieve new file path.") + def assets_done(): if state.proc_build == None: return From c7214f5f79152383a76eed5b56f94c482c504ac0 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Sun, 4 Feb 2024 07:19:30 -0500 Subject: [PATCH 151/175] Revert "added linked file support" This reverts commit 743903796cd9af2e61d79b9746e156c641f6cc39. --- blender/arm/exporter.py | 58 +++-------------------------------------- blender/arm/make.py | 33 +---------------------- 2 files changed, 4 insertions(+), 87 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index c2952df108..bd73697535 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -29,17 +29,6 @@ from arm import assets, exporter_opt, log, make_renderpath from arm.material import cycles, make as make_material, mat_batch -# Global storage for the new file path -new_file_path = None -original_filename = None - -def get_exported_file_path(): - """Function to access the global new_file_path and original_filename.""" - global new_file_path, original_filename - print(f"Retrieving file path: {new_file_path}") - print(f"Retrieving original filename: {original_filename}") - return new_file_path, original_filename - if arm.is_reload(__name__): assets = arm.reload_module(assets) exporter_opt = arm.reload_module(exporter_opt) @@ -2355,54 +2344,15 @@ def export_objects(self, scene): def execute(self): """Exports the scene.""" - global new_file_path - global original_filename - profile_time = time.time() wrd = bpy.data.worlds['Arm'] if wrd.arm_verbose_output: print('Exporting ' + arm.utils.asset_name(self.scene)) if self.compress_enabled: print('Scene data will be compressed which might take a while.') - + current_frame, current_subframe = self.scene.frame_current, self.scene.frame_subframe - - # Save current file path - original_file_path = bpy.data.filepath - original_filename = original_file_path - - print(f'Saved filepath {original_file_path}') - - # Create new file path - savedcopy_file_path = original_file_path.replace('.blend', '-original.blend') - - print(f"Copying file to: {savedcopy_file_path}") - - # Set global variable to the new file so we can pass it to make.py. - new_file_path = savedcopy_file_path - - print(f"Setting new file path: {new_file_path}") - - # Save a copy of the current file - bpy.ops.wm.save_as_mainfile(filepath=savedcopy_file_path, copy=True) - - # Select all collections and make duplicates real - print("Making linked duplicates real - Start") - - # Code to make linked duplicates real - for obj in bpy.context.scene.objects: - if obj.instance_type == 'COLLECTION': - bpy.context.view_layer.objects.active = obj - obj.select_set(True) - bpy.ops.object.duplicates_make_real(use_hierarchy=True) - obj.select_set(False) - - # Debug line for execution completion - print("Making linked duplicates real - Complete") - - # Save changes to the duplicated file - bpy.ops.wm.save_mainfile() - + scene_objects: List[bpy.types.Object] = self.scene.collection.all_objects.values() # bobject => blender object for bobject in scene_objects: @@ -2575,11 +2525,9 @@ def execute(self): # Restore frame if self.scene.frame_current != current_frame: self.scene.frame_set(current_frame, subframe=current_subframe) - - + if wrd.arm_verbose_output: print('Scene exported in {:0.3f}s'.format(time.time() - profile_time)) - def create_default_camera(self, is_viewport_camera=False): """Creates the default camera and adds a WalkNavigation trait to it.""" diff --git a/blender/arm/make.py b/blender/arm/make.py index 0ce9dfa472..b633bdc34b 100644 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -31,9 +31,6 @@ import arm.utils_vs import arm.write_data as write_data -#Used for scene duplication and restoration. -from arm.exporter import get_exported_file_path - if arm.is_reload(__name__): assets = arm.reload_module(assets) arm.exporter = arm.reload_module(arm.exporter) @@ -503,35 +500,7 @@ def play_done(): state.redraw_ui = True log.clear() live_patch.stop() - - # Call the new function for handling file operations at the end of play_done - handle_reloading_originals() - - -def handle_reloading_originals(): - """Handles the file operations of opening, saving, and deleting files.""" - new_file_path, original_filename = get_exported_file_path() - if new_file_path: - bpy.ops.wm.open_mainfile(filepath=new_file_path) - print("Loaded original file copy.") - if original_filename and os.path.exists(original_filename): - try: - os.remove(original_filename) - print(f"Deleted original file at: {original_filename}") - bpy.ops.wm.save_as_mainfile(filepath=original_filename) - print(f"Saved the file to the original filename: {original_filename}") - except OSError as e: - print(f"Error deleting {original_filename}: {e.strerror}") - - if new_file_path and os.path.exists(new_file_path) and new_file_path != original_filename: - try: - os.remove(new_file_path) - print(f"Deleted temporary file at: {new_file_path}") - except OSError as e: - print(f"Error deleting {new_file_path}: {e.strerror}") - else: - print("Failed to retrieve new file path.") - + def assets_done(): if state.proc_build == None: return From 1c42c1920388d69f21b7399f2a03e7bca89c43db Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sun, 4 Feb 2024 16:50:37 +0100 Subject: [PATCH 152/175] Bump version --- blender/arm/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index d246d408f1..d5086f2ddf 100644 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -23,7 +23,7 @@ arm.enable_reload(__name__) # Armory version -arm_version = '2024.1' +arm_version = '2024.2' arm_commit = '$Id$' def get_project_html5_copy(self): From 6a504524cfa715282f86832d3210f712cd730880 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Mon, 12 Feb 2024 12:05:45 -0500 Subject: [PATCH 153/175] Added an array variable --- blender/arm/exporter.py | 22 ++++- blender/arm/props_properties.py | 150 ++++++++++++++++++++++++++++++-- 2 files changed, 162 insertions(+), 10 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index bd73697535..c81df446ff 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -784,11 +784,31 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N if len(bobject.arm_propertylist) > 0: out_object['properties'] = [] for proplist_item in bobject.arm_propertylist: + # Check if the property is a collection (array type). + if proplist_item.type_prop == 'array': + # Convert the collection to a list. + collection_value = getattr(proplist_item, 'array_prop') + value = [] + for item in collection_value: + if item.string_prop != "": + value.append(item.string_prop) + elif item.integer_prop != 0: + value.append(item.integer_prop) + elif item.float_prop != 0.0: + value.append(item.float_prop) + elif item.boolean_prop is not False: + value.append(item.boolean_prop) + else: + # Handle other types of properties. + value = getattr(proplist_item, proplist_item.type_prop + '_prop') + out_property = { 'name': proplist_item.name_prop, - 'value': getattr(proplist_item, proplist_item.type_prop + '_prop')} + 'value': value + } out_object['properties'].append(out_property) + # Export the object reference and material references objref = bobject.data if objref is not None: diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py index b7fb24ed20..fe5ae894a9 100644 --- a/blender/arm/props_properties.py +++ b/blender/arm/props_properties.py @@ -2,12 +2,31 @@ from bpy.types import Menu, Panel, UIList from bpy.props import * + +class ArmArrayItem(bpy.types.PropertyGroup): + # Name property for each array item + name_prop: StringProperty(name="Name", default="Item") + index_prop = bpy.props.IntProperty( + name="Index", + description="Index of the item", + default=0, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + # Properties for each type + string_prop: StringProperty(name="String", default="") + integer_prop: IntProperty(name="Integer", default=0) + float_prop: FloatProperty(name="Float", default=0.0) + boolean_prop: BoolProperty(name="Boolean", default=False) + + class ArmPropertyListItem(bpy.types.PropertyGroup): type_prop: EnumProperty( items = [('string', 'String', 'String'), ('integer', 'Integer', 'Integer'), ('float', 'Float', 'Float'), ('boolean', 'Boolean', 'Boolean'), + ('array', 'Array', 'Array'), ], name = "Type") name_prop: StringProperty(name="Name", description="A name for this item", default="my_prop") @@ -15,6 +34,89 @@ class ArmPropertyListItem(bpy.types.PropertyGroup): integer_prop: IntProperty(name="Integer", description="A name for this item", default=0) float_prop: FloatProperty(name="Float", description="A name for this item", default=0.0) boolean_prop: BoolProperty(name="Boolean", description="A name for this item", default=False) + array_prop: CollectionProperty(type=ArmArrayItem) + new_array_item_type: EnumProperty( + items = [ + ('string', 'String', 'String'), + ('integer', 'Integer', 'Integer'), + ('float', 'Float', 'Float'), + ('boolean', 'Boolean', 'Boolean'), + # Add more types here as needed + ], + name = "New Item Type", + default = 'string' + ) + +class ARM_UL_ArrayItemList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # Use the item's index within the array as its uneditable name + array_type = data.new_array_item_type + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + # Display the index of the item as its name in a non-editable label + layout.label(text=str(index)) + + # Display the appropriate property based on the array type + if array_type == 'string': + layout.prop(item, "string_prop", text="") + elif array_type == 'integer': + layout.prop(item, "integer_prop", text="") + elif array_type == 'float': + layout.prop(item, "float_prop", text="") + elif array_type == 'boolean': + layout.prop(item, "boolean_prop", text="") + # Add other types as needed + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon="OBJECT_DATAMODE") + + +class ArmArrayAddItem(bpy.types.Operator): + bl_idname = "arm_array.add_item" + bl_label = "Add Array Item" + + def execute(self, context): + obj = bpy.context.object + if obj.arm_propertylist and obj.arm_propertylist_index < len(obj.arm_propertylist): + selected_item = obj.arm_propertylist[obj.arm_propertylist_index] + + # Create a new item in the array + new_item = selected_item.array_prop.add() + + # Set the type of the new item based on the selected type + new_item_type = selected_item.new_array_item_type + if new_item_type == 'string': + new_item.string_prop = "" # Default value for string + elif new_item_type == 'integer': + new_item.integer_prop = 0 # Default value for integer + elif new_item_type == 'float': + new_item.float_prop = 0.0 # Default value for float + elif new_item_type == 'boolean': + new_item.boolean_prop = False # Default value for boolean + + # Set the index of the new item + new_item_index = len(selected_item.array_prop) - 1 + new_item.index_prop = new_item_index + + # Update the array index + selected_item.array_index = new_item_index + + return {'FINISHED'} + +# Operator to remove an item from the array +class ArmArrayRemoveItem(bpy.types.Operator): + bl_idname = "arm_array.remove_item" + bl_label = "Remove Array Item" + + def execute(self, context): + obj = bpy.context.object + if obj.arm_propertylist and obj.arm_propertylist_index < len(obj.arm_propertylist): + selected_item = obj.arm_propertylist[obj.arm_propertylist_index] + if selected_item.array_prop: + selected_item.array_prop.remove(selected_item.array_index) + if selected_item.array_index > 0: + selected_item.array_index -= 1 + return {'FINISHED'} class ARM_UL_PropertyList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -37,6 +139,8 @@ class ArmPropertyListNewItem(bpy.types.Operator): ('integer', 'Integer', 'Integer'), ('float', 'Float', 'Float'), ('boolean', 'Boolean', 'Boolean'), + ('array', 'Array', 'Array'), + ], name = "Type") @@ -126,28 +230,54 @@ def execute(self, context): def draw_properties(layout, obj): layout.label(text="Properties") - rows = 2 - if len(obj.arm_traitlist) > 1: - rows = 4 + + # Draw the ARM_UL_PropertyList + rows = 4 if len(obj.arm_propertylist) > 1 else 2 row = layout.row() row.template_list("ARM_UL_PropertyList", "The_List", obj, "arm_propertylist", obj, "arm_propertylist_index", rows=rows) + + # Column for buttons next to ARM_UL_PropertyList col = row.column(align=True) - op = col.operator("arm_propertylist.new_item", icon='ADD', text="") - op = col.operator("arm_propertylist.delete_item", icon='REMOVE', text="") + col.operator("arm_propertylist.new_item", icon='ADD', text="") + col.operator("arm_propertylist.delete_item", icon='REMOVE', text="") if len(obj.arm_propertylist) > 1: col.separator() - op = col.operator("arm_propertylist.move_item", icon='TRIA_UP', text="") - op.direction = 'UP' - op = col.operator("arm_propertylist.move_item", icon='TRIA_DOWN', text="") - op.direction = 'DOWN' + col.operator("arm_propertylist.move_item", icon='TRIA_UP', text="").direction = 'UP' + col.operator("arm_propertylist.move_item", icon='TRIA_DOWN', text="").direction = 'DOWN' + + # Draw UI List for array items if the selected property is an array + if obj.arm_propertylist and obj.arm_propertylist_index < len(obj.arm_propertylist): + selected_item = obj.arm_propertylist[obj.arm_propertylist_index] + if selected_item.type_prop == 'array': + layout.label(text="Array Items") + + # Dropdown to select the type of new array items + layout.prop(selected_item, "new_array_item_type", text="New Item Type") + + # Template list for array items + row = layout.row() + row.template_list("ARM_UL_ArrayItemList", "", selected_item, "array_prop", selected_item, "array_index", rows=rows) + + # Column for buttons next to the array items list + col = row.column(align=True) + col.operator("arm_array.add_item", icon='ADD', text="") + col.operator("arm_array.remove_item", icon='REMOVE', text="") + + + + __REG_CLASSES = ( + ArmArrayItem, ArmPropertyListItem, ARM_UL_PropertyList, + ARM_UL_ArrayItemList, ArmPropertyListNewItem, ArmPropertyListDeleteItem, ArmPropertyListMoveItem, + ArmArrayAddItem, + ArmArrayRemoveItem, ) __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) @@ -156,3 +286,5 @@ def register(): __reg_classes() bpy.types.Object.arm_propertylist = CollectionProperty(type=ArmPropertyListItem) bpy.types.Object.arm_propertylist_index = IntProperty(name="Index for arm_propertylist", default=0) + # New property for tracking the active index in array items + bpy.types.PropertyGroup.array_index = IntProperty(name="Array Index", default=0) From 0e47339f8c9d88c7fb7b7ebc872f03ad3cc90a43 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Tue, 13 Feb 2024 05:32:27 -0500 Subject: [PATCH 154/175] Update exporter.py --- blender/arm/exporter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index c81df446ff..e7a067d832 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -786,17 +786,17 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N for proplist_item in bobject.arm_propertylist: # Check if the property is a collection (array type). if proplist_item.type_prop == 'array': - # Convert the collection to a list. + # Convert the collection to a list. collection_value = getattr(proplist_item, 'array_prop') value = [] for item in collection_value: - if item.string_prop != "": + if hasattr(item, 'string_prop'): value.append(item.string_prop) - elif item.integer_prop != 0: + elif hasattr(item, 'integer_prop'): value.append(item.integer_prop) - elif item.float_prop != 0.0: + elif hasattr(item, 'float_prop'): value.append(item.float_prop) - elif item.boolean_prop is not False: + elif hasattr(item, 'boolean_prop'): value.append(item.boolean_prop) else: # Handle other types of properties. From 63ad5b52b18f601c3a2b11f77945873d18074ea4 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Tue, 13 Feb 2024 05:50:40 -0500 Subject: [PATCH 155/175] Update exporter.py --- blender/arm/exporter.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index e7a067d832..2f9428d3c6 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -787,17 +787,10 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N # Check if the property is a collection (array type). if proplist_item.type_prop == 'array': # Convert the collection to a list. + array_type = proplist_item.new_array_item_type collection_value = getattr(proplist_item, 'array_prop') - value = [] - for item in collection_value: - if hasattr(item, 'string_prop'): - value.append(item.string_prop) - elif hasattr(item, 'integer_prop'): - value.append(item.integer_prop) - elif hasattr(item, 'float_prop'): - value.append(item.float_prop) - elif hasattr(item, 'boolean_prop'): - value.append(item.boolean_prop) + property_name = array_type + '_prop' + value = [str(getattr(item, property_name)) for item in collection_value] else: # Handle other types of properties. value = getattr(proplist_item, proplist_item.type_prop + '_prop') From 31ff6ba40048f67ad4d53b3f7e4fc35a24e52e30 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Tue, 13 Feb 2024 07:43:53 -0500 Subject: [PATCH 156/175] removed "new" from array property --- blender/arm/exporter.py | 2 +- blender/arm/props_properties.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 2f9428d3c6..bf5822e6a3 100644 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -787,7 +787,7 @@ def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> N # Check if the property is a collection (array type). if proplist_item.type_prop == 'array': # Convert the collection to a list. - array_type = proplist_item.new_array_item_type + array_type = proplist_item.array_item_type collection_value = getattr(proplist_item, 'array_prop') property_name = array_type + '_prop' value = [str(getattr(item, property_name)) for item in collection_value] diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py index fe5ae894a9..489ca890b4 100644 --- a/blender/arm/props_properties.py +++ b/blender/arm/props_properties.py @@ -26,7 +26,7 @@ class ArmPropertyListItem(bpy.types.PropertyGroup): ('integer', 'Integer', 'Integer'), ('float', 'Float', 'Float'), ('boolean', 'Boolean', 'Boolean'), - ('array', 'Array', 'Array'), + ('array', 'Array', 'Array'), ], name = "Type") name_prop: StringProperty(name="Name", description="A name for this item", default="my_prop") @@ -35,7 +35,7 @@ class ArmPropertyListItem(bpy.types.PropertyGroup): float_prop: FloatProperty(name="Float", description="A name for this item", default=0.0) boolean_prop: BoolProperty(name="Boolean", description="A name for this item", default=False) array_prop: CollectionProperty(type=ArmArrayItem) - new_array_item_type: EnumProperty( + array_item_type: EnumProperty( items = [ ('string', 'String', 'String'), ('integer', 'Integer', 'Integer'), @@ -50,7 +50,7 @@ class ArmPropertyListItem(bpy.types.PropertyGroup): class ARM_UL_ArrayItemList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): # Use the item's index within the array as its uneditable name - array_type = data.new_array_item_type + array_type = data.array_item_type if self.layout_type in {'DEFAULT', 'COMPACT'}: # Display the index of the item as its name in a non-editable label @@ -84,7 +84,7 @@ def execute(self, context): new_item = selected_item.array_prop.add() # Set the type of the new item based on the selected type - new_item_type = selected_item.new_array_item_type + new_item_type = selected_item.array_item_type if new_item_type == 'string': new_item.string_prop = "" # Default value for string elif new_item_type == 'integer': @@ -252,7 +252,7 @@ def draw_properties(layout, obj): layout.label(text="Array Items") # Dropdown to select the type of new array items - layout.prop(selected_item, "new_array_item_type", text="New Item Type") + layout.prop(selected_item, "array_item_type", text="New Item Type") # Template list for array items row = layout.row() From 6b13832c766494244bdfadc0af5e3e4b5231a461 Mon Sep 17 00:00:00 2001 From: Jordan Elevons Date: Tue, 13 Feb 2024 10:28:44 -0500 Subject: [PATCH 157/175] Update props_properties.py --- blender/arm/props_properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props_properties.py b/blender/arm/props_properties.py index 489ca890b4..935c946941 100644 --- a/blender/arm/props_properties.py +++ b/blender/arm/props_properties.py @@ -252,7 +252,7 @@ def draw_properties(layout, obj): layout.label(text="Array Items") # Dropdown to select the type of new array items - layout.prop(selected_item, "array_item_type", text="New Item Type") + layout.prop(selected_item, "array_item_type", text="Array Type") # Template list for array items row = layout.row() From 98e8caf14ef6768b9ae646859b92bf94ba423926 Mon Sep 17 00:00:00 2001 From: e2002e Date: Wed, 14 Feb 2024 17:29:40 +0100 Subject: [PATCH 158/175] fix mat4 being included as vec4 --- blender/arm/material/make_cluster.py | 3 +-- blender/arm/material/shader.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/blender/arm/material/make_cluster.py b/blender/arm/material/make_cluster.py index 8b3e98defe..14320ae8b3 100644 --- a/blender/arm/material/make_cluster.py +++ b/blender/arm/material/make_cluster.py @@ -57,8 +57,7 @@ def write(vert: shader.Shader, frag: shader.Shader): frag.add_uniform('sampler2DShadow shadowMapAtlas', top=True) else: frag.add_uniform('sampler2DShadow shadowMapSpot[4]', included=True) - # FIXME: type is actually mat4, but otherwise it will not be set as floats when writing the shaders' json files - frag.add_uniform('vec4 LWVPSpotArray[maxLightsCluster]', link='_biasLightWorldViewProjectionMatrixSpotArray', included=True) + frag.add_uniform('mat4 LWVPSpotArray[maxLightsCluster]', link='_biasLightWorldViewProjectionMatrixSpotArray', included=True) frag.write('for (int i = 0; i < min(numLights, maxLightsCluster); i++) {') frag.write('int li = int(texelFetch(clustersData, ivec2(clusterI, i + 1), 0).r * 255);') diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index bf6c7b8cfa..2bcca6a874 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -284,6 +284,9 @@ def add_uniform(self, s, link=None, included=False, top=False, elif ar[0] == 'vec4' and '[' in ar[1]: ar[0] = 'floats' ar[1] = ar[1].split('[', 1)[0] + elif ar[0] == 'mat4' and '[' in ar[1]: + ar[0] = 'floats' + ar[1] = ar[1].split('[', 1)[0] 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: From 95de89e79ee57323eb6b7f1c44483bab01acded6 Mon Sep 17 00:00:00 2001 From: e2002e Date: Wed, 14 Feb 2024 18:51:38 +0100 Subject: [PATCH 159/175] fix forward clusters spotDir & spotBlend --- Shaders/std/light.glsl | 2 +- blender/arm/material/make_cluster.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Shaders/std/light.glsl b/Shaders/std/light.glsl index 6e32b202ac..20a4a7a6d3 100644 --- a/Shaders/std/light.glsl +++ b/Shaders/std/light.glsl @@ -53,7 +53,7 @@ uniform sampler2DShadow shadowMapAtlasSpot; #endif #else - uniform sampler2DShadow shadowMapSpot[maxLightsCluster]; + uniform sampler2DShadow shadowMapSpot[4]; #endif uniform mat4 LWVPSpotArray[maxLightsCluster]; #endif diff --git a/blender/arm/material/make_cluster.py b/blender/arm/material/make_cluster.py index 14320ae8b3..553e06fd5b 100644 --- a/blender/arm/material/make_cluster.py +++ b/blender/arm/material/make_cluster.py @@ -78,8 +78,8 @@ def write(vert: shader.Shader, frag: shader.Shader): if '_Spot' in wrd.world_defs: frag.write('\t, lightsArray[li * 3 + 2].y != 0.0') frag.write('\t, lightsArray[li * 3 + 2].y') # spot size (cutoff) - frag.write('\t, lightsArraySpot[li].w') # spot blend (exponent) - frag.write('\t, lightsArraySpot[li].xyz') # spotDir + frag.write('\t, lightsArraySpot[li * 2].w') # spot blend (exponent) + frag.write('\t, lightsArraySpot[li * 2].xyz') # spotDir frag.write('\t, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w)') # scale frag.write('\t, lightsArraySpot[li * 2 + 1].xyz') # right if '_VoxelShadow' in wrd.world_defs and '_VoxelAOvar' in wrd.world_defs: From 8a39eb351cd4eefb9ad7f547f3ee6a3d681107f5 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 25 Feb 2024 00:50:45 +0100 Subject: [PATCH 160/175] re-implement navmesh preview --- blender/arm/props_traits.py | 104 ++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 3ecaa002ec..799ad89398 100644 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -414,33 +414,91 @@ def execute(self, context): if not arm.utils.check_sdkpath(self): return {"CANCELLED"} + print("Started visualization generation") + + # Append objects to be included in NavMesh + export_objects = [] + # Append Object with trait + export_objects.append(obj) + # Get NavMesh trait + for trait in obj.arm_traitlist: + if trait.arm_traitpropslist and trait.class_name_prop == 'NavMesh': + # Check if child objects should be included in NavMesh + prop = trait.arm_traitpropslist['combineImmidiateChildren'] + if(prop.get_value()): + # If yes, check if child is a mesh + for child_obj in obj.children: + if obj.type == 'MESH': + # Append child + export_objects.append(child_obj) + + # get dependency graph depsgraph = bpy.context.evaluated_depsgraph_get() - armature = obj.find_armature() - apply_modifiers = not armature - obj_eval = obj.evaluated_get(depsgraph) if apply_modifiers else obj - export_mesh = obj_eval.to_mesh() - # TODO: build tilecache here - print("Started visualization generation") - # For visualization + # Get build directory nav_full_path = arm.utils.get_fp_build() + '/compiled/Assets/navigation' if not os.path.exists(nav_full_path): os.makedirs(nav_full_path) - nav_mesh_name = 'nav_' + obj_eval.data.name + # Get export OBJ name and path + nav_mesh_name = 'nav_' + obj.data.name mesh_path = nav_full_path + '/' + nav_mesh_name + '.obj' + # Max index of objects (vertices) traversed + max_overall_index = 0 + # Open to OBJ file with open(mesh_path, 'w') as f: - for v in export_mesh.vertices: - f.write("v %.4f " % (v.co[0] * obj_eval.scale.x)) - f.write("%.4f " % (v.co[2] * obj_eval.scale.z)) - f.write("%.4f\n" % (v.co[1] * obj_eval.scale.y)) # Flipped - for p in export_mesh.polygons: - f.write("f") - for i in reversed(p.vertices): # Flipped normals - f.write(" %d" % (i + 1)) - f.write("\n") - + for export_obj in export_objects: + # If armature, apply armature modifier + armature = export_obj.find_armature() + apply_modifiers = not armature + obj_eval = export_obj.evaluated_get(depsgraph) if apply_modifiers else export_obj + + # Get mesh data + export_mesh = obj_eval.to_mesh() + + # Get world transform + world_matrix = obj_eval.matrix_world + + # Iterate over the triangles and get vertices and indices + triangles = export_mesh.loop_triangles + traversed_indices = [] + + # For each triangle in the object + for triangle in triangles: + # For each index in triangle + for loop_index in triangle.loops: + # Get vertex index + vertex_index = export_mesh.loops[loop_index].vertex_index + # Skip if vertex already appended + if (vertex_index not in traversed_indices): + # If not, append vertex + traversed_indices.append(vertex_index) + vertex = export_mesh.vertices[vertex_index].co + # Apply world transform + tv = world_matrix @ vertex + # Write to OBJ + f.write("v %.4f " % (tv[0])) + f.write("%.4f " % (tv[2])) + f.write("%.4f\n" % (tv[1])) # Flipped + + # Max index of this object + max_index = 0 + # For each triangle in the object + for triangle in triangles: + # Write index to OBJ + f.write("f") + for loop_index in triangle.loops: + # index of this object should be > index of previous objects + curr_index = max_overall_index + loop_index + 1 + f.write(" %d" % (curr_index)) + if(curr_index > max_overall_index): + max_index = curr_index + f.write("\n") + # Store max overall index + max_overall_index = max_index + + # Get buildnavjs buildnavjs_path = arm.utils.get_sdk_path() + '/lib/haxerecast/buildnavjs' # append config values @@ -461,9 +519,10 @@ def execute(self, context): navmesh = bpy.ops.import_scene.obj(filepath=mesh_path) navmesh = bpy.context.selected_objects[0] + # NavMesh preview settings, cleanup navmesh.name = nav_mesh_name navmesh.rotation_euler = (0, 0, 0) - navmesh.location = (obj.location.x, obj.location.y, obj.location.z) + navmesh.location = (0, 0, 0) navmesh.arm_export = False bpy.context.view_layer.objects.active = navmesh @@ -886,8 +945,11 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b # Bundled scripts else: - row.enabled = item.class_name_prop != '' - row.operator("arm.edit_bundled_script", icon_value=ICON_HAXE).is_object = is_object + if item.class_name_prop == 'NavMesh': + row.operator("arm.generate_navmesh", icon="UV_VERTEXSEL") + else: + row.enabled = item.class_name_prop != '' + row.operator("arm.edit_bundled_script", icon_value=ICON_HAXE).is_object = is_object refresh_op = "arm.refresh_object_scripts" if is_object else "arm.refresh_scripts" row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") From 9d3641730a5fd5650b3702b864d4bfd166b64629 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 25 Feb 2024 00:51:31 +0100 Subject: [PATCH 161/175] Fix navmesh child object scaling and position --- Sources/armory/trait/NavMesh.hx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/armory/trait/NavMesh.hx b/Sources/armory/trait/NavMesh.hx index 9d135140b1..4239ce95d0 100644 --- a/Sources/armory/trait/NavMesh.hx +++ b/Sources/armory/trait/NavMesh.hx @@ -429,21 +429,22 @@ class NavMesh extends Trait { vertexIndexMap = generateVertexIndexMap(vecind, vertexMapArray); // Parented object - clear parent location + /* if (object.parent != null && object.parent.name != "") { object.transform.loc.x += object.parent.transform.worldx(); object.transform.loc.y += object.parent.transform.worldy(); object.transform.loc.z += object.parent.transform.worldz(); object.transform.localOnly = true; object.transform.buildMatrix(); - } + }*/ var positions = fromI16(geom.positions.values, mo.data.scalePos); for (i in 0...Std.int(positions.length / 3)) { v.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); v.applyQuat(object.transform.rot); - v.x *= object.transform.scale.x; - v.y *= object.transform.scale.y; - v.z *= object.transform.scale.z; + v.x *= object.transform.scale.x * object.parent.transform.scale.x; + v.y *= object.transform.scale.y * object.parent.transform.scale.y; + v.z *= object.transform.scale.z * object.parent.transform.scale.z; v.addf(object.transform.worldx(), object.transform.worldy(), object.transform.worldz()); positions[i * 3 ] = v.x; positions[i * 3 + 1] = v.y; From ab0aab81f12483f1010fb7b7708b16f961b9dea9 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 2 Mar 2024 12:23:12 +0100 Subject: [PATCH 162/175] fix node indces --- Sources/armory/logicnode/NetworkHttpRequestNode.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx index 9ebf7a7df6..47837ace4f 100644 --- a/Sources/armory/logicnode/NetworkHttpRequestNode.hx +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -63,13 +63,13 @@ class NetworkHttpRequestNode extends LogicNode { try { if(property0 == "post") { - var bytes = inputs[2].get(); + var bytes = inputs[3].get(); if(bytes == true){ - var data:haxe.io.Bytes = inputs[3].get(); + var data:haxe.io.Bytes = inputs[2].get(); request.setPostBytes(data); request.request(true); }else{ - var data:Dynamic = inputs[3].get(); + var data:Dynamic = inputs[2].get(); request.setPostData(data.toString()); request.request(true); } From db7fb0c6d5859ab597dd433375546bf52c6f6de2 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 3 Mar 2024 13:59:51 +0100 Subject: [PATCH 163/175] New select output node --- Sources/armory/logicnode/SelectOutputNode.hx | 20 ++++++++++ .../arm/logicnode/logic/LN_select_output.py | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 Sources/armory/logicnode/SelectOutputNode.hx create mode 100644 blender/arm/logicnode/logic/LN_select_output.py diff --git a/Sources/armory/logicnode/SelectOutputNode.hx b/Sources/armory/logicnode/SelectOutputNode.hx new file mode 100644 index 0000000000..a9c8dcc925 --- /dev/null +++ b/Sources/armory/logicnode/SelectOutputNode.hx @@ -0,0 +1,20 @@ +package armory.logicnode; + +class SelectOutputNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + //Get index to run + var outIndex: Int = inputs[1].get(); + // Check if output index found + if(outIndex > (outputs.length - 2) || outIndex < 0) + { + runOutput(0); + return; + } + runOutput(outIndex + 1); + } +} diff --git a/blender/arm/logicnode/logic/LN_select_output.py b/blender/arm/logicnode/logic/LN_select_output.py new file mode 100644 index 0000000000..adcbc8c791 --- /dev/null +++ b/blender/arm/logicnode/logic/LN_select_output.py @@ -0,0 +1,40 @@ +from arm.logicnode.arm_nodes import * + +class SelectOutputNode(ArmLogicTreeNode): + """Selects one of multiple outputs depending on the index + + @input In: Action input. + + @input Index: Output index to run. + + @output Default: Run if output index not present. + """ + + bl_idname = 'LNSelectOutputNode' + bl_label = 'Select output' + arm_version = 1 + min_outputs = 2 + + def __init__(self): + super(SelectOutputNode, self).__init__() + array_nodes[self.get_id_str()] = self + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmIntSocket', 'Index') + + self.add_output('ArmNodeSocketAction', 'Default') + self.add_output('ArmNodeSocketAction', 'Index 0') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('arm.node_add_output', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.socket_type = 'ArmNodeSocketAction' + op.name_format = 'Index {0}' + op.index_name_offset = -1 + column = row.column(align=True) + op = column.operator('arm.node_remove_output', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + if len(self.outputs) == self.min_outputs: + column.enabled = False From 7da68371e06c3c7e76b73424c9361114980d9fb3 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 3 Mar 2024 14:00:11 +0100 Subject: [PATCH 164/175] document set map value node --- blender/arm/logicnode/map/LN_set_map_value.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/blender/arm/logicnode/map/LN_set_map_value.py b/blender/arm/logicnode/map/LN_set_map_value.py index 081f1c5318..8bbd4dc1e8 100644 --- a/blender/arm/logicnode/map/LN_set_map_value.py +++ b/blender/arm/logicnode/map/LN_set_map_value.py @@ -2,7 +2,17 @@ class SetMapValueNode(ArmLogicTreeNode): - """Set Map Value""" + """Set Map Value + + @input In: Set the map. + + @input Map: Map to set values. + + @input Key: Key to be set. + + @input Value: Value for the key. + """ + bl_idname = 'LNSetMapValueNode' bl_label = 'Set Map Value' arm_version = 1 From 64e85049338a2bf7cb6d9ffabcc4a3e3c153511d Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 3 Mar 2024 14:00:39 +0100 Subject: [PATCH 165/175] New set Map From Array Node --- .../armory/logicnode/SetMapFromArrayNode.hx | 25 ++++++++++++++++++ .../logicnode/map/LN_set_map_from_array.py | 26 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 Sources/armory/logicnode/SetMapFromArrayNode.hx create mode 100644 blender/arm/logicnode/map/LN_set_map_from_array.py diff --git a/Sources/armory/logicnode/SetMapFromArrayNode.hx b/Sources/armory/logicnode/SetMapFromArrayNode.hx new file mode 100644 index 0000000000..a54e91d1ee --- /dev/null +++ b/Sources/armory/logicnode/SetMapFromArrayNode.hx @@ -0,0 +1,25 @@ +package armory.logicnode; + + +class SetMapFromArrayNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + var map: Map = inputs[1].get(); + if (map == null) return; + + var keys: Array = inputs[2].get(); + var values: Array = inputs[3].get(); + + assert(Error, keys.length == values.length, "Number of keys and values should be equal"); + + for(i in 0...keys.length) { + map[keys[i]] = values[i]; + } + runOutput(0); + } + +} diff --git a/blender/arm/logicnode/map/LN_set_map_from_array.py b/blender/arm/logicnode/map/LN_set_map_from_array.py new file mode 100644 index 0000000000..0bac0e888e --- /dev/null +++ b/blender/arm/logicnode/map/LN_set_map_from_array.py @@ -0,0 +1,26 @@ +from arm.logicnode.arm_nodes import * + + +class SetMapFromArrayNode(ArmLogicTreeNode): + """Set Map From Arrays + + @input In: Set the map. + + @input Map: Map to set values. + + @input Key: Array of keys to be set. + + @input Value: Array of corresponding values for the keys. + """ + + bl_idname = 'LNSetMapFromArrayNode' + bl_label = 'Set Map From Array' + arm_version = 1 + + def init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_input('ArmDynamicSocket', 'Map') + self.add_input('ArmNodeSocketArray', 'Keys') + self.add_input('ArmNodeSocketArray', 'Values') + + self.add_output('ArmNodeSocketAction', 'Out') From 6d9957f6a0a3304ad88ef400b2ca5aacdf4b00c2 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sun, 3 Mar 2024 14:01:17 +0100 Subject: [PATCH 166/175] New String Map Node --- Sources/armory/logicnode/StringMapNode.hx | 24 ++++++++++ blender/arm/logicnode/map/LN_string_map.py | 55 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 Sources/armory/logicnode/StringMapNode.hx create mode 100644 blender/arm/logicnode/map/LN_string_map.py diff --git a/Sources/armory/logicnode/StringMapNode.hx b/Sources/armory/logicnode/StringMapNode.hx new file mode 100644 index 0000000000..433bfb3d78 --- /dev/null +++ b/Sources/armory/logicnode/StringMapNode.hx @@ -0,0 +1,24 @@ +package armory.logicnode; + + +class StringMapNode extends LogicNode { + + public var property0: Int; + public var map: Map = []; + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from: Int) { + map.clear(); + for(i in 0...property0) { + map.set(inputs[i * 2 + 1].get(), inputs[i * 2 + 2].get()); + } + runOutput(0); + } + + override function get(from: Int):Dynamic { + return map; + } + +} diff --git a/blender/arm/logicnode/map/LN_string_map.py b/blender/arm/logicnode/map/LN_string_map.py new file mode 100644 index 0000000000..1175f5de83 --- /dev/null +++ b/blender/arm/logicnode/map/LN_string_map.py @@ -0,0 +1,55 @@ +from arm.logicnode.arm_nodes import * + +class StringMapNode(ArmLogicTreeNode): + """Create String Map + + @input In: Create a map using given keys and values. + + @input Key: Key. + + @input Value: Value. + + @output Out: Run after map is created. + + @output Map: The created map. + """ + + bl_idname = 'LNStringMapNode' + bl_label = 'String Map' + arm_version = 1 + + min_inputs = 1 + property0: HaxeIntProperty('property0', name='Number of keys', default=0) + + def __init__(self): + super(StringMapNode, self).__init__() + self.register_id() + + def arm_init(self, context): + self.add_input('ArmNodeSocketAction', 'In') + self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmDynamicSocket', 'Map') + + def add_sockets(self): + self.add_input('ArmStringSocket', f'Key [{self.property0}]') + self.add_input('ArmStringSocket', f'Value [{self.property0}]') + self.property0 += 1 + + def remove_sockets(self): + if self.property0 > 0: + self.inputs.remove(self.inputs.values()[-1]) + self.inputs.remove(self.inputs.values()[-1]) + self.property0 -= 1 + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('arm.node_call_func', text='New', icon='PLUS', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'add_sockets' + column = row.column(align=True) + op = column.operator('arm.node_call_func', text='', icon='X', emboss=True) + op.node_index = self.get_id_str() + op.callback_name = 'remove_sockets' + if len(self.inputs) == self.min_inputs: + column.enabled = False From 6eaf09180983a5f93677d1b515d7c71a10b8a721 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Thu, 7 Mar 2024 21:31:07 +0100 Subject: [PATCH 167/175] fix documentation --- blender/arm/logicnode/logic/LN_select_output.py | 2 +- blender/arm/logicnode/map/LN_set_map_from_array.py | 2 +- blender/arm/logicnode/map/LN_set_map_value.py | 2 +- blender/arm/logicnode/map/LN_string_map.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blender/arm/logicnode/logic/LN_select_output.py b/blender/arm/logicnode/logic/LN_select_output.py index adcbc8c791..9d05f4a9bc 100644 --- a/blender/arm/logicnode/logic/LN_select_output.py +++ b/blender/arm/logicnode/logic/LN_select_output.py @@ -1,7 +1,7 @@ from arm.logicnode.arm_nodes import * class SelectOutputNode(ArmLogicTreeNode): - """Selects one of multiple outputs depending on the index + """Selects one of multiple outputs depending on the index. @input In: Action input. diff --git a/blender/arm/logicnode/map/LN_set_map_from_array.py b/blender/arm/logicnode/map/LN_set_map_from_array.py index 0bac0e888e..af11aaf2b3 100644 --- a/blender/arm/logicnode/map/LN_set_map_from_array.py +++ b/blender/arm/logicnode/map/LN_set_map_from_array.py @@ -2,7 +2,7 @@ class SetMapFromArrayNode(ArmLogicTreeNode): - """Set Map From Arrays + """Set Map From Arrays. @input In: Set the map. diff --git a/blender/arm/logicnode/map/LN_set_map_value.py b/blender/arm/logicnode/map/LN_set_map_value.py index 8bbd4dc1e8..45e14bae80 100644 --- a/blender/arm/logicnode/map/LN_set_map_value.py +++ b/blender/arm/logicnode/map/LN_set_map_value.py @@ -2,7 +2,7 @@ class SetMapValueNode(ArmLogicTreeNode): - """Set Map Value + """Set Map Value. @input In: Set the map. diff --git a/blender/arm/logicnode/map/LN_string_map.py b/blender/arm/logicnode/map/LN_string_map.py index 1175f5de83..57ce1db331 100644 --- a/blender/arm/logicnode/map/LN_string_map.py +++ b/blender/arm/logicnode/map/LN_string_map.py @@ -1,7 +1,7 @@ from arm.logicnode.arm_nodes import * class StringMapNode(ArmLogicTreeNode): - """Create String Map + """Create String Map. @input In: Create a map using given keys and values. From d0db8c22e4d6052e0d356cebd45c763ca9ffd8d6 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Thu, 7 Mar 2024 23:44:07 +0100 Subject: [PATCH 168/175] Implement JSON nodes --- Sources/armory/logicnode/JsonStringifyNode.hx | 14 ++++++++++++++ Sources/armory/logicnode/ParseJsonNode.hx | 13 +++++++++++++ blender/arm/logicnode/map/LN_json_parse.py | 18 ++++++++++++++++++ blender/arm/logicnode/map/LN_json_stringify.py | 18 ++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 Sources/armory/logicnode/JsonStringifyNode.hx create mode 100644 Sources/armory/logicnode/ParseJsonNode.hx create mode 100644 blender/arm/logicnode/map/LN_json_parse.py create mode 100644 blender/arm/logicnode/map/LN_json_stringify.py diff --git a/Sources/armory/logicnode/JsonStringifyNode.hx b/Sources/armory/logicnode/JsonStringifyNode.hx new file mode 100644 index 0000000000..cd54413558 --- /dev/null +++ b/Sources/armory/logicnode/JsonStringifyNode.hx @@ -0,0 +1,14 @@ +package armory.logicnode; +import haxe.Json; + +class JsonStringifyNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from: Int):Dynamic { + return Json.stringify(inputs[0].get()); + } + +} diff --git a/Sources/armory/logicnode/ParseJsonNode.hx b/Sources/armory/logicnode/ParseJsonNode.hx new file mode 100644 index 0000000000..01d30779c9 --- /dev/null +++ b/Sources/armory/logicnode/ParseJsonNode.hx @@ -0,0 +1,13 @@ +package armory.logicnode; +import haxe.Json; + +class ParseJsonNode extends LogicNode { + + public function new(tree:LogicTree) { + super(tree); + } + + override function get(from: Int):Dynamic { + return Json.parse(inputs[0].get()); + } +} diff --git a/blender/arm/logicnode/map/LN_json_parse.py b/blender/arm/logicnode/map/LN_json_parse.py new file mode 100644 index 0000000000..4bbd826d62 --- /dev/null +++ b/blender/arm/logicnode/map/LN_json_parse.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +class ParseJsonNode(ArmLogicTreeNode): + """Parse a JSON String to Haxe object. + + @input JSON: JSON string. + + @output Value: Parsed value. + """ + + bl_idname = 'LNParseJsonNode' + bl_label = 'Parse JSON' + arm_version = 1 + + def init(self, context): + self.add_input('ArmStringSocket', 'JSON') + self.add_output('ArmDynamicSocket', 'Value') diff --git a/blender/arm/logicnode/map/LN_json_stringify.py b/blender/arm/logicnode/map/LN_json_stringify.py new file mode 100644 index 0000000000..670964fe8c --- /dev/null +++ b/blender/arm/logicnode/map/LN_json_stringify.py @@ -0,0 +1,18 @@ +from arm.logicnode.arm_nodes import * + + +class JsonStringifyNode(ArmLogicTreeNode): + """Convert a Haxe object to JSON String. + + @input Value: Value to convert. + + @output String: JSON String. + """ + + bl_idname = 'LNJsonStringifyNode' + bl_label = 'JSON Stringify' + arm_version = 1 + + def init(self, context): + self.add_input('ArmDynamicSocket', 'Value') + self.add_output('ArmStringSocket', 'JSON') \ No newline at end of file From 4452eed80d22ce912e0d18c467451f02d9eb4bd7 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Sat, 2 Mar 2024 22:19:24 +0100 Subject: [PATCH 169/175] Refactor HTTP request node --- .../logicnode/NetworkHttpRequestNode.hx | 37 ++++++--- .../network/LN_network_http_request.py | 80 ++++++++++++++----- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx index 47837ace4f..131c81ee31 100644 --- a/Sources/armory/logicnode/NetworkHttpRequestNode.hx +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -4,8 +4,10 @@ import iron.object.Object; class NetworkHttpRequestNode extends LogicNode { public var property0: String; + public var callbackType: Int; public var statusInt: Int; public var response: Dynamic; + public var errorOut: String; public var headers: Map; public var parameters: Map; @@ -18,13 +20,10 @@ class NetworkHttpRequestNode extends LogicNode { var url = inputs[1].get(); if(url == null){return;} - if(property0 == "post") { - headers = inputs[4].get(); - parameters = inputs[5].get(); - }else{ - headers = inputs[2].get(); - parameters = inputs[3].get(); - } + + headers = inputs[2].get(); + parameters = inputs[3].get(); + var printErrors: Bool = inputs[4].get(); var request = new haxe.Http(url); #if js @@ -42,34 +41,41 @@ class NetworkHttpRequestNode extends LogicNode { } request.onStatus = function(status:Int) { + callbackType = 1; statusInt = status; runOutput(0); } request.onBytes = function(data:haxe.io.Bytes) { + callbackType = 2; response = data; runOutput(0); } request.onData = function(data:String) { + callbackType = 3; response = data; runOutput(0); } request.onError = function(error:String){ - trace ("Error: " + error ); + callbackType = 4; + errorOut = error; + if(printErrors) { + trace ("Error: " + error ); + } runOutput(0); } try { if(property0 == "post") { - var bytes = inputs[3].get(); + var bytes = inputs[6].get(); if(bytes == true){ - var data:haxe.io.Bytes = inputs[2].get(); + var data:haxe.io.Bytes = inputs[5].get(); request.setPostBytes(data); request.request(true); }else{ - var data:Dynamic = inputs[2].get(); + var data:Dynamic = inputs[5].get(); request.setPostData(data.toString()); request.request(true); } @@ -80,13 +86,18 @@ class NetworkHttpRequestNode extends LogicNode { trace("Could not complete request: " + e); } + callbackType = 0; + runOutput(0); + } override function get(from: Int): Dynamic { return switch (from) { - case 1: statusInt; - case 2: response; + case 1: callbackType; + case 2: statusInt; + case 3: response; + case 4: errorOut; default: throw "Unreachable"; } diff --git a/blender/arm/logicnode/network/LN_network_http_request.py b/blender/arm/logicnode/network/LN_network_http_request.py index 76130bb8b1..c2b16cf9b3 100644 --- a/blender/arm/logicnode/network/LN_network_http_request.py +++ b/blender/arm/logicnode/network/LN_network_http_request.py @@ -2,10 +2,47 @@ class NetworkHttpRequestNode(ArmLogicTreeNode): - """Network Http Request""" + """Network HTTP Request. + + @option Get/ Post: Use HTTP GET or POST methods. + + @input In: Action input. + + @input Url: Url as string. + + @input Headers: Headers as a Haxe map. + + @input Parameters: Parameters for the request as Haxe map. + + @seeNode Create Map + + @input Print Error: Print Error in console. + + @input Data: Data to send. Any type. + + @input Bytes: Is the data sent as bytes or as a string. + + @output Out: Multi-functional output. Type of output given by `Callback Type`. + + @output Callback Type: Type of output. + 0 = Node Executed + 1 = Status Callback + 2 = Bytes Data Response Callback + 3 = String Data Response Callback + 4 = Error String Callback + + @utput Status: Status value + + @utput Response: Response value + + @output Error: Error + """ + bl_idname = 'LNNetworkHttpRequestNode' bl_label = 'Http Request' - arm_version = 1 + arm_version = 2 + + default_inputs_count = 5 @staticmethod @@ -26,32 +63,19 @@ def set_enum(self, value): select_current = self.get_enum_id_value(self, 'property0', value) select_prev = self.property0 - if select_prev != select_current: - - for i in self.inputs: - self.inputs.remove(i) - for i in self.outputs: - self.outputs.remove(i) + if select_prev == select_current: + return if (self.get_count_in(select_current) == 0): - self.add_input('ArmNodeSocketAction', 'In') - self.add_input('ArmStringSocket', 'Url') - self.add_input('ArmDynamicSocket', 'Headers') - self.add_input('ArmDynamicSocket', 'Parameters') - self.add_output('ArmNodeSocketAction', 'Out') - self.add_output('ArmIntSocket', 'Status') - self.add_output('ArmDynamicSocket', 'Response') + idx = 0 + for inp in self.inputs: + if idx >= self.default_inputs_count: + self.inputs.remove(inp) + idx += 1 self['property0'] = value else: - self.add_input('ArmNodeSocketAction', 'In') - self.add_input('ArmStringSocket', 'Url') self.add_input('ArmDynamicSocket', 'Data') self.add_input('ArmBoolSocket', 'Bytes') - self.add_input('ArmDynamicSocket', 'Headers') - self.add_input('ArmDynamicSocket', 'Parameters') - self.add_output('ArmNodeSocketAction', 'Out') - self.add_output('ArmIntSocket', 'Status') - self.add_output('ArmDynamicSocket', 'Response') self['property0'] = value @@ -69,10 +93,22 @@ def arm_init(self, context): self.add_input('ArmStringSocket', 'Url') self.add_input('ArmDynamicSocket', 'Headers') self.add_input('ArmDynamicSocket', 'Parameters') + self.add_input('ArmBoolSocket', 'Print Error') self.add_output('ArmNodeSocketAction', 'Out') + self.add_output('ArmIntSocket', 'Callback Type') self.add_output('ArmIntSocket', 'Status') self.add_output('ArmDynamicSocket', 'Response') + self.add_output('ArmStringSocket', 'Error') def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + 'LNNetworkHttpRequestNode', self.arm_version, 'LNNetworkHttpRequestNode', 2, + in_socket_mapping = {0:0, 1:1}, out_socket_mapping={0:0} + ) \ No newline at end of file From 84cddeee093aae977df0cd85abdc27be7abb3b27 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Thu, 7 Mar 2024 23:48:26 +0100 Subject: [PATCH 170/175] use tabs for indentation --- .../logicnode/NetworkHttpRequestNode.hx | 182 +++++++++--------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/Sources/armory/logicnode/NetworkHttpRequestNode.hx b/Sources/armory/logicnode/NetworkHttpRequestNode.hx index 131c81ee31..c73c70d18f 100644 --- a/Sources/armory/logicnode/NetworkHttpRequestNode.hx +++ b/Sources/armory/logicnode/NetworkHttpRequestNode.hx @@ -3,103 +3,103 @@ import iron.object.Object; class NetworkHttpRequestNode extends LogicNode { - public var property0: String; - public var callbackType: Int; - public var statusInt: Int; - public var response: Dynamic; - public var errorOut: String; - public var headers: Map; - public var parameters: Map; + public var property0: String; + public var callbackType: Int; + public var statusInt: Int; + public var response: Dynamic; + public var errorOut: String; + public var headers: Map; + public var parameters: Map; - public function new(tree:LogicTree) { - super(tree); - } + public function new(tree:LogicTree) { + super(tree); + } - override function run(from:Int) { + override function run(from:Int) { - var url = inputs[1].get(); + var url = inputs[1].get(); - if(url == null){return;} + if(url == null){return;} - headers = inputs[2].get(); - parameters = inputs[3].get(); - var printErrors: Bool = inputs[4].get(); + headers = inputs[2].get(); + parameters = inputs[3].get(); + var printErrors: Bool = inputs[4].get(); - var request = new haxe.Http(url); + var request = new haxe.Http(url); #if js - request.async = true; + request.async = true; #end - if(headers != null){ - for (k in headers.keys()) { - request.addHeader( k, headers[k]); - } - } - if(parameters != null){ - for (k in parameters.keys()) { - request.addParameter( k, parameters[k]); - } - } - - request.onStatus = function(status:Int) { - callbackType = 1; - statusInt = status; - runOutput(0); - } - - request.onBytes = function(data:haxe.io.Bytes) { - callbackType = 2; - response = data; - runOutput(0); - } - - request.onData = function(data:String) { - callbackType = 3; - response = data; - runOutput(0); - } - - request.onError = function(error:String){ - callbackType = 4; - errorOut = error; - if(printErrors) { - trace ("Error: " + error ); - } - runOutput(0); - } - - try { - if(property0 == "post") { - var bytes = inputs[6].get(); - if(bytes == true){ - var data:haxe.io.Bytes = inputs[5].get(); - request.setPostBytes(data); - request.request(true); - }else{ - var data:Dynamic = inputs[5].get(); - request.setPostData(data.toString()); - request.request(true); - } - } else { - request.request(false); - } - } catch( e : Dynamic ) { - trace("Could not complete request: " + e); - } - - callbackType = 0; - runOutput(0); - - } - - override function get(from: Int): Dynamic { - - return switch (from) { - case 1: callbackType; - case 2: statusInt; - case 3: response; - case 4: errorOut; - default: throw "Unreachable"; - } - - } + if(headers != null){ + for (k in headers.keys()) { + request.addHeader( k, headers[k]); + } + } + if(parameters != null){ + for (k in parameters.keys()) { + request.addParameter( k, parameters[k]); + } + } + + request.onStatus = function(status:Int) { + callbackType = 1; + statusInt = status; + runOutput(0); + } + + request.onBytes = function(data:haxe.io.Bytes) { + callbackType = 2; + response = data; + runOutput(0); + } + + request.onData = function(data:String) { + callbackType = 3; + response = data; + runOutput(0); + } + + request.onError = function(error:String){ + callbackType = 4; + errorOut = error; + if(printErrors) { + trace ("Error: " + error ); + } + runOutput(0); + } + + try { + if(property0 == "post") { + var bytes = inputs[6].get(); + if(bytes == true){ + var data:haxe.io.Bytes = inputs[5].get(); + request.setPostBytes(data); + request.request(true); + }else{ + var data:Dynamic = inputs[5].get(); + request.setPostData(data.toString()); + request.request(true); + } + } else { + request.request(false); + } + } catch( e : Dynamic ) { + trace("Could not complete request: " + e); + } + + callbackType = 0; + runOutput(0); + + } + + override function get(from: Int): Dynamic { + + return switch (from) { + case 1: callbackType; + case 2: statusInt; + case 3: response; + case 4: errorOut; + default: throw "Unreachable"; + } + + } } From 26113bc0ca3fdba60116cee345195962616a656c Mon Sep 17 00:00:00 2001 From: e2002e Date: Fri, 29 Mar 2024 10:11:11 +0100 Subject: [PATCH 171/175] bump --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 44 ++------ Shaders/ssrefr_pass/ssrefr_pass.json | 10 -- .../armory/renderpath/RenderPathDeferred.hx | 100 +++++------------- .../armory/renderpath/RenderPathForward.hx | 4 +- .../arm/material/cycles_nodes/nodes_shader.py | 2 +- blender/arm/material/make_mesh.py | 6 +- 7 files changed, 40 insertions(+), 128 deletions(-) diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 138056afdd..84aa2fab3b 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -92,7 +92,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 reflected = reflect(normalize(viewPos), viewNormal); + vec3 reflected = reflect(viewPos, viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index a1e2567a28..08366beb83 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -14,17 +14,12 @@ uniform mat4 P; uniform mat3 V3; uniform vec2 cameraProj; -#ifdef _CPostprocess -uniform vec3 PPComp9; -uniform vec3 PPComp10; -#endif - in vec3 viewRay; in vec2 texCoord; out vec4 fragColor; vec3 hitCoord; -float d; +float depth; const int numBinarySearchSteps = 7; const int maxSteps = int(ceil(1.0 / ss_refractionRayStep) * ss_refractionSearchDist); @@ -39,14 +34,12 @@ vec2 getProjectedCoord(const vec3 hit) { return projectedCoord.xy; } - float getDeltaDepth(const vec3 hit) { - d = textureLod(gbufferD, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; - vec3 viewPos = normalize(getPosView(viewRay, d, cameraProj)); + depth = textureLod(gbufferD1, getProjectedCoord(hit), 0.0).r * 2.0 - 1.0; + vec3 viewPos = getPosView(viewRay, depth, cameraProj); return viewPos.z - hit.z; } - vec4 binarySearch(vec3 dir) { float ddepth; for (int i = 0; i < numBinarySearchSteps; i++) { @@ -55,28 +48,17 @@ vec4 binarySearch(vec3 dir) { ddepth = getDeltaDepth(hitCoord); if (ddepth < 0.0) hitCoord += dir; } - // Ugly discard of hits too far away - //using a divider of 500 doesn't work here unless the distance is set to at least 500 for a blender unit. - #ifdef _CPostprocess - if (abs(ddepth) > PPComp9.z) return vec4(texCoord.xy, 0.0, 0.0); - #else - if (abs(ddepth) > ss_refractionSearchDist) return vec4(texCoord.xy, 0.0, 0.0); - #endif + if (abs(ddepth) > ss_refractionSearchDist / 500) return vec4(0.0); return vec4(getProjectedCoord(hitCoord), 0.0, 1.0); } - vec4 rayCast(vec3 dir) { float ddepth; - #ifdef _CPostprocess - dir *= PPComp9.x; - #else - dir *= ss_refractionRayStep; - #endif + dir *= ss_refractionRayStep; for (int i = 0; i < maxSteps; i++) { hitCoord += dir; ddepth = getDeltaDepth(hitCoord); - if (ddepth > d) return binarySearch(dir); + if (ddepth > 0.0) return binarySearch(dir); } return vec4(0.0); } @@ -88,7 +70,7 @@ void main() { float ior = gr.x; float opac = gr.y; - d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; + float d = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; if (d == 1.0 || ior == 1.0 || opac == 1.0) { fragColor.rgb = textureLod(tex1, texCoord, 0.0).rgb; @@ -102,15 +84,11 @@ void main() { n = normalize(n); vec3 viewNormal = V3 * n; - vec3 viewPos = normalize(getPosView(viewRay, d, cameraProj)); - vec3 refracted = refract(viewPos, viewNormal, 1.0 / ior); + vec3 viewPos = getPosView(viewRay, d, cameraProj); + vec3 refracted = refract(-viewPos, viewNormal, ior); hitCoord = viewPos; -#ifdef _CPostprocess - vec3 dir = refracted * (1.0 - rand(texCoord) * PPComp10.y * roughness) * 2.0; -#else vec3 dir = refracted * (1.0 - rand(texCoord) * ss_refractionJitter * roughness) * 2.0; -#endif vec4 coords = rayCast(dir); vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); @@ -118,11 +96,7 @@ void main() { float refractivity = 1.0; -#ifdef _CPostprocess - float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp(-refracted.z, 0.0, 1.0) * clamp((PPComp9.z - length(viewPos - hitCoord)) * (1.0 / PPComp9.z), 0.0, 1.0) * coords.w; -#else float intensity = pow(refractivity, ss_refractionFalloffExp) * screenEdgeFactor * clamp((ss_refractionSearchDist - length(viewPos - hitCoord)) * (1.0 / ss_refractionSearchDist), 0.0, 1.0) * coords.w; -#endif intensity = clamp(intensity, 0.0, 1.0); vec3 refractionCol = textureLod(tex1, coords.xy, 0.0).rgb; diff --git a/Shaders/ssrefr_pass/ssrefr_pass.json b/Shaders/ssrefr_pass/ssrefr_pass.json index 4a86991140..716f2180e6 100755 --- a/Shaders/ssrefr_pass/ssrefr_pass.json +++ b/Shaders/ssrefr_pass/ssrefr_pass.json @@ -29,16 +29,6 @@ { "name": "cameraProj", "link": "_cameraPlaneProj" - }, - { - "name": "PPComp9", - "link": "_PPComp9", - "ifdef": ["_CPostprocess"] - }, - { - "name": "PPComp10", - "link": "_PPComp10", - "ifdef": ["_CPostprocess"] } ], "vertex_shader": "../include/pass_viewray2.vert.glsl", diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index afa234427e..dcffcd39c8 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -361,9 +361,29 @@ class RenderPathDeferred { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = "DEPTH16"; + t.format = "R32"; + t.scale = Inc.getSuperSampling(); + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "lbuffer0"; + t.width = 0; + t.height = 0; + t.format = Inc.getHdrFormat(); + t.displayp = Inc.getDisplayp(); + t.scale = Inc.getSuperSampling(); + t.depth_buffer = "main"; + path.createRenderTarget(t); + + var t = new RenderTargetRaw(); + t.name = "lbuffer1"; + t.width = 0; + t.height = 0; + t.format = Inc.getHdrFormat(); + t.displayp = Inc.getDisplayp(); t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); + } #end @@ -763,10 +783,6 @@ class RenderPathDeferred { { if (armory.data.Config.raw.rp_ssrefr != false) { - #if (!kha_opengl) - path.setDepthFrom("tex", "gbuffer1"); // Unbind depth so we can read it - #end - //save depth path.setTarget("gbufferD1"); path.bindTarget("_main", "tex"); @@ -777,85 +793,17 @@ class RenderPathDeferred { path.bindTarget("tex", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); - RenderPathCreator.setTargetMeshes(); + path.setTarget("lbuffer0", ["lbuffer1", "gbuffer_refraction"]); path.drawMeshes("refraction"); - path.setTarget("tex"); - path.bindTarget("_main", "gbufferD"); - path.bindTarget("gbuffer0", "gbuffer0"); - path.bindTarget("gbuffer1", "gbuffer1"); - - #if rp_gbuffer2 - { - path.bindTarget("gbuffer2", "gbuffer2"); - } - #end - - #if rp_gbuffer_emission - { - path.bindTarget("gbuffer_emission", "gbufferEmission"); - } - #end - - #if (rp_ssgi != "Off") - { - if (armory.data.Config.raw.rp_ssgi != false) { - path.bindTarget("singlea", "ssaotex"); - } - else { - path.bindTarget("empty_white", "ssaotex"); - } - } - #end - - var voxelao_pass = false; - #if rp_voxelao - if (armory.data.Config.raw.rp_voxels != false) - { - #if arm_config - voxelao_pass = true; - #end - path.bindTarget(voxels, "voxels"); - #if arm_voxelgi_temporal - { - path.bindTarget(voxelsLast, "voxelsLast"); - } - #end - } - #end - - #if rp_shadowmap - { - #if arm_shadowmap_atlas - Inc.bindShadowMapAtlas(); - #else - Inc.bindShadowMap(); - #end - } - #end - - #if rp_material_solid - path.drawShader("shader_datas/deferred_light_solid/deferred_light"); - #elseif rp_material_mobile - path.drawShader("shader_datas/deferred_light_mobile/deferred_light"); - #else - voxelao_pass ? - path.drawShader("shader_datas/deferred_light/deferred_light_VoxelAOvar") : - path.drawShader("shader_datas/deferred_light/deferred_light"); - #end - path.setTarget("tex"); path.bindTarget("refr", "tex1"); - path.bindTarget("tex", "tex"); + path.bindTarget("lbuffer0", "tex"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); - path.bindTarget("gbuffer0", "gbuffer0"); + path.bindTarget("lbuffer1", "gbuffer0"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); - - #if (!kha_opengl) - path.setDepthFrom("tex", "gbuffer0"); - #end } } #end diff --git a/Sources/armory/renderpath/RenderPathForward.hx b/Sources/armory/renderpath/RenderPathForward.hx index 13757b9538..0df28f28cd 100644 --- a/Sources/armory/renderpath/RenderPathForward.hx +++ b/Sources/armory/renderpath/RenderPathForward.hx @@ -111,7 +111,7 @@ class RenderPathForward { t.name = "lbuffer1"; t.width = 0; t.height = 0; - t.format = "RGBA64"; + t.format = Inc.getHdrFormat(); t.displayp = Inc.getDisplayp(); t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); @@ -147,7 +147,7 @@ class RenderPathForward { t.width = 0; t.height = 0; t.displayp = Inc.getDisplayp(); - t.format = "DEPTH16"; + t.format = "R32"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); } diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index 449e679497..75c7528979 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -158,7 +158,7 @@ def parse_bsdfglass(node: bpy.types.ShaderNodeBsdfGlass, out_socket: NodeSocket, c.write_normal(node.inputs[3]) state.out_roughness = c.parse_value_input(node.inputs[1]) if state.parse_opacity: - state.out_opacity = '0.5' # for refraction mix(refraction, color) + state.out_opacity = '0.0' state.out_ior = c.parse_value_input(node.inputs[2]) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 50eb059a15..989637e6f5 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -75,7 +75,7 @@ def make(context_id, rpasses): con_mesh = mat_state.data.add_context(con) mat_state.con_mesh = con_mesh - if rid == 'Forward' or blend: + if rid == 'Forward' or blend or 'refraction' in rpasses: if rpdat.arm_material_model == 'Mobile': make_forward_mobile(con_mesh) elif rpdat.arm_material_model == 'Solid': @@ -544,9 +544,9 @@ def make_forward(con_mesh): if not blend: mrt = 0 # mrt: multiple render targets if rpdat.rp_ssr: - mrt += 1 + mrt = 1 if rpdat.rp_ss_refraction: - mrt += 1 + mrt = 2 if mrt != 0: # Store light gbuffer for post-processing frag.add_out(f'vec4 fragColor[{mrt}+1]') From 4fb6ae9a8f635fe9e718dbfe459944a5972cac6a Mon Sep 17 00:00:00 2001 From: e2002e Date: Fri, 29 Mar 2024 10:29:18 +0100 Subject: [PATCH 172/175] add a buffer for the normal from RenderPathForward; --- .../armory/renderpath/RenderPathDeferred.hx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index dcffcd39c8..5f2d35722b 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -344,7 +344,7 @@ class RenderPathDeferred { t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); - //holds colors before refractive meshes are drawn + // holds colors before refractive meshes are drawn var t = new RenderTargetRaw(); t.name = "refr"; t.width = 0; @@ -355,7 +355,7 @@ class RenderPathDeferred { t.depth_buffer = "main"; path.createRenderTarget(t); - //holds background depth + // holds background depth var t = new RenderTargetRaw(); t.name = "gbufferD1"; t.width = 0; @@ -365,16 +365,7 @@ class RenderPathDeferred { t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); - var t = new RenderTargetRaw(); - t.name = "lbuffer0"; - t.width = 0; - t.height = 0; - t.format = Inc.getHdrFormat(); - t.displayp = Inc.getDisplayp(); - t.scale = Inc.getSuperSampling(); - t.depth_buffer = "main"; - path.createRenderTarget(t); - + // holds normals var t = new RenderTargetRaw(); t.name = "lbuffer1"; t.width = 0; @@ -793,12 +784,12 @@ class RenderPathDeferred { path.bindTarget("tex", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); - path.setTarget("lbuffer0", ["lbuffer1", "gbuffer_refraction"]); + path.setTarget("tex", ["lbuffer1", "gbuffer_refraction"]); path.drawMeshes("refraction"); path.setTarget("tex"); path.bindTarget("refr", "tex1"); - path.bindTarget("lbuffer0", "tex"); + path.bindTarget("tex", "tex"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); path.bindTarget("lbuffer1", "gbuffer0"); From 00688efee1e96e9040d094a69e9acdaf6cce33ac Mon Sep 17 00:00:00 2001 From: e2002e Date: Fri, 29 Mar 2024 17:21:21 +0100 Subject: [PATCH 173/175] parse_opacity discards fragment even with refraction on. --- blender/arm/material/make_mesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 53a2654c8d..94184c9c73 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -195,7 +195,7 @@ def make_deferred(con_mesh, rpasses): rpdat = arm.utils.get_rp() arm_discard = mat_state.material.arm_discard - parse_opacity = arm_discard or 'translucent' in rpasses or 'refraction' in rpasses + parse_opacity = arm_discard or 'translucent' make_base(con_mesh, parse_opacity=parse_opacity) @@ -203,7 +203,7 @@ def make_deferred(con_mesh, rpasses): vert = con_mesh.vert tese = con_mesh.tese - if parse_opacity and not 'refraction' in rpasses: + if parse_opacity: if arm_discard: opac = mat_state.material.arm_discard_opacity else: @@ -590,7 +590,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag = con_mesh.frag tese = con_mesh.tese - if (parse_opacity or arm_discard) and not '_SSRefraction' in wrd.world_defs: + if (parse_opacity and not '_SSRefraction' in wrd.world_defs) or arm_discard: if arm_discard or blend: opac = mat_state.material.arm_discard_opacity frag.write('if (opacity < {0}) discard;'.format(opac)) From 036e6db46b469fe9d68852b7ffb547ec11b0891f Mon Sep 17 00:00:00 2001 From: e2002e Date: Sat, 30 Mar 2024 16:12:06 +0100 Subject: [PATCH 174/175] start making refraction pass a pass on top of a mesh pass --- blender/arm/material/make_mesh.py | 4 ++-- blender/arm/material/make_shader.py | 3 ++- blender/arm/material/mat_utils.py | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/blender/arm/material/make_mesh.py b/blender/arm/material/make_mesh.py index 94184c9c73..2933a02cca 100644 --- a/blender/arm/material/make_mesh.py +++ b/blender/arm/material/make_mesh.py @@ -75,7 +75,7 @@ def make(context_id, rpasses): con_mesh = mat_state.data.add_context(con) mat_state.con_mesh = con_mesh - if rid == 'Forward' or blend or 'refraction' in rpasses: + if rid == 'Forward' or blend: if rpdat.arm_material_model == 'Mobile': make_forward_mobile(con_mesh) elif rpdat.arm_material_model == 'Solid': @@ -590,7 +590,7 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): frag = con_mesh.frag tese = con_mesh.tese - if (parse_opacity and not '_SSRefraction' in wrd.world_defs) or arm_discard: + if parse_opacity or arm_discard: if arm_discard or blend: opac = mat_state.material.arm_discard_opacity frag.write('if (opacity < {0}) discard;'.format(opac)) diff --git a/blender/arm/material/make_shader.py b/blender/arm/material/make_shader.py index 115b3b0127..4126ff663d 100644 --- a/blender/arm/material/make_shader.py +++ b/blender/arm/material/make_shader.py @@ -16,6 +16,7 @@ import arm.material.make_mesh as make_mesh import arm.material.make_overlay as make_overlay import arm.material.make_transluc as make_transluc +import arm.material.make_refract as make_refract import arm.material.make_voxel as make_voxel import arm.material.mat_state as mat_state import arm.material.mat_utils as mat_utils @@ -96,7 +97,7 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_armus con = make_transluc.make(rp) elif rp == 'refraction': - con = make_mesh.make(rp, rpasses) + con = make_refract.make(rp) elif rp == 'overlay': con = make_overlay.make(rp) diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py index f660ce4d8b..2809f6e143 100644 --- a/blender/arm/material/mat_utils.py +++ b/blender/arm/material/mat_utils.py @@ -42,14 +42,14 @@ def get_rpasses(material): ar.append('decal') elif material.arm_overlay: ar.append('overlay') - elif is_transluc(material) and not material.arm_discard and not material.arm_blending and rpdat.rp_ss_refraction: - ar.append('refraction') else: ar.append('mesh') for con in add_mesh_contexts: ar.append(con) - if is_transluc(material) and not material.arm_discard and rpdat.rp_translucency_state != 'Off' and not material.arm_blending: + if is_transluc(material) and not material.arm_discard and rpdat.rp_translucency_state != 'Off' and not material.arm_blending and not rpdat.rp_ss_refraction: ar.append('translucent') + elif is_transluc(material) and not material.arm_discard and not material.arm_blending and rpdat.rp_ss_refraction: + ar.append('refraction') if rpdat.rp_voxelao and has_voxels: ar.append('voxel') if rpdat.rp_renderer == 'Forward' and rpdat.rp_depthprepass and not material.arm_blending and not material.arm_particle_flag: From a469aeec3344519f2dba4ef7fed2eef7b1aa329a Mon Sep 17 00:00:00 2001 From: e2002e Date: Mon, 1 Apr 2024 10:17:05 +0200 Subject: [PATCH 175/175] use correct normalize values, add refraction pass on top of mesh pass and use an extra python file to create the shader like transluc. --- Shaders/ssr_pass/ssr_pass.frag.glsl | 2 +- Shaders/ssrefr_pass/ssrefr_pass.frag.glsl | 3 +- .../armory/renderpath/RenderPathDeferred.hx | 15 +----- blender/arm/material/make_refract.py | 48 +++++++++++++++++++ 4 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 blender/arm/material/make_refract.py diff --git a/Shaders/ssr_pass/ssr_pass.frag.glsl b/Shaders/ssr_pass/ssr_pass.frag.glsl index 84aa2fab3b..138056afdd 100644 --- a/Shaders/ssr_pass/ssr_pass.frag.glsl +++ b/Shaders/ssr_pass/ssr_pass.frag.glsl @@ -92,7 +92,7 @@ void main() { vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 reflected = reflect(viewPos, viewNormal); + vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; #ifdef _CPostprocess diff --git a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl index 08366beb83..91ccb21155 100644 --- a/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl +++ b/Shaders/ssrefr_pass/ssrefr_pass.frag.glsl @@ -81,11 +81,10 @@ void main() { vec3 n; n.z = 1.0 - abs(enc.x) - abs(enc.y); n.xy = n.z >= 0.0 ? enc.xy : octahedronWrap(enc.xy); - n = normalize(n); vec3 viewNormal = V3 * n; vec3 viewPos = getPosView(viewRay, d, cameraProj); - vec3 refracted = refract(-viewPos, viewNormal, ior); + vec3 refracted = refract(normalize(viewPos), viewNormal, 1.0 / ior); hitCoord = viewPos; vec3 dir = refracted * (1.0 - rand(texCoord) * ss_refractionJitter * roughness) * 2.0; diff --git a/Sources/armory/renderpath/RenderPathDeferred.hx b/Sources/armory/renderpath/RenderPathDeferred.hx index 5f2d35722b..e7f14aec20 100644 --- a/Sources/armory/renderpath/RenderPathDeferred.hx +++ b/Sources/armory/renderpath/RenderPathDeferred.hx @@ -364,17 +364,6 @@ class RenderPathDeferred { t.format = "R32"; t.scale = Inc.getSuperSampling(); path.createRenderTarget(t); - - // holds normals - var t = new RenderTargetRaw(); - t.name = "lbuffer1"; - t.width = 0; - t.height = 0; - t.format = Inc.getHdrFormat(); - t.displayp = Inc.getDisplayp(); - t.scale = Inc.getSuperSampling(); - path.createRenderTarget(t); - } #end @@ -784,7 +773,7 @@ class RenderPathDeferred { path.bindTarget("tex", "tex"); path.drawShader("shader_datas/copy_pass/copy_pass"); - path.setTarget("tex", ["lbuffer1", "gbuffer_refraction"]); + path.setTarget("tex", ["gbuffer0", "gbuffer_refraction"]); path.drawMeshes("refraction"); path.setTarget("tex"); @@ -792,7 +781,7 @@ class RenderPathDeferred { path.bindTarget("tex", "tex"); path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD1"); - path.bindTarget("lbuffer1", "gbuffer0"); + path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer_refraction", "gbuffer_refraction"); path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass"); } diff --git a/blender/arm/material/make_refract.py b/blender/arm/material/make_refract.py new file mode 100644 index 0000000000..6d13230128 --- /dev/null +++ b/blender/arm/material/make_refract.py @@ -0,0 +1,48 @@ +import bpy + +import arm +import arm.material.cycles as cycles +import arm.material.mat_state as mat_state +import arm.material.make_mesh as make_mesh +import arm.material.make_finalize as make_finalize +import arm.assets as assets + +if arm.is_reload(__name__): + cycles = arm.reload_module(cycles) + mat_state = arm.reload_module(mat_state) + make_mesh = arm.reload_module(make_mesh) + make_finalize = arm.reload_module(make_finalize) + assets = arm.reload_module(assets) +else: + arm.enable_reload(__name__) + + +def make(context_id): + con_refract = mat_state.data.add_context({ 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' }) + + make_mesh.make_forward_base(con_refract, parse_opacity=True, transluc_pass=True) + + vert = con_refract.vert + frag = con_refract.frag + tese = con_refract.tese + + frag.add_include('std/gbuffer.glsl') + frag.add_out('vec4 fragColor[3]') + + # Remove fragColor = ...; + frag.main = frag.main[:frag.main.rfind('fragColor')] + frag.write('\n') + + wrd = bpy.data.worlds['Arm'] + + frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') + frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);') + frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));') + frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);') + frag.write('fragColor[2] = vec4(ior, opacity, 0.0, 0.0);') + make_finalize.make(con_refract) + + # assets.vs_equal(con_refract, assets.shader_cons['transluc_vert']) # shader_cons has no transluc yet + # assets.fs_equal(con_refract, assets.shader_cons['transluc_frag']) + + return con_refract

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 ZcAgz$Pp~K2ZnnEU$(~HkLK9l*e*jr~ZsGs{ literal 0 HcmV?d00001 diff --git a/Assets/font_license_roboto.txt b/Assets/font_license_roboto.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/Assets/font_license_roboto.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Assets/font_mono.ttf b/Assets/font_mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1c3576f087728ca345e3818b0a0ccdc0ddf88545 GIT binary patch literal 19792 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 YV!^IP;1OkyfBwYj57>3-{}=ZE0EVHf$^ZZW literal 0 HcmV?d00001 diff --git a/Assets/font_mono_full.ttf b/Assets/font_mono_full.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b158a334eb372a9ab2ecd4f2566e60d561e71a9f GIT binary patch literal 114624 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

}@e(r{vph5f;zFlP@O9aK*r?PNjH$~ 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