Compare commits

..

204 Commits

Author SHA1 Message Date
3bee97a560 Update leenkx/blender/lnx/material/cycles.py 2025-11-14 17:41:39 +00:00
4f4f28d62f Merge pull request 'main' (#111) from Onek8/LNXSDK:main into main
Reviewed-on: #111
2025-11-06 16:32:50 +00:00
7076fb6b7e Update leenkx/blender/lnx/material/cycles.py 2025-11-06 16:29:46 +00:00
b72a22b5e9 Update leenkx/blender/lnx/props_ui.py 2025-11-06 16:17:18 +00:00
b265ab863c Update leenkx/blender/lnx/props.py 2025-11-06 16:13:39 +00:00
48f5575e4e Update leenkx/Sources/leenkx/logicnode/OnContactNode.hx 2025-10-03 07:56:12 +00:00
f2c4be6336 Update leenkx/Sources/leenkx/logicnode/HasContactNode.hx 2025-10-03 07:55:46 +00:00
2ddc938db8 Update leenkx/Sources/leenkx/logicnode/AnyContactNode.hx 2025-10-03 07:55:14 +00:00
5eb735ada2 Update leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx 2025-10-03 07:52:01 +00:00
9894cc20f2 Update leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx 2025-10-03 07:51:32 +00:00
dbe6d0829a Add leenkx/Sources/leenkx/trait/physics/PhysicsCache.hx 2025-10-03 07:51:07 +00:00
6f383e2ab2 Update leenkx/Sources/leenkx/trait/physics/PhysicsWorld.hx 2025-10-03 05:38:54 +00:00
5c2d29d7ce Update leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx 2025-10-03 05:37:50 +00:00
28579e14d7 Update leenkx/Sources/leenkx/logicnode/AnyContactNode.hx 2025-10-03 05:37:06 +00:00
2ec6f43cc5 Update leenkx/Sources/leenkx/logicnode/OnContactNode.hx 2025-10-03 05:36:14 +00:00
027021815a Update leenkx/Sources/leenkx/logicnode/HasContactNode.hx 2025-10-03 05:35:48 +00:00
b9b387803f Add leenkx/blender/lnx/logicnode/physics/LN_any_contact.py 2025-10-03 05:06:23 +00:00
e05d9d0237 Update leenkx/Sources/leenkx/logicnode/HasContactNode.hx 2025-10-03 05:04:48 +00:00
c908e6cad2 Update leenkx/Sources/leenkx/logicnode/OnContactNode.hx 2025-10-03 05:04:18 +00:00
506a0a0245 Add leenkx/Sources/leenkx/logicnode/AnyContactNode.hx 2025-10-03 05:03:29 +00:00
5cf33724e4 Update leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx 2025-10-03 05:02:28 +00:00
ac5aa3d19c Merge pull request 'Hashlink fix' (#110) from Onek8/LNXSDK:main into main
Reviewed-on: #110
2025-10-03 03:21:56 +00:00
0c534ee632 Update leenkx/Sources/iron/object/ParticleSystem.hx 2025-10-01 01:42:51 +00:00
e3e7855d26 Merge pull request 'main' (#109) from Onek8/LNXSDK:main into main
Reviewed-on: #109
2025-09-30 05:59:23 +00:00
f7917974f8 Update leenkx/Sources/iron/object/ObjectAnimation.hx 2025-09-30 05:52:44 +00:00
fa2d8f05d5 Update leenkx/Sources/iron/object/ObjectAnimation.hx 2025-09-30 05:35:50 +00:00
73fcb55acc Update leenkx/blender/lnx/props_traits.py 2025-09-29 05:28:13 +00:00
c24baa3364 Update leenkx/blender/lnx/props_traits_props.py 2025-09-29 05:27:43 +00:00
4517c4863f Merge pull request 'Downward support to 2.8 LTS!!' (#108) from Onek8/LNXSDK:main into main
Reviewed-on: #108
2025-09-28 20:02:58 +00:00
1299306e09 Update leenkx.py 2025-09-28 20:01:00 +00:00
f97d8fd846 Blender 2.8 - 4.5 Support 2025-09-28 12:44:04 -07:00
8f8d4b1376 Update leenkx/blender/lnx/props_traits.py 2025-09-28 00:09:57 +00:00
a926fa8dbb Update leenkx/blender/lnx/nodes_logic.py 2025-09-27 03:03:08 +00:00
6c3efa6c83 Update leenkx/blender/lnx/props_ui.py 2025-09-24 01:54:38 +00:00
21afad6d09 Update leenkx/blender/lnx/exporter.py 2025-09-24 01:53:43 +00:00
04c6983a09 Update leenkx/Sources/iron/data/SceneFormat.hx 2025-09-24 01:52:47 +00:00
45966ef0bb Update leenkx/blender/lnx/props.py 2025-09-24 01:51:11 +00:00
a72edc6203 Update leenkx/Sources/iron/object/ParticleSystem.hx 2025-09-24 01:50:03 +00:00
6af1ef2df1 Update leenkx/blender/lnx/props_ui.py 2025-09-24 01:33:47 +00:00
46e3047877 Update leenkx/blender/lnx/props.py 2025-09-23 19:57:53 +00:00
de74af215a Merge pull request 'main' (#107) from Onek8/LNXSDK:main into main
Reviewed-on: #107
2025-09-23 17:54:11 +00:00
b6e96553c2 Update leenkx/blender/lnx/lightmapper/utility/build.py 2025-09-19 22:52:01 +00:00
58e009f709 Terrain Generation fix 2025-09-19 21:17:58 +00:00
e88f101ca6 t3du - Particle info random 2025-09-19 19:40:49 +00:00
d28d59b9e6 t3du - Particle info random 2025-09-19 19:38:12 +00:00
a4398c7279 t3du - Particle info random 2025-09-19 19:31:45 +00:00
abedfd799e t3du - Fix World Errors 2025-09-19 19:28:54 +00:00
4520422f6b t3du - Fix World Errors 2025-09-19 19:27:02 +00:00
88418c06c3 t3du - Fix World Errors 2025-09-19 19:25:30 +00:00
aedc2783ab t3du - Labels for finding nodes 2025-09-19 19:22:45 +00:00
1505414c4c t3du - Labels for finding nodes 2025-09-19 19:19:25 +00:00
fa818602c4 t3du - Labels for finding nodes 2025-09-19 19:18:05 +00:00
79dc458671 t3du - Labels for finding nodes 2025-09-19 19:15:41 +00:00
8e635fb1e9 t3du - Labels for finding nodes 2025-09-19 19:11:47 +00:00
4c2e6ab26a t3du - Probabilistic Index Node 2025-09-19 19:09:18 +00:00
2371e3777e t3du - Probabilistic Index Node 2025-09-19 19:08:03 +00:00
b458b77e5c moisesjpelaez - Include external blend files on build 2025-09-19 19:04:43 +00:00
9b76f8cca9 moisesjpelaez - Include external blend files on build 2025-09-19 19:03:22 +00:00
5f2acb209e moisesjpelaez - Include external blend files on build 2025-09-19 19:00:50 +00:00
6fc446e7a9 moisesjpelaez - General Fixes 2025-09-19 18:54:44 +00:00
71e57026e1 moisesjpelaez - General Fixes 2025-09-19 18:53:25 +00:00
5288a98440 moisesjpelaez - General Fixes 2025-09-19 18:49:09 +00:00
35e346be39 moisesjpelaez - General Fixes 2025-09-19 18:39:54 +00:00
843ef0b058 moisesjpelaez - General Fixes 2025-09-19 18:39:14 +00:00
177890bf39 moisesjpelaez - General Fixes 2025-09-19 18:37:01 +00:00
9ac37e6dc7 moisesjpelaez - General Fixes 2025-09-19 18:34:42 +00:00
e697437778 moisesjpelaez - General Fixes 2025-09-19 18:33:44 +00:00
c94fc0fd97 moisesjpelaez - General Fixes 2025-09-19 18:29:52 +00:00
cd0a6f6788 Update leenkx/Sources/iron/data/SceneFormat.hx 2025-09-19 18:28:19 +00:00
4400e0e9c8 moisesjpelaez - General Fixes 2025-09-19 18:27:22 +00:00
20cf07cfc3 moisesjpelaez - General Fixes 2025-09-19 18:25:54 +00:00
1939f19c05 moisesjpelaez - General Fixes 2025-09-19 18:24:19 +00:00
0d2b152ccb moisesjpelaez - General Fixes 2025-09-19 18:15:23 +00:00
7f58e0fc85 moisesjpelaez - General Fixes 2025-09-19 18:13:00 +00:00
0e4a6575c7 moisesjpelaez - General Material Updates 2025-09-19 18:09:04 +00:00
024676f43a moisesjpelaez - General Material Updates 2025-09-19 17:43:54 +00:00
8fe758862c moisesjpelaez - General Material Updates 2025-09-19 17:35:59 +00:00
1f3d1b47ae moisesjpelaez - General Material Updates 2025-09-19 17:34:27 +00:00
f659a3c2be moisesjpelaez - General Material Updates 2025-09-19 17:32:38 +00:00
6eeb9017d4 moisesjpelaez - General Material Updates 2025-09-19 17:30:42 +00:00
afe89c3834 Update leenkx/Sources/iron/data/ShaderData.hx 2025-09-19 17:27:14 +00:00
8b695f72bb moisesjpelaez - General Material Updates 2025-09-19 17:25:03 +00:00
3d99fa60c0 moisesjpelaez - General Material Updates 2025-09-19 17:23:42 +00:00
43be7729ba moisesjpelaez - Tween var 2025-09-19 17:17:41 +00:00
de0b1075c2 moisesjpelaez - Time Fix 2025-09-19 17:13:16 +00:00
c7aba23fa4 t3du - Fix DOF condition 2025-09-19 17:08:21 +00:00
881f3267cc t3du - Fix DOF condition 2025-09-19 17:06:10 +00:00
19b79d61c7 ObiNoWanKenobi - FirstPersonController Changes 2025-09-19 17:03:20 +00:00
fcbab54a0c moisesjpelaez - General Fixes 2025-09-19 16:57:49 +00:00
8fd05d5514 Update leenkx/Sources/leenkx/renderpath/RenderPathForward.hx 2025-08-28 19:21:48 +00:00
ad4013ed75 Update leenkx/Sources/leenkx/renderpath/RenderPathForward.hx 2025-08-28 19:11:31 +00:00
8ac567b57b Merge pull request 'Update leenkx/Shaders/std/conetrace.glsl' (#104) from Onek8/LNXSDK:main into main
Reviewed-on: #104
2025-08-14 23:01:04 +00:00
43b7ae7060 Update leenkx/Shaders/std/conetrace.glsl 2025-08-14 22:58:57 +00:00
29e9e71a6a Merge pull request 'main' (#103) from Onek8/LNXSDK:main into main
Reviewed-on: #103
2025-08-14 21:29:54 +00:00
bfb85b0a3b Update leenkx/Sources/iron/Scene.hx 2025-08-14 20:29:28 +00:00
ef99b800e0 Update leenkx/Sources/iron/App.hx 2025-08-14 20:27:20 +00:00
7cca955fc5 Update leenkx/Sources/iron/App.hx 2025-08-14 20:26:33 +00:00
7e7bbd5eae merge upstream 2025-08-14 20:24:23 +00:00
c31b2a18ad Update leenkx/blender/lnx/logicnode/draw/LN_draw_string.py 2025-08-14 19:03:28 +00:00
fb47bf2564 Update leenkx/blender/lnx/logicnode/draw/LN_draw_Text_Area_string.py 2025-08-14 19:01:59 +00:00
7ae6750620 Update leenkx/blender/lnx/logicnode/camera/LN_set_camera_start_end.py 2025-08-14 19:00:58 +00:00
5b87010f76 Update leenkx/Sources/leenkx/trait/internal/DebugConsole.hx 2025-08-14 18:58:52 +00:00
97e952fc15 Update leenkx/Sources/leenkx/logicnode/DrawStringNode.hx 2025-08-14 18:57:13 +00:00
b440539d65 Merge pull request 'main' (#102) from Onek8/LNXSDK:main into main
Reviewed-on: #102
2025-07-23 17:34:02 +00:00
60a9db6459 Update api/api.hxml 2025-07-22 21:54:56 +00:00
3b5a93c92a Update leenkx/Sources/leenkx/trait/PhysicsBreak.hx 2025-07-22 21:51:25 +00:00
4af990796e t3du - Add TSceneFormat as Trait property type 2025-07-21 23:27:34 +00:00
9fb4916c3c t3du - Add TSceneFormat as Trait property type 2025-07-21 23:24:30 +00:00
f61d5833bb Update leenkx/blender/lnx/exporter.py 2025-07-21 23:15:59 +00:00
40b52be713 t3du - Add TSceneFormat as Trait property type 2025-07-21 23:12:43 +00:00
07d8422f22 Merge pull request 'Update leenkx/Sources/iron/system/Time.hx' (#101) from Onek8/LNXSDK:main into main
Reviewed-on: #101
2025-07-19 20:23:46 +00:00
7179d42b27 Update leenkx/Sources/iron/system/Time.hx 2025-07-19 20:07:08 +00:00
99a5d7d445 Merge pull request 'Update leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx' (#100) from Onek8/LNXSDK:main into main
Reviewed-on: #100
2025-07-17 15:58:08 +00:00
5e0acd3d5d Update leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx 2025-07-16 22:18:14 +00:00
f4077e461b Merge pull request 'Tangazo - Once Node + Set Object Delayed Location Node [ Additional Clean Handler ]' (#99) from Onek8/LNXSDK:main into main
Reviewed-on: #99
2025-07-16 05:23:34 +00:00
28943f1522 Add leenkx/Sources/leenkx/logicnode/SetObjectDelayedLocationNode.hx 2025-07-16 05:15:59 +00:00
df4feac132 Add leenkx/blender/lnx/logicnode/object/LN_set_object_delayed_location.py 2025-07-16 05:14:28 +00:00
82412dbf81 Add leenkx/Sources/leenkx/logicnode/OnceNode.hx 2025-07-15 22:07:02 +00:00
6afc209db7 Add leenkx/blender/lnx/logicnode/logic/LN_once.py 2025-07-15 22:06:11 +00:00
e9aae53be9 t3du - Fix attribute error rp_gi 2025-07-15 19:05:14 +00:00
a65675ef75 Update leenkx/blender/lnx/handlers.py 2025-07-15 17:57:38 +00:00
8f073c5ae1 merge upstream 2025-07-15 17:56:41 +00:00
08d08e42d9 Merge pull request 'improved mouse look node and added missing rigid body settings in Properties > physics > leenkx Props' (#98) from wuaieyo/LNXSDK:main into main
Reviewed-on: #98
2025-07-15 02:59:53 +00:00
a1ee335c68 removed the translation daping and rotation dampign because they would override the alreayd existedt under Dynamics panel 2025-07-13 04:23:51 +02:00
de6bf8a08a added last needed important rigid body settings in the blender RB leenkx settings for game engine ? like min max velocity,damping and lock translation and rotationboolean settings 2025-07-11 19:21:01 +02:00
b9848cd2dc revert e922cc38e6
revert Update leenkx/Shaders/std/shadows.glsl
2025-07-09 23:20:46 +00:00
e922cc38e6 Update leenkx/Shaders/std/shadows.glsl 2025-07-09 23:17:55 +00:00
57f0e937d0 fixed properties numbering, comments and LNXfactor to LNXFloat 2025-07-08 22:48:16 +02:00
e234c8615c merge upstream 2025-07-08 20:01:27 +00:00
e594518e57 Merge pull request 'main' (#96) from Onek8/LNXSDK:main into main
Reviewed-on: #96
2025-07-06 17:29:11 +00:00
a41be0f436 Update leenkx/blender/lnx/material/cycles_nodes/nodes_input.py 2025-07-06 17:23:04 +00:00
1306033b36 Update leenkx/Sources/leenkx/logicnode/SetParticleDataNode.hx 2025-07-05 22:03:54 +00:00
eee0011cdd Update leenkx/Sources/leenkx/logicnode/GetWorldOrientationNode.hx 2025-07-05 21:52:57 +00:00
315ac0bd16 Update lib/aura/Sources/aura/dsp/panner/StereoPanner.hx 2025-07-05 21:49:10 +00:00
f289e6f89c merge upstream 2025-07-03 07:46:23 +00:00
700d236bf1 Merge pull request 'main' (#94) from Onek8/LNXSDK:main into main
Reviewed-on: #94
2025-07-03 04:12:51 +00:00
f228eab8d3 Add leenkx/Sources/leenkx/logicnode/SetAudioPositionNode.hx 2025-07-03 03:55:23 +00:00
863d884b76 Add leenkx/Sources/leenkx/logicnode/GetAudioPositionNode.hx 2025-07-03 03:54:54 +00:00
34e0f5a282 Add leenkx/blender/lnx/logicnode/custom/LN_set_audio_position.py 2025-07-03 03:53:59 +00:00
45e2e52008 Add leenkx/blender/lnx/logicnode/custom/LN_get_audio_position.py 2025-07-03 03:52:39 +00:00
444a215e63 made default resolution adaptive sensiticity because makes more sense, removed other things since no difference and dunno 2025-07-03 05:27:24 +02:00
fb2d2a1a7c Add leenkx/Sources/leenkx/logicnode/SetPositionSpeakerNode.hx 2025-07-03 01:23:13 +00:00
f88c04abea Add leenkx/Sources/leenkx/logicnode/GetPositionSpeakerNode.hx 2025-07-03 01:22:44 +00:00
6fdd4b3f70 Add leenkx/blender/lnx/logicnode/sound/LN_get_position_speaker.py 2025-07-03 01:21:37 +00:00
a389c27d75 Add leenkx/blender/lnx/logicnode/sound/LN_set_position_speaker.py 2025-07-03 01:20:22 +00:00
1909c3da9f Update leenkx/blender/lnx/material/cycles.py 2025-07-02 15:29:05 +00:00
5824bd91aa Merge pull request 't3du [ Repe ] - VR Code' (#93) from Onek8/LNXSDK:main into main
Reviewed-on: #93
2025-07-02 05:19:25 +00:00
43fe559eef t3du - Restore VR code 2025-07-02 05:16:53 +00:00
12c09545ce t3du - Restore VR code 2025-07-02 05:15:34 +00:00
0e60951ec9 t3du - Restore VR code 2025-07-02 05:14:05 +00:00
ccb8b358d3 t3du - Restore VR code 2025-07-02 05:11:23 +00:00
1a8586777b Merge pull request 'main' (#91) from Onek8/LNXSDK:main into main
Reviewed-on: #91
2025-06-30 21:01:50 +00:00
3721c774a1 Update leenkx/blender/lnx/material/node_meta.py 2025-06-30 20:59:56 +00:00
a58fba408d Update leenkx/blender/lnx/material/cycles.py 2025-06-30 20:58:56 +00:00
268fba6cd5 Merge pull request 'main' (#90) from Onek8/LNXSDK:main into main
Reviewed-on: #90
2025-06-30 20:39:41 +00:00
4ab14ce6c8 merge upstream 2025-06-30 20:39:14 +00:00
9023e8d1da Merge pull request 'added Mouse Look node for FSP style movement of object like camera...' (#89) from wuaieyo/LNXSDK:main into main
Reviewed-on: #89
2025-06-30 20:37:10 +00:00
b58c7a9632 Update leenkx/blender/lnx/material/cycles.py 2025-06-30 20:33:04 +00:00
99b70622f5 Update leenkx/blender/lnx/material/node_meta.py 2025-06-30 20:31:50 +00:00
647b73b746 added Mouse Look node for FSP style movement of object like camera... 2025-06-30 06:35:06 +02:00
935c30ec08 Merge pull request 'main' (#88) from wuaieyo/LNXSDK:main into main
Reviewed-on: #88
2025-06-27 22:39:32 +00:00
0b0d597f89 merge upstream 2025-06-27 22:31:19 +00:00
d5878afb30 Merge pull request 't3du [ Repe ] + Moisesjpelaez Fixes' (#87) from Onek8/LNXSDK:main into main
Reviewed-on: #87
2025-06-27 22:28:35 +00:00
96b55a1a56 t3du - Add SetLightShadowNode for controlling light shadows 2025-06-27 18:03:37 +00:00
91b3072305 t3du - Add SetLightShadowNode for controlling light shadows 2025-06-27 17:57:26 +00:00
1d0b338d92 t3du - Particle export: add support for linked particle info 2025-06-27 17:52:19 +00:00
8e83c0d0d0 moisesjpelaez - Fix linked particle's render object vertex shader export 2025-06-27 17:49:35 +00:00
927baec4df t3du - Show world name in debug console 2025-06-27 17:46:36 +00:00
f5c9e70d1a 1. added local rotation so that if the source object is child then it would still align to the target object. 2. removed rotation output socket since is not really needed. 2025-06-26 03:05:11 +02:00
0423a735fc Update leenkx/Sources/leenkx/logicnode/PlayAnimationTreeNode.hx 2025-06-24 18:43:30 +00:00
bd413917fc Merge pull request 'main' (#84) from Onek8/LNXSDK:main into main
Reviewed-on: #84
2025-06-24 18:18:27 +00:00
852377f60d merge upstream 2025-06-24 18:07:23 +00:00
e17e9a8e35 Merge pull request 'added new set look at rotation node' (#83) from wuaieyo/LNXSDK:main into main
Reviewed-on: #83
2025-06-24 18:06:48 +00:00
4055c979a1 add new Set Look At Rotation Node for making logic nodes great again 2025-06-24 19:47:03 +02:00
06b003ecdb revert 14cf5cebed
revert add new node called Set Look at Rotation
2025-06-24 17:38:18 +00:00
fd7f215bb2 .gitattributes 2025-06-24 17:31:59 +00:00
6a1df9ec46 merge upstream 2025-06-24 17:31:29 +00:00
deccac3c46 Merge pull request '.gitignore' (#81) from Onek8/LNXSDK:main into main
Reviewed-on: #81
2025-06-24 17:21:28 +00:00
432b0210b2 .gitignore 2025-06-24 17:13:27 +00:00
14cf5cebed add new node called Set Look at Rotation 2025-06-24 18:39:16 +02:00
2307e1504f Merge pull request 't3du [ Repe ] - Fix particle export' (#78) from Onek8/LNXSDK:main into main
Reviewed-on: #78
2025-06-22 20:25:21 +00:00
1ebfecb644 Update leenkx/blender/lnx/exporter.py 2025-06-22 20:13:33 +00:00
175b575b23 Update leenkx/blender/lnx/exporter.py 2025-06-22 20:12:43 +00:00
63943a9cbf Merge pull request 'Remove pycache folders' (#77) from Onek8/LNXSDK:main into main
Reviewed-on: #77
2025-06-20 20:51:03 +00:00
224d9be76f Remove cache folders 2025-06-20 13:47:02 -07:00
62d3c8757b Update leenkx/blender/lnx/exporter.py 2025-06-20 16:06:08 +00:00
3785f93573 Merge pull request 'main' (#75) from Onek8/LNXSDK:main into main
Reviewed-on: #75
2025-06-20 15:59:18 +00:00
ffb276745f Update leenkx/blender/lnx/exporter.py 2025-06-20 15:55:56 +00:00
d1c9258da5 Update leenkx/blender/lnx/exporter.py 2025-06-20 15:55:29 +00:00
3e0cd2be35 Update leenkx/blender/lnx/props_traits.py 2025-06-20 15:50:09 +00:00
1fd1973470 Update leenkx/blender/lnx/material/make_mesh.py 2025-06-18 17:12:53 +00:00
a01c72ef76 Merge pull request 'Update leenkx/blender/lnx/exporter.py' (#74) from Onek8/LNXSDK:main into main
Reviewed-on: #74
2025-06-17 16:38:21 +00:00
4b01a562c9 Update leenkx/blender/lnx/exporter.py 2025-06-16 02:34:28 +00:00
6972d9abc4 Merge pull request 't3du [ Repe ] - Camera Render Filter' (#73) from Onek8/LNXSDK:main into main
Reviewed-on: #73
2025-06-13 15:14:13 +00:00
63c6b9d98b t3du - Camera Render Filter 2025-06-12 22:38:30 +00:00
313d24bbc8 t3du - Camera Render Filter 2025-06-12 22:35:21 +00:00
6d2812306d t3du - Camera Render Filter 2025-06-12 22:23:33 +00:00
e84d6ada84 t3du - Camera Render Filter 2025-06-12 22:21:24 +00:00
5057f2b946 t3du - Camera Render Filter 2025-06-12 22:16:22 +00:00
2715fe3398 t3du - Camera Render Filter 2025-06-12 22:14:08 +00:00
7cb8b8a2d2 t3du - Camera Render Filter 2025-06-12 22:11:59 +00:00
cd606009e0 t3du - Camera Render Filter 2025-06-12 22:10:19 +00:00
965162b101 t3du - Camera Render Filter 2025-06-12 22:08:21 +00:00
c61a57bfb3 t3du - Camera Render Filter 2025-06-12 22:07:16 +00:00
824 changed files with 4460 additions and 1156 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.hdr binary
blender/lnx/props.py ident

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
*.DS_Store

0
Krom/Krom Normal file → Executable file
View File

View File

@ -2,10 +2,12 @@
-cp ../Kha/Backends/Krom -cp ../Kha/Backends/Krom
-cp ../leenkx/Sources -cp ../leenkx/Sources
-cp ../iron/Sources -cp ../iron/Sources
-cp ../lib/aura/Sources
-cp ../lib/haxebullet/Sources -cp ../lib/haxebullet/Sources
-cp ../lib/haxerecast/Sources -cp ../lib/haxerecast/Sources
-cp ../lib/zui/Sources -cp ../lib/zui/Sources
--macro include('iron', true, null, ['../iron/Sources']) --macro include('iron', true, null, ['../iron/Sources'])
--macro include('aura', true, null, ['../lib/aura/Sources'])
--macro include('haxebullet', true, null, ['../lib/haxebullet/Sources']) --macro include('haxebullet', true, null, ['../lib/haxebullet/Sources'])
--macro include('haxerecast', true, null, ['../lib/haxerecast/Sources']) --macro include('haxerecast', true, null, ['../lib/haxerecast/Sources'])
--macro include('leenkx', true, ['leenkx.network'], ['../leenkx/Sources','../iron/Sources']) --macro include('leenkx', true, ['leenkx.network'], ['../leenkx/Sources','../iron/Sources'])

View File

@ -24,7 +24,7 @@ import textwrap
import threading import threading
import traceback import traceback
import typing import typing
from typing import Callable, Optional from typing import Callable, Optional, List
import webbrowser import webbrowser
import bpy import bpy
@ -33,6 +33,12 @@ from bpy.props import *
from bpy.types import Operator, AddonPreferences from bpy.types import Operator, AddonPreferences
if bpy.app.version < (2, 90, 0):
ListType = List
else:
ListType = list
class SDKSource(IntEnum): class SDKSource(IntEnum):
PREFS = 0 PREFS = 0
LOCAL = 1 LOCAL = 1
@ -73,9 +79,10 @@ def detect_sdk_path():
area = win.screen.areas[0] area = win.screen.areas[0]
area_type = area.type area_type = area.type
area.type = "INFO" area.type = "INFO"
with bpy.context.temp_override(window=win, screen=win.screen, area=area): if bpy.app.version >= (2, 92, 0):
bpy.ops.info.select_all(action='SELECT') with bpy.context.temp_override(window=win, screen=win.screen, area=area):
bpy.ops.info.report_copy() bpy.ops.info.select_all(action='SELECT')
bpy.ops.info.report_copy()
area.type = area_type area.type = area_type
clipboard = bpy.context.window_manager.clipboard clipboard = bpy.context.window_manager.clipboard
@ -85,6 +92,7 @@ def detect_sdk_path():
if match: if match:
addon_prefs.sdk_path = os.path.dirname(match[-1]) addon_prefs.sdk_path = os.path.dirname(match[-1])
def get_link_web_server(self): def get_link_web_server(self):
return self.get('link_web_server', 'http://localhost/') return self.get('link_web_server', 'http://localhost/')
@ -558,7 +566,7 @@ def remove_readonly(func, path, excinfo):
func(path) func(path)
def run_proc(cmd: list[str], done: Optional[Callable[[bool], None]] = None): def run_proc(cmd: ListType[str], done: Optional[Callable[[bool], None]] = None):
def fn(p, done): def fn(p, done):
p.wait() p.wait()
if done is not None: if done is not None:
@ -840,7 +848,13 @@ def update_leenkx_py(sdk_path: str, force_relink=False):
else: else:
raise err raise err
else: else:
lnx_module_file.unlink(missing_ok=True) if bpy.app.version < (2, 92, 0):
try:
lnx_module_file.unlink()
except FileNotFoundError:
pass
else:
lnx_module_file.unlink(missing_ok=True)
shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file) shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file)

View File

@ -97,9 +97,9 @@ vec4 traceCone(const sampler3D voxels, const sampler3D voxelsSDF, const vec3 ori
vec3 aniso_direction = -dir; vec3 aniso_direction = -dir;
vec3 face_offset = vec3( vec3 face_offset = vec3(
aniso_direction.x > 0.0 ? 0 : 1, aniso_direction.x > 0.0 ? 0.0 : 1.0,
aniso_direction.y > 0.0 ? 2 : 3, aniso_direction.y > 0.0 ? 2.0 : 3.0,
aniso_direction.z > 0.0 ? 4 : 5 aniso_direction.z > 0.0 ? 4.0 : 5.0
) / (6 + DIFFUSE_CONE_COUNT); ) / (6 + DIFFUSE_CONE_COUNT);
vec3 direction_weight = abs(dir); vec3 direction_weight = abs(dir);
@ -201,9 +201,9 @@ float traceConeAO(const sampler3D voxels, const vec3 origin, const vec3 n, const
vec3 aniso_direction = -dir; vec3 aniso_direction = -dir;
vec3 face_offset = vec3( vec3 face_offset = vec3(
aniso_direction.x > 0.0 ? 0 : 1, aniso_direction.x > 0.0 ? 0.0 : 1.0,
aniso_direction.y > 0.0 ? 2 : 3, aniso_direction.y > 0.0 ? 2.0 : 3.0,
aniso_direction.z > 0.0 ? 4 : 5 aniso_direction.z > 0.0 ? 4.0 : 5.0
) / (6 + DIFFUSE_CONE_COUNT); ) / (6 + DIFFUSE_CONE_COUNT);
vec3 direction_weight = abs(dir); vec3 direction_weight = abs(dir);
@ -272,9 +272,9 @@ float traceConeShadow(const sampler3D voxels, const sampler3D voxelsSDF, const v
vec3 aniso_direction = -dir; vec3 aniso_direction = -dir;
vec3 face_offset = vec3( vec3 face_offset = vec3(
aniso_direction.x > 0.0 ? 0 : 1, aniso_direction.x > 0.0 ? 0.0 : 1.0,
aniso_direction.y > 0.0 ? 2 : 3, aniso_direction.y > 0.0 ? 2.0 : 3.0,
aniso_direction.z > 0.0 ? 4 : 5 aniso_direction.z > 0.0 ? 4.0 : 5.0
) / (6 + DIFFUSE_CONE_COUNT); ) / (6 + DIFFUSE_CONE_COUNT);
vec3 direction_weight = abs(dir); vec3 direction_weight = abs(dir);
float coneCoefficient = 2.0 * tan(aperture * 0.5); float coneCoefficient = 2.0 * tan(aperture * 0.5);

View File

@ -54,6 +54,22 @@ class App {
if (Scene.active == null || !Scene.active.ready) return; if (Scene.active == null || !Scene.active.ready) return;
iron.system.Time.update(); iron.system.Time.update();
if (lastw == -1) {
lastw = App.w();
lasth = App.h();
}
if (lastw != App.w() || lasth != App.h()) {
if (onResize != null) onResize();
else {
if (Scene.active != null && Scene.active.camera != null) {
Scene.active.camera.buildProjection();
}
}
}
lastw = App.w();
lasth = App.h();
if (pauseUpdates) return; if (pauseUpdates) return;
#if lnx_debug #if lnx_debug
@ -98,22 +114,6 @@ class App {
for (cb in endFrameCallbacks) cb(); for (cb in endFrameCallbacks) cb();
updateTime = kha.Scheduler.realTime() - startTime; updateTime = kha.Scheduler.realTime() - startTime;
#end #end
// Rebuild projection on window resize
if (lastw == -1) {
lastw = App.w();
lasth = App.h();
}
if (lastw != App.w() || lasth != App.h()) {
if (onResize != null) onResize();
else {
if (Scene.active != null && Scene.active.camera != null) {
Scene.active.camera.buildProjection();
}
}
}
lastw = App.w();
lasth = App.h();
} }
static function render(frames: Array<kha.Framebuffer>) { static function render(frames: Array<kha.Framebuffer>) {

View File

@ -331,15 +331,18 @@ class RenderPath {
}); });
} }
public static function sortMeshesShader(meshes: Array<MeshObject>) { public static function sortMeshesIndex(meshes: Array<MeshObject>) {
meshes.sort(function(a, b): Int { meshes.sort(function(a, b): Int {
#if rp_depth_texture #if rp_depth_texture
var depthDiff = boolToInt(a.depthRead) - boolToInt(b.depthRead); var depthDiff = boolToInt(a.depthRead) - boolToInt(b.depthRead);
if (depthDiff != 0) return depthDiff; if (depthDiff != 0) return depthDiff;
#end #end
return a.materials[0].name >= b.materials[0].name ? 1 : -1; if (a.data.sortingIndex != b.data.sortingIndex) {
}); return a.data.sortingIndex > b.data.sortingIndex ? 1 : -1;
}
return a.data.name >= b.data.name ? 1 : -1; });
} }
public function drawMeshes(context: String) { public function drawMeshes(context: String) {
@ -399,7 +402,7 @@ class RenderPath {
#if lnx_batch #if lnx_batch
sortMeshesDistance(Scene.active.meshBatch.nonBatched); sortMeshesDistance(Scene.active.meshBatch.nonBatched);
#else #else
drawOrder == DrawOrder.Shader ? sortMeshesShader(meshes) : sortMeshesDistance(meshes); drawOrder == DrawOrder.Index ? sortMeshesIndex(meshes) : sortMeshesDistance(meshes);
#end #end
meshesSorted = true; meshesSorted = true;
} }
@ -518,12 +521,44 @@ class RenderPath {
return Reflect.field(kha.Shaders, handle + "_comp"); return Reflect.field(kha.Shaders, handle + "_comp");
} }
#if (kha_krom && lnx_vr) #if lnx_vr
public function drawStereo(drawMeshes: Int->Void) { public function drawStereo(drawMeshes: Void->Void) {
for (eye in 0...2) { var vr = kha.vr.VrInterface.instance;
Krom.vrBeginRender(eye); var appw = iron.App.w();
drawMeshes(eye); var apph = iron.App.h();
Krom.vrEndRender(eye); var halfw = Std.int(appw / 2);
var g = currentG;
if (vr != null && vr.IsPresenting()) {
// Left eye
Scene.active.camera.V.setFrom(Scene.active.camera.leftV);
Scene.active.camera.P.self = vr.GetProjectionMatrix(0);
g.viewport(0, 0, halfw, apph);
drawMeshes();
// Right eye
begin(g, additionalTargets);
Scene.active.camera.V.setFrom(Scene.active.camera.rightV);
Scene.active.camera.P.self = vr.GetProjectionMatrix(1);
g.viewport(halfw, 0, halfw, apph);
drawMeshes();
}
else { // Simulate
Scene.active.camera.buildProjection(halfw / apph);
// Left eye
g.viewport(0, 0, halfw, apph);
drawMeshes();
// Right eye
begin(g, additionalTargets);
Scene.active.camera.transform.move(Scene.active.camera.right(), 0.032);
Scene.active.camera.buildMatrix();
g.viewport(halfw, 0, halfw, apph);
drawMeshes();
Scene.active.camera.transform.move(Scene.active.camera.right(), -0.032);
Scene.active.camera.buildMatrix();
} }
} }
#end #end
@ -882,6 +917,6 @@ class CachedShaderContext {
@:enum abstract DrawOrder(Int) from Int { @:enum abstract DrawOrder(Int) from Int {
var Distance = 0; // Early-z var Distance = 0; // Early-z
var Shader = 1; // Less state changes var Index = 1; // Less state changes
// var Mix = 2; // Distance buckets sorted by shader // var Mix = 2; // Distance buckets sorted by shader
} }

View File

@ -783,6 +783,11 @@ class Scene {
if (o.tilesheet_ref != null) { if (o.tilesheet_ref != null) {
cast(object, MeshObject).setupTilesheet(sceneName, o.tilesheet_ref, o.tilesheet_action_ref); cast(object, MeshObject).setupTilesheet(sceneName, o.tilesheet_ref, o.tilesheet_action_ref);
} }
if (o.camera_list != null){
cast(object, MeshObject).cameraList = o.camera_list;
}
returnObject(object, o, done); returnObject(object, o, done);
}); });
} }
@ -882,8 +887,12 @@ class Scene {
var ptype: String = t.props[i * 3 + 1]; var ptype: String = t.props[i * 3 + 1];
var pval: Dynamic = t.props[i * 3 + 2]; var pval: Dynamic = t.props[i * 3 + 2];
if (StringTools.endsWith(ptype, "Object") && pval != "") { if (StringTools.endsWith(ptype, "Object") && pval != "" && pval != null) {
Reflect.setProperty(traitInst, pname, Scene.active.getChild(pval)); Reflect.setProperty(traitInst, pname, Scene.active.getChild(pval));
} else if (ptype == "TSceneFormat" && pval != "") {
Data.getSceneRaw(pval, function (r: TSceneFormat) {
Reflect.setProperty(traitInst, pname, r);
});
} }
else { else {
switch (ptype) { switch (ptype) {

View File

@ -9,6 +9,7 @@ import iron.data.SceneFormat;
class MeshData { class MeshData {
public var name: String; public var name: String;
public var sortingIndex: Int;
public var raw: TMeshData; public var raw: TMeshData;
public var format: TSceneFormat; public var format: TSceneFormat;
public var geom: Geometry; public var geom: Geometry;
@ -23,7 +24,8 @@ class MeshData {
public function new(raw: TMeshData, done: MeshData->Void) { public function new(raw: TMeshData, done: MeshData->Void) {
this.raw = raw; this.raw = raw;
this.name = raw.name; this.name = raw.name;
this.sortingIndex = raw.sorting_index;
if (raw.scale_pos != null) scalePos = raw.scale_pos; if (raw.scale_pos != null) scalePos = raw.scale_pos;
if (raw.scale_tex != null) scaleTex = raw.scale_tex; if (raw.scale_tex != null) scaleTex = raw.scale_tex;

View File

@ -49,6 +49,7 @@ typedef TMeshData = {
@:structInit class TMeshData { @:structInit class TMeshData {
#end #end
public var name: String; public var name: String;
public var sorting_index: Int;
public var vertex_arrays: Array<TVertexArray>; public var vertex_arrays: Array<TVertexArray>;
public var index_arrays: Array<TIndexArray>; public var index_arrays: Array<TIndexArray>;
@:optional public var dynamic_usage: Null<Bool>; @:optional public var dynamic_usage: Null<Bool>;
@ -222,6 +223,7 @@ typedef TShaderData = {
@:structInit class TShaderData { @:structInit class TShaderData {
#end #end
public var name: String; public var name: String;
public var next_pass: String;
public var contexts: Array<TShaderContext>; public var contexts: Array<TShaderContext>;
} }
@ -393,6 +395,7 @@ typedef TParticleData = {
public var name: String; public var name: String;
public var type: Int; // 0 - Emitter, Hair public var type: Int; // 0 - Emitter, Hair
public var auto_start: Bool; public var auto_start: Bool;
public var dynamic_emitter: Bool;
public var is_unique: Bool; public var is_unique: Bool;
public var loop: Bool; public var loop: Bool;
public var count: Int; public var count: Int;
@ -441,6 +444,7 @@ typedef TObj = {
@:optional public var traits: Array<TTrait>; @:optional public var traits: Array<TTrait>;
@:optional public var properties: Array<TProperty>; @:optional public var properties: Array<TProperty>;
@:optional public var vertex_groups: Array<TVertex_groups>; @:optional public var vertex_groups: Array<TVertex_groups>;
@:optional public var camera_list: Array<String>;
@:optional public var constraints: Array<TConstraint>; @:optional public var constraints: Array<TConstraint>;
@:optional public var dimensions: Float32Array; // Geometry objects @:optional public var dimensions: Float32Array; // Geometry objects
@:optional public var object_actions: Array<String>; @:optional public var object_actions: Array<String>;

View File

@ -22,6 +22,7 @@ using StringTools;
class ShaderData { class ShaderData {
public var name: String; public var name: String;
public var nextPass: String;
public var raw: TShaderData; public var raw: TShaderData;
public var contexts: Array<ShaderContext> = []; public var contexts: Array<ShaderContext> = [];
@ -33,6 +34,7 @@ class ShaderData {
public function new(raw: TShaderData, done: ShaderData->Void, overrideContext: TShaderOverride = null) { public function new(raw: TShaderData, done: ShaderData->Void, overrideContext: TShaderOverride = null) {
this.raw = raw; this.raw = raw;
this.name = raw.name; this.name = raw.name;
this.nextPass = raw.next_pass;
for (c in raw.contexts) contexts.push(null); for (c in raw.contexts) contexts.push(null);
var contextsLoaded = 0; var contextsLoaded = 0;

View File

@ -30,12 +30,22 @@ class CameraObject extends Object {
static var sphereCenter = new Vec4(); static var sphereCenter = new Vec4();
static var vcenter = new Vec4(); static var vcenter = new Vec4();
static var vup = new Vec4(); static var vup = new Vec4();
#if lnx_vr
var helpMat = Mat4.identity();
public var leftV = Mat4.identity();
public var rightV = Mat4.identity();
#end
public function new(data: CameraData) { public function new(data: CameraData) {
super(); super();
this.data = data; this.data = data;
#if lnx_vr
iron.system.VR.initButton();
#end
buildProjection(); buildProjection();
V = Mat4.identity(); V = Mat4.identity();
@ -117,6 +127,26 @@ class CameraObject extends Object {
V.getInverse(transform.world); V.getInverse(transform.world);
VP.multmats(P, V); VP.multmats(P, V);
#if lnx_vr
var vr = kha.vr.VrInterface.instance;
if (vr != null && vr.IsPresenting()) {
leftV.setFrom(V);
helpMat.self = vr.GetViewMatrix(0);
leftV.multmat(helpMat);
rightV.setFrom(V);
helpMat.self = vr.GetViewMatrix(1);
rightV.multmat(helpMat);
}
else {
leftV.setFrom(V);
}
VP.multmats(P, leftV);
#else
VP.multmats(P, V);
#end
if (data.raw.frustum_culling) { if (data.raw.frustum_culling) {
buildViewFrustum(VP, frustumPlanes); buildViewFrustum(VP, frustumPlanes);
} }

View File

@ -155,8 +155,13 @@ class LightObject extends Object {
} }
public function setCascade(camera: CameraObject, cascade: Int) { public function setCascade(camera: CameraObject, cascade: Int) {
m.setFrom(camera.V);
#if lnx_vr
m.setFrom(camera.leftV);
#else
m.setFrom(camera.V);
#end
#if lnx_csm #if lnx_csm
if (camSlicedP == null) { if (camSlicedP == null) {
camSlicedP = []; camSlicedP = [];

View File

@ -24,6 +24,7 @@ class MeshObject extends Object {
public var render_emitter = true; public var render_emitter = true;
#end #end
public var cameraDistance: Float; public var cameraDistance: Float;
public var cameraList: Array<String> = null;
public var screenSize = 0.0; public var screenSize = 0.0;
public var frustumCulling = true; public var frustumCulling = true;
public var activeTilesheet: Tilesheet = null; public var activeTilesheet: Tilesheet = null;
@ -235,6 +236,8 @@ class MeshObject extends Object {
if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return; if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return;
var meshContext = raw != null ? context == "mesh" : false; var meshContext = raw != null ? context == "mesh" : false;
if (cameraList != null && cameraList.indexOf(Scene.active.camera.name) < 0) return;
#if lnx_particles #if lnx_particles
if (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner if (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner
if (particleSystems != null && meshContext) { if (particleSystems != null && meshContext) {
@ -245,6 +248,7 @@ class MeshObject extends Object {
Scene.active.spawnObject(psys.data.raw.instance_object, null, function(o: Object) { Scene.active.spawnObject(psys.data.raw.instance_object, null, function(o: Object) {
if (o != null) { if (o != null) {
var c: MeshObject = cast o; var c: MeshObject = cast o;
c.cameraList = this.cameraList;
particleChildren.push(c); particleChildren.push(c);
c.particleOwner = this; c.particleOwner = this;
c.particleIndex = particleChildren.length - 1; c.particleIndex = particleChildren.length - 1;
@ -298,6 +302,10 @@ class MeshObject extends Object {
// Render mesh // Render mesh
var ldata = lod.data; var ldata = lod.data;
// Next pass rendering first (inverse order)
renderNextPass(g, context, bindParams, lod);
for (i in 0...ldata.geom.indexBuffers.length) { for (i in 0...ldata.geom.indexBuffers.length) {
var mi = ldata.geom.materialIndices[i]; var mi = ldata.geom.materialIndices[i];
@ -401,4 +409,85 @@ class MeshObject extends Object {
} }
} }
} }
function renderNextPass(g: Graphics, context: String, bindParams: Array<String>, lod: MeshObject) {
var ldata = lod.data;
for (i in 0...ldata.geom.indexBuffers.length) {
var mi = ldata.geom.materialIndices[i];
if (mi >= materials.length) continue;
var currentMaterial: MaterialData = materials[mi];
if (currentMaterial == null || currentMaterial.shader == null) continue;
var nextPassName: String = currentMaterial.shader.nextPass;
if (nextPassName == null || nextPassName == "") continue;
var nextMaterial: MaterialData = null;
for (mat in materials) {
// First try exact match
if (mat.name == nextPassName) {
nextMaterial = mat;
break;
}
// If no exact match, try to match base name for linked materials
if (mat.name.indexOf("_") > 0 && mat.name.substr(mat.name.length - 6) == ".blend") {
var baseName = mat.name.substring(0, mat.name.indexOf("_"));
if (baseName == nextPassName) {
nextMaterial = mat;
break;
}
}
}
if (nextMaterial == null) continue;
var nextMaterialContext: MaterialContext = null;
var nextShaderContext: ShaderContext = null;
for (j in 0...nextMaterial.raw.contexts.length) {
if (nextMaterial.raw.contexts[j].name.substr(0, context.length) == context) {
nextMaterialContext = nextMaterial.contexts[j];
nextShaderContext = nextMaterial.shader.getContext(context);
break;
}
}
if (nextShaderContext == null) continue;
if (skipContext(context, nextMaterial)) continue;
var elems = nextShaderContext.raw.vertex_elements;
// Uniforms
if (nextShaderContext.pipeState != lastPipeline) {
g.setPipeline(nextShaderContext.pipeState);
lastPipeline = nextShaderContext.pipeState;
}
Uniforms.setContextConstants(g, nextShaderContext, bindParams);
Uniforms.setObjectConstants(g, nextShaderContext, this);
Uniforms.setMaterialConstants(g, nextShaderContext, nextMaterialContext);
// VB / IB
#if lnx_deinterleaved
g.setVertexBuffers(ldata.geom.get(elems));
#else
if (ldata.geom.instancedVB != null) {
g.setVertexBuffers([ldata.geom.get(elems), ldata.geom.instancedVB]);
}
else {
g.setVertexBuffer(ldata.geom.get(elems));
}
#end
g.setIndexBuffer(ldata.geom.indexBuffers[i]);
// Draw next pass for this specific geometry section
if (ldata.geom.instanced) {
g.drawIndexedVerticesInstanced(ldata.geom.instanceCount, ldata.geom.start, ldata.geom.count);
}
else {
g.drawIndexedVertices(ldata.geom.start, ldata.geom.count);
}
}
}
} }

View File

@ -24,6 +24,9 @@ class ObjectAnimation extends Animation {
public var transformMap: Map<String, FastFloat>; public var transformMap: Map<String, FastFloat>;
var defaultSampler: ActionSampler = null;
static inline var DEFAULT_SAMPLER_ID = "__object_default_action__";
public static var trackNames: Array<String> = [ "xloc", "yloc", "zloc", public static var trackNames: Array<String> = [ "xloc", "yloc", "zloc",
"xrot", "yrot", "zrot", "xrot", "yrot", "zrot",
"qwrot", "qxrot", "qyrot", "qzrot", "qwrot", "qxrot", "qyrot", "qzrot",
@ -39,7 +42,6 @@ class ObjectAnimation extends Animation {
isSkinned = false; isSkinned = false;
super(); super();
} }
function getAction(action: String): TObj { function getAction(action: String): TObj {
for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0]; for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0];
return null; return null;
@ -47,10 +49,29 @@ class ObjectAnimation extends Animation {
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) {
super.play(action, onComplete, blendTime, speed, loop); super.play(action, onComplete, blendTime, speed, loop);
if (this.action == "" && oactions[0] != null) this.action = oactions[0].objects[0].name; if (this.action == "" && oactions != null && oactions[0] != null){
this.action = oactions[0].objects[0].name;
}
oaction = getAction(this.action); oaction = getAction(this.action);
if (oaction != null) { if (oaction != null) {
isSampled = oaction.sampled != null && oaction.sampled; isSampled = oaction.sampled != null && oaction.sampled;
if (defaultSampler != null) {
deRegisterAction(DEFAULT_SAMPLER_ID);
}
var callbacks = onComplete != null ? [onComplete] : null;
defaultSampler = new ActionSampler(this.action, speed, loop, false, callbacks);
registerAction(DEFAULT_SAMPLER_ID, defaultSampler);
if (paused) defaultSampler.paused = true;
updateAnimation = function(map: Map<String, FastFloat>) {
sampleAction(defaultSampler, map);
};
}
else {
if (defaultSampler != null) {
deRegisterAction(DEFAULT_SAMPLER_ID);
defaultSampler = null;
}
updateAnimation = null;
} }
} }
@ -61,12 +82,13 @@ class ObjectAnimation extends Animation {
Animation.beginProfile(); Animation.beginProfile();
#end #end
if(transformMap == null) transformMap = new Map(); if (transformMap == null) transformMap = new Map();
transformMap = initTransformMap(); transformMap = initTransformMap();
super.update(delta); super.update(delta);
if (defaultSampler != null) defaultSampler.paused = paused;
if (paused) return; if (paused) return;
if(updateAnimation == null) return; if (updateAnimation == null) return;
if (!isSkinned) updateObjectAnimation(); if (!isSkinned) updateObjectAnimation();
#if lnx_debug #if lnx_debug

View File

@ -8,6 +8,8 @@ import kha.arrays.Float32Array;
import iron.data.Data; import iron.data.Data;
import iron.data.ParticleData; import iron.data.ParticleData;
import iron.data.SceneFormat; import iron.data.SceneFormat;
import iron.data.Geometry;
import iron.data.MeshData;
import iron.system.Time; import iron.system.Time;
import iron.math.Mat4; import iron.math.Mat4;
import iron.math.Quat; import iron.math.Quat;
@ -17,6 +19,7 @@ import iron.math.Vec4;
class ParticleSystem { class ParticleSystem {
public var data: ParticleData; public var data: ParticleData;
public var speed = 1.0; public var speed = 1.0;
public var dynamicEmitter: Bool = true;
var currentSpeed = 0.0; var currentSpeed = 0.0;
var particles: Array<Particle>; var particles: Array<Particle>;
var ready: Bool; var ready: Bool;
@ -52,6 +55,12 @@ class ParticleSystem {
var random = 0.0; var random = 0.0;
var tmpV4 = new Vec4();
var instancedData: Float32Array = null;
var lastSpawnedCount: Int = 0;
var hasUniqueGeom: Bool = false;
public function new(sceneName: String, pref: TParticleReference) { public function new(sceneName: String, pref: TParticleReference) {
seed = pref.seed; seed = pref.seed;
currentSpeed = speed; currentSpeed = speed;
@ -62,6 +71,12 @@ class ParticleSystem {
Data.getParticle(sceneName, pref.particle, function(b: ParticleData) { Data.getParticle(sceneName, pref.particle, function(b: ParticleData) {
data = b; data = b;
r = data.raw; r = data.raw;
var dyn: Null<Bool> = r.dynamic_emitter;
var dynValue: Bool = true;
if (dyn != null) {
dynValue = dyn;
}
dynamicEmitter = dynValue;
if (Scene.active.raw.gravity != null) { if (Scene.active.raw.gravity != null) {
gx = Scene.active.raw.gravity[0] * r.weight_gravity; gx = Scene.active.raw.gravity[0] * r.weight_gravity;
gy = Scene.active.raw.gravity[1] * r.weight_gravity; gy = Scene.active.raw.gravity[1] * r.weight_gravity;
@ -98,6 +113,8 @@ class ParticleSystem {
lap = 0; lap = 0;
lapTime = 0; lapTime = 0;
speed = currentSpeed; speed = currentSpeed;
lastSpawnedCount = 0;
instancedData = null;
} }
public function pause() { public function pause() {
@ -130,8 +147,13 @@ class ParticleSystem {
// Copy owner world transform but discard scale // Copy owner world transform but discard scale
owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl); owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl);
object.transform.loc = ownerLoc; if (dynamicEmitter) {
object.transform.rot = ownerRot; object.transform.loc.x = 0; object.transform.loc.y = 0; object.transform.loc.z = 0;
object.transform.rot = new Quat();
} else {
object.transform.loc = ownerLoc;
object.transform.rot = ownerRot;
}
// Set particle size per particle system // Set particle size per particle system
object.transform.scale = new Vec4(r.particle_size, r.particle_size, r.particle_size, 1); object.transform.scale = new Vec4(r.particle_size, r.particle_size, r.particle_size, 1);
@ -158,13 +180,18 @@ class ParticleSystem {
if (lap > prevLap && !r.loop) { if (lap > prevLap && !r.loop) {
end(); end();
} }
if (lap > prevLap && r.loop) {
lastSpawnedCount = 0;
}
updateGpu(object, owner); updateGpu(object, owner);
} }
public function getData(): Mat4 { public function getData(): Mat4 {
var hair = r.type == 1; var hair = r.type == 1;
m._00 = animtime; // Store loop flag in the sign: positive -> loop, negative -> no loop
m._00 = r.loop ? animtime : -animtime;
m._01 = hair ? 1 / particles.length : spawnRate; m._01 = hair ? 1 / particles.length : spawnRate;
m._02 = hair ? 1 : lifetime; m._02 = hair ? 1 : lifetime;
m._03 = particles.length; m._03 = particles.length;
@ -187,17 +214,26 @@ class ParticleSystem {
return r.size_random; return r.size_random;
} }
public function getRandom(): FastFloat { public inline function getRandom(): FastFloat {
return random; return random;
} }
public function getSize(): FastFloat { public inline function getSize(): FastFloat {
return r.particle_size; return r.particle_size;
} }
function updateGpu(object: MeshObject, owner: MeshObject) { function updateGpu(object: MeshObject, owner: MeshObject) {
if (!object.data.geom.instanced) setupGeomGpu(object, owner); if (dynamicEmitter) {
// GPU particles transform is attached to owner object if (!hasUniqueGeom) ensureUniqueGeom(object);
var needSetup = instancedData == null || object.data.geom.instancedVB == null;
if (needSetup) setupGeomGpuDynamic(object, owner);
updateSpawnedInstances(object, owner);
}
else {
if (!hasUniqueGeom) ensureUniqueGeom(object);
if (!object.data.geom.instanced) setupGeomGpu(object, owner);
}
// GPU particles transform is attached to owner object in static mode
} }
function setupGeomGpu(object: MeshObject, owner: MeshObject) { function setupGeomGpu(object: MeshObject, owner: MeshObject) {
@ -258,18 +294,134 @@ class ParticleSystem {
object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage); object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage);
} }
function fhash(n: Int): Float { // allocate instanced VB once for this object
var s = n + 1.0; function setupGeomGpuDynamic(object: MeshObject, owner: MeshObject) {
s *= 9301.0 % s; if (instancedData == null) instancedData = new Float32Array(particles.length * 3);
s = (s * 9301.0 + 49297.0) % 233280.0; lastSpawnedCount = 0;
return s / 233280.0; // Create instanced VB once if missing (seed with our instancedData)
if (object.data.geom.instancedVB == null) {
object.data.geom.setupInstanced(instancedData, 1, Usage.DynamicUsage);
}
} }
function ensureUniqueGeom(object: MeshObject) {
if (hasUniqueGeom) return;
var newData: MeshData = null;
new MeshData(object.data.raw, function(dat: MeshData) {
dat.scalePos = object.data.scalePos;
dat.scaleTex = object.data.scaleTex;
dat.format = object.data.format;
newData = dat;
});
if (newData != null) object.setData(newData);
hasUniqueGeom = true;
}
function updateSpawnedInstances(object: MeshObject, owner: MeshObject) {
if (instancedData == null) return;
var targetCount = count;
if (targetCount > particles.length) targetCount = particles.length;
if (targetCount <= lastSpawnedCount) return;
var normFactor = 1 / 32767;
var scalePosOwner = owner.data.scalePos;
var scalePosParticle = object.data.scalePos;
var particleSize = r.particle_size;
var base = 1.0 / (particleSize * scalePosParticle);
switch (r.emit_from) {
case 0: // Vert
var pa = owner.data.geom.positions;
var osx = owner.transform.scale.x;
var osy = owner.transform.scale.y;
var osz = owner.transform.scale.z;
var pCount = Std.int(pa.values.length / pa.size);
for (idx in lastSpawnedCount...targetCount) {
var j = Std.int(fhash(idx) * pCount);
var lx = pa.values[j * pa.size ] * normFactor * scalePosOwner * osx;
var ly = pa.values[j * pa.size + 1] * normFactor * scalePosOwner * osy;
var lz = pa.values[j * pa.size + 2] * normFactor * scalePosOwner * osz;
tmpV4.x = lx; tmpV4.y = ly; tmpV4.z = lz; tmpV4.w = 1;
tmpV4.applyQuat(ownerRot);
var o = idx * 3;
instancedData.set(o , (tmpV4.x + ownerLoc.x) * base);
instancedData.set(o + 1, (tmpV4.y + ownerLoc.y) * base);
instancedData.set(o + 2, (tmpV4.z + ownerLoc.z) * base);
}
case 1: // Face
var positions = owner.data.geom.positions.values;
var osx1 = owner.transform.scale.x;
var osy1 = owner.transform.scale.y;
var osz1 = owner.transform.scale.z;
for (idx in lastSpawnedCount...targetCount) {
var ia = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)];
var faceIndex = Std.random(Std.int(ia.length / 3));
var i0 = ia[faceIndex * 3 + 0];
var i1 = ia[faceIndex * 3 + 1];
var i2 = ia[faceIndex * 3 + 2];
var v0x = positions[i0 * 4 ], v0y = positions[i0 * 4 + 1], v0z = positions[i0 * 4 + 2];
var v1x = positions[i1 * 4 ], v1y = positions[i1 * 4 + 1], v1z = positions[i1 * 4 + 2];
var v2x = positions[i2 * 4 ], v2y = positions[i2 * 4 + 1], v2z = positions[i2 * 4 + 2];
var rx = Math.random(); var ry = Math.random(); if (rx + ry > 1) { rx = 1 - rx; ry = 1 - ry; }
var pxs = v0x + rx * (v1x - v0x) + ry * (v2x - v0x);
var pys = v0y + rx * (v1y - v0y) + ry * (v2y - v0y);
var pzs = v0z + rx * (v1z - v0z) + ry * (v2z - v0z);
var px = pxs * normFactor * scalePosOwner * osx1;
var py = pys * normFactor * scalePosOwner * osy1;
var pz = pzs * normFactor * scalePosOwner * osz1;
tmpV4.x = px; tmpV4.y = py; tmpV4.z = pz; tmpV4.w = 1;
tmpV4.applyQuat(ownerRot);
var o1 = idx * 3;
instancedData.set(o1 , (tmpV4.x + ownerLoc.x) * base);
instancedData.set(o1 + 1, (tmpV4.y + ownerLoc.y) * base);
instancedData.set(o1 + 2, (tmpV4.z + ownerLoc.z) * base);
}
case 2: // Volume
var dim = object.transform.dim;
for (idx in lastSpawnedCount...targetCount) {
tmpV4.x = (Math.random() * 2.0 - 1.0) * (dim.x * 0.5);
tmpV4.y = (Math.random() * 2.0 - 1.0) * (dim.y * 0.5);
tmpV4.z = (Math.random() * 2.0 - 1.0) * (dim.z * 0.5);
tmpV4.w = 1;
tmpV4.applyQuat(ownerRot);
var o2 = idx * 3;
instancedData.set(o2 , (tmpV4.x + ownerLoc.x) * base);
instancedData.set(o2 + 1, (tmpV4.y + ownerLoc.y) * base);
instancedData.set(o2 + 2, (tmpV4.z + ownerLoc.z) * base);
}
}
// Upload full active range [0..targetCount) to this object's instanced VB
var geom = object.data.geom;
if (geom.instancedVB == null) {
geom.setupInstanced(instancedData, 1, Usage.DynamicUsage);
}
var vb = geom.instancedVB.lock();
var totalFloats = targetCount * 3; // xyz per instance
var i = 0;
while (i < totalFloats) {
vb.setFloat32(i * 4, instancedData[i]);
i++;
}
geom.instancedVB.unlock();
geom.instanceCount = targetCount;
lastSpawnedCount = targetCount;
}
inline function fhash(n: Int): Float {
var s = n + 1.0;
s *= 9301.0 % s;
s = (s * 9301.0 + 49297.0) % 233280.0;
return s / 233280.0;
}
public function remove() {} public function remove() {}
/** /**
Generates a random point in the triangle with vertex positions abc. Generates a random point in the triangle with vertex positions abc.
Please note that the given position vectors are changed in-place by this Please note that the given position vectors are changed in-place by this
function and can be considered garbage afterwards, so make sure to clone function and can be considered garbage afterwards, so make sure to clone
them first if needed. them first if needed.

View File

@ -14,7 +14,7 @@ class Time {
return 1 / frequency; return 1 / frequency;
} }
static var _fixedStep: Null<Float>; static var _fixedStep: Null<Float> = 1/60;
public static var fixedStep(get, never): Float; public static var fixedStep(get, never): Float;
static function get_fixedStep(): Float { static function get_fixedStep(): Float {
return _fixedStep; return _fixedStep;
@ -39,11 +39,11 @@ class Time {
} }
public static inline function time(): Float { public static inline function time(): Float {
return kha.Scheduler.time(); return kha.Scheduler.time() * scale;
} }
public static inline function realTime(): Float { public static inline function realTime(): Float {
return kha.Scheduler.realTime(); return kha.Scheduler.realTime() * scale;
} }
public static function update() { public static function update() {

View File

@ -94,34 +94,34 @@ class Tween {
// Way too much Reflect trickery.. // Way too much Reflect trickery..
var ps = Reflect.fields(a.props); var ps = Reflect.fields(a.props);
for (i in 0...ps.length) { for (j in 0...ps.length) {
var p = ps[i]; var p = ps[j];
var k = a._time / a.duration; var k = a._time / a.duration;
if (k > 1) k = 1; if (k > 1) k = 1;
if (a._comps[i] == 1) { if (a._comps[j] == 1) {
var fromVal: Float = a._x[i]; var fromVal: Float = a._x[j];
var toVal: Float = Reflect.getProperty(a.props, p); var toVal: Float = Reflect.getProperty(a.props, p);
var val: Float = fromVal + (toVal - fromVal) * eases[a.ease](k); var val: Float = fromVal + (toVal - fromVal) * eases[a.ease](k);
Reflect.setProperty(a.target, p, val); Reflect.setProperty(a.target, p, val);
} }
else { // _comps[i] == 4 else { // _comps[j] == 4
var obj = Reflect.getProperty(a.props, p); var obj = Reflect.getProperty(a.props, p);
var toX: Float = Reflect.getProperty(obj, "x"); var toX: Float = Reflect.getProperty(obj, "x");
var toY: Float = Reflect.getProperty(obj, "y"); var toY: Float = Reflect.getProperty(obj, "y");
var toZ: Float = Reflect.getProperty(obj, "z"); var toZ: Float = Reflect.getProperty(obj, "z");
var toW: Float = Reflect.getProperty(obj, "w"); var toW: Float = Reflect.getProperty(obj, "w");
if (a._normalize[i]) { if (a._normalize[j]) {
var qdot = (a._x[i] * toX) + (a._y[i] * toY) + (a._z[i] * toZ) + (a._w[i] * toW); var qdot = (a._x[j] * toX) + (a._y[j] * toY) + (a._z[j] * toZ) + (a._w[j] * toW);
if (qdot < 0.0) { if (qdot < 0.0) {
toX = -toX; toY = -toY; toZ = -toZ; toW = -toW; toX = -toX; toY = -toY; toZ = -toZ; toW = -toW;
} }
} }
var x: Float = a._x[i] + (toX - a._x[i]) * eases[a.ease](k); var x: Float = a._x[j] + (toX - a._x[j]) * eases[a.ease](k);
var y: Float = a._y[i] + (toY - a._y[i]) * eases[a.ease](k); var y: Float = a._y[j] + (toY - a._y[j]) * eases[a.ease](k);
var z: Float = a._z[i] + (toZ - a._z[i]) * eases[a.ease](k); var z: Float = a._z[j] + (toZ - a._z[j]) * eases[a.ease](k);
var w: Float = a._w[i] + (toW - a._w[i]) * eases[a.ease](k); var w: Float = a._w[j] + (toW - a._w[j]) * eases[a.ease](k);
if (a._normalize[i]) { if (a._normalize[j]) {
var l = Math.sqrt(x * x + y * y + z * z + w * w); var l = Math.sqrt(x * x + y * y + z * z + w * w);
if (l > 0.0) { if (l > 0.0) {
l = 1.0 / l; l = 1.0 / l;

View File

@ -0,0 +1,52 @@
package iron.system;
import iron.math.Mat4;
#if lnx_vr
class VR {
static var undistortionMatrix: Mat4 = null;
public function new() {}
public static function getUndistortionMatrix(): Mat4 {
if (undistortionMatrix == null) {
undistortionMatrix = Mat4.identity();
}
return undistortionMatrix;
}
public static function getMaxRadiusSq(): Float {
return 0.0;
}
public static function initButton() {
function vrDownListener(index: Int, x: Float, y: Float) {
var vr = kha.vr.VrInterface.instance;
if (vr == null || !vr.IsVrEnabled() || vr.IsPresenting()) return;
var w: Float = iron.App.w();
var h: Float = iron.App.h();
if (x < w - 150 || y < h - 150) return;
vr.onVRRequestPresent();
}
function vrRender2D(g: kha.graphics2.Graphics) {
var vr = kha.vr.VrInterface.instance;
if (vr == null || !vr.IsVrEnabled() || vr.IsPresenting()) return;
var w: Float = iron.App.w();
var h: Float = iron.App.h();
g.color = 0xffff0000;
g.fillRect(w - 150, h - 150, 140, 140);
}
kha.input.Mouse.get().notify(vrDownListener, null, null, null);
iron.App.notifyOnRender2D(vrRender2D);
var vr = kha.vr.VrInterface.instance; // Straight to VR (Oculus Carmel)
if (vr != null && vr.IsVrEnabled()) {
vr.onVRRequestPresent();
}
}
}
#end

View File

@ -0,0 +1,48 @@
package leenkx.logicnode;
import iron.object.Object;
#if lnx_physics
import leenkx.trait.physics.PhysicsCache;
import leenkx.trait.physics.RigidBody;
#end
class AnyContactNode 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();
if (object1 == null) object1 = tree.object;
if (object1 == null) return;
var contact = false;
#if lnx_physics
var rb1 = PhysicsCache.getCachedRigidBody(object1);
if (rb1 != null) {
var rbs = PhysicsCache.getCachedContacts(rb1);
contact = (rbs != null && rbs.length > 0);
}
#end
var shouldTrigger = false;
switch (property0) {
case "begin":
shouldTrigger = contact && !lastContact;
case "overlap":
shouldTrigger = contact;
case "end":
shouldTrigger = !contact && lastContact;
}
lastContact = contact;
if (shouldTrigger) runOutput(0);
}
}

View File

@ -62,7 +62,7 @@ class DrawStringNode extends LogicNode {
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
return from == 1 ? RenderToTexture.g.font.height(RenderToTexture.g.fontSize) : RenderToTexture.g.font.width(RenderToTexture.g.fontSize, string); return from == 1 ? RenderToTexture.g.font.width(RenderToTexture.g.fontSize, string) : RenderToTexture.g.font.height(RenderToTexture.g.fontSize);
} }
} }

View File

@ -0,0 +1,17 @@
package leenkx.logicnode;
import aura.Aura;
import aura.Types;
class GetAudioPositionNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var audio = inputs[0].get();
if (audio == null || audio.channel == null) return 0.0;
return audio.channel.floatPosition / audio.channel.sampleRate;
}
}

View File

@ -0,0 +1,19 @@
package leenkx.logicnode;
import iron.object.MeshObject;
import iron.object.CameraObject;
class GetCameraRenderFilterNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var mo: MeshObject = cast inputs[0].get();
if (mo == null) return null;
return mo.cameraList;
}
}

View File

@ -0,0 +1,33 @@
package leenkx.logicnode;
#if lnx_audio
import iron.object.SpeakerObject;
import kha.audio1.AudioChannel;
#end
class GetPositionSpeakerNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
#if lnx_audio
var object: SpeakerObject = cast(inputs[0].get(), SpeakerObject);
if (object == null || object.sound == null) return 0.0;
if (object.channels.length == 0) return 0.0;
var channel = object.channels[0];
var position = 0.0;
if (channel != null) {
position = @:privateAccess channel.get_position();
}
return position;
#else
return 0.0;
#end
}
}

View File

@ -3,7 +3,7 @@ package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
import iron.math.Vec4; import iron.math.Vec4;
class GetWorldNode extends LogicNode { class GetWorldOrientationNode extends LogicNode {
public var property0: String; public var property0: String;

View File

@ -1,7 +1,10 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
#if lnx_physics
import leenkx.trait.physics.PhysicsCache;
import leenkx.trait.physics.RigidBody; import leenkx.trait.physics.RigidBody;
#end
class HasContactNode extends LogicNode { class HasContactNode extends LogicNode {
@ -15,12 +18,15 @@ class HasContactNode extends LogicNode {
if (object1 == null || object2 == null) return false; if (object1 == null || object2 == null) return false;
#if lnx_physics #if lnx_physics
var physics = leenkx.trait.physics.PhysicsWorld.active; var rb1 = PhysicsCache.getCachedRigidBody(object1);
var rb2 = object2.getTrait(RigidBody); var rb2 = PhysicsCache.getCachedRigidBody(object2);
var rbs = physics.getContacts(object1.getTrait(RigidBody));
if (rbs != null) for (rb in rbs) if (rb == rb2) return true; if (rb1 != null && rb2 != null) {
#end var rbs = PhysicsCache.getCachedContacts(rb1);
return PhysicsCache.hasContactWith(rbs, rb2);
}
#end
return false; return false;
} }
} }

View File

@ -0,0 +1,233 @@
package leenkx.logicnode;
import iron.math.Vec4;
import iron.system.Input;
import iron.object.Object;
import kha.System;
import kha.FastFloat;
/**
* MouseLookNode - FPS-style mouse look camera controller
*
* This node provides smooth, resolution-independent mouse look functionality for
* first-person perspective controls. It supports separate body and head objects,
* allowing for realistic FPS camera movement where the body rotates horizontally
* and the head/camera rotates vertically.
*
* Key Features:
* - Resolution-adaptive scaling for consistent feel across different screen sizes
* - Configurable axis orientations (X, Y, Z as front)
* - Optional mouse cursor locking and hiding
* - Invertible X/Y axes
* - Rotation capping/limiting for both horizontal and vertical movement
* - Smoothing support for smoother camera movement
* - Physics integration with automatic rigid body synchronization
* - Support for both local and world space head rotation
*/
class MouseLookNode extends LogicNode {
// Configuration properties (set from Blender node interface)
public var property0: String; // Front axis: "X", "Y", or "Z"
public var property1: Bool; // Hide Locked: auto-lock mouse cursor
public var property2: Bool; // Invert X: invert horizontal mouse movement
public var property3: Bool; // Invert Y: invert vertical mouse movement
public var property4: Bool; // Cap Left/Right: limit horizontal rotation
public var property5: Bool; // Cap Up/Down: limit vertical rotation
public var property6: Bool; // Head Local Space: use local space for head rotation
// Smoothing state variables - maintain previous frame values for interpolation
var smoothX: Float = 0.0; // Smoothed horizontal mouse delta
var smoothY: Float = 0.0; // Smoothed vertical mouse delta
// Rotation limits (in radians)
var maxHorizontal: Float = Math.PI; // Maximum horizontal rotation (180 degrees)
var maxVertical: Float = Math.PI / 2; // Maximum vertical rotation (90 degrees)
// Current rotation tracking for capping calculations
var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation
var currentVertical: Float = 0.0; // Accumulated vertical rotation
// Resolution scaling reference - base resolution for consistent sensitivity
var baseResolutionWidth: Float = 1920.0;
// Sensitivity scaling constants
static inline var BASE_SCALE: Float = 1500.0; // Base sensitivity scale factor
static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; // Degrees to radians conversion with sensitivity scaling
public function new(tree: LogicTree) {
super(tree);
}
/**
* Main execution function called every frame when the node is active
*
* Input connections:
* [0] - Action trigger (not used in current implementation)
* [1] - Body Object: the main object that rotates horizontally
* [2] - Head Object: optional object that rotates vertically (typically camera)
* [3] - Sensitivity: mouse sensitivity multiplier
* [4] - Smoothing: movement smoothing factor (0.0 = no smoothing, 0.99 = maximum smoothing)
*/
override function run(from: Int) {
// Get input values from connected nodes
var bodyObject: Object = inputs[1].get();
var headObject: Object = inputs[2].get();
var sensitivity: FastFloat = inputs[3].get();
var smoothing: FastFloat = inputs[4].get();
// Early exit if no body object is provided
if (bodyObject == null) {
runOutput(0);
return;
}
// Get mouse input state
var mouse = Input.getMouse();
// Handle automatic mouse cursor locking for FPS controls
if (property1) {
if (mouse.started() && !mouse.locked) {
mouse.lock(); // Center and hide cursor, enable unlimited movement
}
}
// Only process mouse look when cursor is locked or mouse button is held
// This prevents unwanted camera movement when UI elements are being used
if (!mouse.locked && !mouse.down()) {
runOutput(0);
return;
}
// Get raw mouse movement delta (pixels moved since last frame)
var deltaX: Float = mouse.movementX;
var deltaY: Float = mouse.movementY;
// Apply axis inversion if configured
if (property2) deltaX = -deltaX; // Invert horizontal movement
if (property3) deltaY = -deltaY; // Invert vertical movement
// Calculate resolution-adaptive scaling to maintain consistent sensitivity
// across different screen resolutions. Higher resolutions will have proportionally
// higher scaling to compensate for increased pixel density.
var resolutionMultiplier: Float = System.windowWidth() / baseResolutionWidth;
// Apply movement smoothing if enabled
// This creates a weighted average between current and previous movement values
// to reduce jittery camera movement, especially useful for low framerates
if (smoothing > 0.0) {
var smoothingFactor: Float = Math.min(smoothing, 0.99); // Cap smoothing to prevent complete freeze
smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor);
smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor);
deltaX = smoothX;
deltaY = smoothY;
}
// Define rotation axes based on the configured front axis
// These determine which 3D axes are used for horizontal and vertical rotation
var horizontalAxis = new Vec4(); // Axis for left/right body rotation
var verticalAxis = new Vec4(); // Axis for up/down head rotation
switch (property0) {
case "X": // X-axis forward (e.g., for side-scrolling or specific orientations)
horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(0, 1, 0); // Y-axis for vertical rotation
case "Y": // Y-axis forward (most common for 3D games)
#if lnx_yaxisup
// Y-up coordinate system (Blender default)
horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
#else
// Z-up coordinate system
horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
#end
case "Z": // Z-axis forward (top-down or specific orientations)
horizontalAxis.set(0, 1, 0); // Y-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
}
// Calculate final sensitivity scaling combining base scale and resolution adaptation
var finalScale: Float = BASE_SCALE * resolutionMultiplier;
// Apply user-defined sensitivity multiplier
deltaX *= sensitivity;
deltaY *= sensitivity;
// Convert pixel movement to rotation angles (radians)
// Negative values ensure natural movement direction (moving mouse right rotates right)
var horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR;
var verticalRotation: Float = (-deltaY / finalScale) * RADIAN_SCALING_FACTOR;
// Apply horizontal rotation capping if enabled
// This prevents the character from rotating beyond specified limits
if (property4) {
currentHorizontal += horizontalRotation;
// Clamp rotation to maximum horizontal range and adjust current frame rotation
if (currentHorizontal > maxHorizontal) {
horizontalRotation -= (currentHorizontal - maxHorizontal);
currentHorizontal = maxHorizontal;
} else if (currentHorizontal < -maxHorizontal) {
horizontalRotation -= (currentHorizontal + maxHorizontal);
currentHorizontal = -maxHorizontal;
}
}
// Apply vertical rotation capping if enabled
// This prevents looking too far up or down (like human neck limitations)
if (property5) {
currentVertical += verticalRotation;
// Clamp rotation to maximum vertical range and adjust current frame rotation
if (currentVertical > maxVertical) {
verticalRotation -= (currentVertical - maxVertical);
currentVertical = maxVertical;
} else if (currentVertical < -maxVertical) {
verticalRotation -= (currentVertical + maxVertical);
currentVertical = -maxVertical;
}
}
// Apply horizontal rotation to body object (character turning left/right)
if (horizontalRotation != 0.0) {
bodyObject.transform.rotate(horizontalAxis, horizontalRotation);
// Synchronize physics rigid body if present
// This ensures physics simulation stays in sync with visual transform
#if lnx_physics
var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody);
if (rigidBody != null) rigidBody.syncTransform();
#end
}
// Apply vertical rotation to head object (camera looking up/down)
if (headObject != null && verticalRotation != 0.0) {
if (property6) {
// Local space rotation - recommended when head is a child of body
// This prevents gimbal lock and rotation inheritance issues
headObject.transform.rotate(verticalAxis, verticalRotation);
} else {
// World space rotation - uses head object's current right vector
// More accurate for independent head objects but can cause issues with parenting
var headVerticalAxis = headObject.transform.world.right();
headObject.transform.rotate(headVerticalAxis, verticalRotation);
}
// Synchronize head physics rigid body if present
#if lnx_physics
var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody);
if (headRigidBody != null) headRigidBody.syncTransform();
#end
} else if (headObject == null && verticalRotation != 0.0) {
// Fallback: if no separate head object, apply vertical rotation to body
// This creates a simpler single-object camera control
bodyObject.transform.rotate(verticalAxis, verticalRotation);
// Synchronize body physics rigid body
#if lnx_physics
var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody);
if (rigidBody != null) rigidBody.syncTransform();
#end
}
// Continue to next connected node in the logic tree
runOutput(0);
}
}

View File

@ -1,7 +1,11 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
#if lnx_physics
import leenkx.trait.physics.PhysicsCache;
import leenkx.trait.physics.RigidBody; import leenkx.trait.physics.RigidBody;
#end
class OnContactNode extends LogicNode { class OnContactNode extends LogicNode {
@ -23,22 +27,15 @@ class OnContactNode extends LogicNode {
var contact = false; var contact = false;
#if lnx_physics #if lnx_physics
var physics = leenkx.trait.physics.PhysicsWorld.active; var rb1 = PhysicsCache.getCachedRigidBody(object1);
var rb1 = object1.getTrait(RigidBody); var rb2 = PhysicsCache.getCachedRigidBody(object2);
if (rb1 != null) {
var rbs = physics.getContacts(rb1); if (rb1 != null && rb2 != null) {
if (rbs != null) { var rbs = PhysicsCache.getCachedContacts(rb1);
var rb2 = object2.getTrait(RigidBody); contact = PhysicsCache.hasContactWith(rbs, rb2);
for (rb in rbs) {
if (rb == rb2) {
contact = true;
break;
}
}
} }
} #end
#end
var b = false; var b = false;
switch (property0) { switch (property0) {

View File

@ -0,0 +1,23 @@
package leenkx.logicnode;
class OnceNode extends LogicNode {
var triggered:Bool = false;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
if(from == 1){
triggered = false;
return;
}
if (!triggered) {
triggered = true;
runOutput(0);
}
}
}

View File

@ -9,19 +9,38 @@ import iron.Scene;
class PlayAnimationTreeNode extends LogicNode { class PlayAnimationTreeNode extends LogicNode {
var object: Object;
var action: Dynamic;
var init: Bool = false;
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
} }
override function run(from: Int) { override function run(from: Int) {
var object: Object = inputs[1].get(); object = inputs[1].get();
var action: Dynamic = inputs[2].get(); action = inputs[2].get();
assert(Error, object != null, "The object input not be null"); assert(Error, object != null, "The object input not be null");
init = true;
tree.notifyOnUpdate(playAnim);
// TO DO: Investigate AnimAction get and PlayAnimationTree notifiers
}
function playAnim() {
if (init = false) return;
init = false;
tree.removeUpdate(playAnim);
var animation = object.animation; var animation = object.animation;
if(animation == null) { if(animation == null) {
#if lnx_skin #if lnx_skin
animation = object.getBoneAnimation(object.uid); animation = object.getBoneAnimation(object.uid);
if (animation == null) {
tree.notifyOnUpdate(playAnim);
init = true;
return;
}
cast(animation, BoneAnimation).setAnimationLoop(function f(mats) { cast(animation, BoneAnimation).setAnimationLoop(function f(mats) {
action(mats); action(mats);
}); });
@ -32,7 +51,6 @@ class PlayAnimationTreeNode extends LogicNode {
action(mats); action(mats);
}); });
} }
runOutput(0); runOutput(0);
} }
} }

View File

@ -0,0 +1,41 @@
package leenkx.logicnode;
class ProbabilisticIndexNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var probs: Array<Float> = [];
var probs_acum: Array<Float> = [];
var sum: Float = 0;
for (p in 0...inputs.length){
probs.push(inputs[p].get());
sum += probs[p];
}
if (sum > 1){
for (p in 0...probs.length)
probs[p] /= sum;
}
sum = 0;
for (p in 0...probs.length){
sum += probs[p];
probs_acum.push(sum);
}
var rand: Float = Math.random();
for (p in 0...probs.length){
if (p == 0 && rand <= probs_acum[p]) return p;
else if (0 < p && p < probs.length-1 && probs_acum[p-1] < rand && rand <= probs_acum[p]) return p;
else if (p == probs.length-1 && probs_acum[p-1] < rand) return p;
}
return null;
}
}

View File

@ -0,0 +1,23 @@
package leenkx.logicnode;
import aura.Aura;
import aura.Types;
class SetAudioPositionNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var audio = inputs[1].get();
if (audio == null) return;
var positionInSeconds:Float = inputs[2].get();
if (positionInSeconds < 0.0) positionInSeconds = 0.0;
audio.channel.floatPosition = positionInSeconds * audio.channel.sampleRate;
runOutput(0);
}
}

View File

@ -0,0 +1,38 @@
package leenkx.logicnode;
import iron.object.MeshObject;
import iron.object.CameraObject;
class SetCameraRenderFilterNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var mo: MeshObject = cast inputs[1].get();
var camera: CameraObject = inputs[2].get();
assert(Error, Std.isOfType(camera, CameraObject), "Camera must be a camera object!");
if (camera == null || mo == null) return;
if (property0 == 'Add'){
if (mo.cameraList == null || mo.cameraList.indexOf(camera.name) == -1){
if (mo.cameraList == null) mo.cameraList = [];
mo.cameraList.push(camera.name);
}
}
else{
if (mo.cameraList != null){
mo.cameraList.remove(camera.name);
if (mo.cameraList.length == 0)
mo.cameraList = null;
}
}
runOutput(0);
}
}

View File

@ -0,0 +1,21 @@
package leenkx.logicnode;
import iron.object.LightObject;
class SetLightShadowNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var light: LightObject = inputs[1].get();
var shadow: Bool = inputs[2].get();
if (light == null) return;
light.data.raw.cast_shadow = shadow;
runOutput(0);
}
}

View File

@ -0,0 +1,206 @@
package leenkx.logicnode;
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
import iron.object.Object;
class SetLookAtRotationNode extends LogicNode {
public var property0: String; // Axis to align
public var property1: String; // Use vector for target (true/false)
public var property2: String; // Use vector for source (true/false)
public var property3: String; // Damping value (backward compatibility, now input socket)
public var property4: String; // Disable rotation on aligning axis (true/false)
public var property5: String; // Use local space (true/false)
// Store the calculated rotation for output
var calculatedRotation: Quat = null;
// Store the previous rotation for smooth interpolation
var previousRotation: Quat = null;
public function new(tree: LogicTree) {
super(tree);
previousRotation = new Quat();
}
override function run(from: Int): Void {
// Determine if we're using a vector or an object as source
var useSourceVector: Bool = property2 == "true";
var objectToUse: Object = null;
var objectLoc: Vec4 = null;
if (useSourceVector) {
// Use tree.object as the object to rotate
objectToUse = tree.object;
if (objectToUse == null) {
runOutput(0);
return;
}
// Get the source location directly
objectLoc = inputs[1].get();
if (objectLoc == null) {
runOutput(0);
return;
}
} else {
// Get the source object (or fallback to tree.object)
objectToUse = (inputs.length > 1 && inputs[1] != null) ? inputs[1].get() : tree.object;
if (objectToUse == null) {
runOutput(0);
return;
}
// Get source object's WORLD position (important for child objects)
objectLoc = new Vec4(objectToUse.transform.worldx(), objectToUse.transform.worldy(), objectToUse.transform.worldz());
}
// Determine if we're using a vector or an object as target
var useTargetVector: Bool = property1 == "true";
var targetLoc: Vec4 = null;
if (useTargetVector) {
// Get the target location directly
targetLoc = inputs[2].get();
if (targetLoc == null) {
runOutput(0);
return;
}
} else {
// Get the target object
var targetObject: Object = inputs[2].get();
if (targetObject == null) {
runOutput(0);
return;
}
// Get target object's WORLD position (important for child objects)
targetLoc = new Vec4(targetObject.transform.worldx(), targetObject.transform.worldy(), targetObject.transform.worldz());
}
// Calculate direction to target
var direction = new Vec4(
targetLoc.x - objectLoc.x,
targetLoc.y - objectLoc.y,
targetLoc.z - objectLoc.z
);
direction.normalize();
// Calculate target rotation based on selected axis
calculatedRotation = new Quat();
switch (property0) {
case "X":
calculatedRotation.fromTo(new Vec4(1, 0, 0), direction);
case "-X":
calculatedRotation.fromTo(new Vec4(-1, 0, 0), direction);
case "Y":
calculatedRotation.fromTo(new Vec4(0, 1, 0), direction);
case "-Y":
calculatedRotation.fromTo(new Vec4(0, -1, 0), direction);
case "Z":
calculatedRotation.fromTo(new Vec4(0, 0, 1), direction);
case "-Z":
calculatedRotation.fromTo(new Vec4(0, 0, -1), direction);
}
// If disable rotation on aligning axis is enabled, constrain the target rotation
if (property4 == "true") {
// Apply constraint to the target rotation BEFORE damping to avoid jiggling
var eulerAngles = calculatedRotation.toEulerOrdered("XYZ");
// Set the rotation around the selected axis to 0
switch (property0) {
case "X", "-X":
eulerAngles.x = 0.0;
case "Y", "-Y":
eulerAngles.y = 0.0;
case "Z", "-Z":
eulerAngles.z = 0.0;
}
// Convert back to quaternion
calculatedRotation.fromEulerOrdered(eulerAngles, "XYZ");
}
// Convert world rotation to local rotation if local space is enabled and object has a parent
var targetRotation = new Quat();
if (property5 == "true" && objectToUse.parent != null) {
// Get parent's world rotation
var parentWorldLoc = new Vec4();
var parentWorldRot = new Quat();
var parentWorldScale = new Vec4();
objectToUse.parent.transform.world.decompose(parentWorldLoc, parentWorldRot, parentWorldScale);
// Convert world rotation to local space by removing parent's rotation influence
// local_rotation = inverse(parent_world_rotation) * world_rotation
var invParentRot = new Quat().setFrom(parentWorldRot);
invParentRot.x = -invParentRot.x;
invParentRot.y = -invParentRot.y;
invParentRot.z = -invParentRot.z;
targetRotation.multquats(invParentRot, calculatedRotation);
} else {
// No local space conversion needed, use world rotation directly
targetRotation.setFrom(calculatedRotation);
}
// Apply rotation with damping
var dampingValue: Float = 0.0;
// Try to get damping from input socket first (index 3), fallback to property
if (inputs.length > 3 && inputs[3] != null) {
var dampingInput: Dynamic = inputs[3].get();
if (dampingInput != null) {
dampingValue = dampingInput;
}
} else {
// Fallback to property for backward compatibility
dampingValue = Std.parseFloat(property3);
}
if (dampingValue > 0.0) {
// Create a fixed interpolation rate that never reaches exactly 1.0
// Higher damping = slower rotation (smaller step)
var step = Math.max(0.001, (1.0 - dampingValue) * 0.2); // 0.001 to 0.2 range
// Get current local rotation as quaternion
var currentLocalRot = new Quat().setFrom(objectToUse.transform.rot);
// Calculate the difference between current and target rotation
var diffQuat = new Quat();
// q1 * inverse(q2) gives the rotation from q2 to q1
var invCurrent = new Quat().setFrom(currentLocalRot);
invCurrent.x = -invCurrent.x;
invCurrent.y = -invCurrent.y;
invCurrent.z = -invCurrent.z;
diffQuat.multquats(targetRotation, invCurrent);
// Convert to axis-angle representation
var axis = new Vec4();
var angle = diffQuat.toAxisAngle(axis);
// Apply only a portion of this rotation (step)
var partialAngle = angle * step;
// Create partial rotation quaternion
var partialRot = new Quat().fromAxisAngle(axis, partialAngle);
// Apply this partial rotation to current local rotation
var newLocalRot = new Quat();
newLocalRot.multquats(partialRot, currentLocalRot);
// Apply the new local rotation
objectToUse.transform.rot.setFrom(newLocalRot);
} else {
// No damping, apply instant rotation
objectToUse.transform.rot.setFrom(targetRotation);
}
objectToUse.transform.buildMatrix();
runOutput(0);
}
// No output sockets needed - this node only performs actions
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
import iron.object.Object;
import iron.math.Vec4;
import iron.math.Mat4;
import iron.system.Time;
class SetObjectDelayedLocationNode extends LogicNode {
public var use_local_space: Bool = false;
private var initialOffset: Vec4 = null;
private var targetPos: Vec4 = new Vec4();
private var currentPos: Vec4 = new Vec4();
private var deltaVec: Vec4 = new Vec4();
private var tempVec: Vec4 = new Vec4();
private var lastParent: Object = null;
private var invParentMatrix: Mat4 = null;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var follower: Object = inputs[1].get();
var target: Object = inputs[2].get();
var delay: Float = inputs[3].get();
if (follower == null || target == null) return runOutput(0);
if (initialOffset == null) {
initialOffset = new Vec4();
var followerPos = follower.transform.world.getLoc();
var targetPos = target.transform.world.getLoc();
initialOffset.setFrom(followerPos);
initialOffset.sub(targetPos);
}
targetPos.setFrom(target.transform.world.getLoc());
currentPos.setFrom(follower.transform.world.getLoc());
tempVec.setFrom(targetPos).add(initialOffset);
deltaVec.setFrom(tempVec).sub(currentPos);
if (deltaVec.length() < 0.001 && delay < 0.01) {
runOutput(0);
return;
}
if (delay == 0.0) {
currentPos.setFrom(tempVec);
} else {
var smoothFactor = Math.exp(-Time.delta / Math.max(0.0001, delay));
currentPos.x = tempVec.x + (currentPos.x - tempVec.x) * smoothFactor;
currentPos.y = tempVec.y + (currentPos.y - tempVec.y) * smoothFactor;
currentPos.z = tempVec.z + (currentPos.z - tempVec.z) * smoothFactor;
}
if (use_local_space && follower.parent != null) {
if (follower.parent != lastParent || invParentMatrix == null) {
lastParent = follower.parent;
invParentMatrix = Mat4.identity();
invParentMatrix.getInverse(follower.parent.transform.world);
}
tempVec.setFrom(currentPos);
tempVec.applymat(invParentMatrix);
follower.transform.loc.set(tempVec.x, tempVec.y, tempVec.z);
} else {
follower.transform.loc.set(currentPos.x, currentPos.y, currentPos.z);
}
follower.transform.buildMatrix();
runOutput(0);
}
}

View File

@ -55,9 +55,9 @@ class SetParticleDataNode extends LogicNode {
@:privateAccess psys.aligny = vel.y; @:privateAccess psys.aligny = vel.y;
@:privateAccess psys.alignz = vel.z; @:privateAccess psys.alignz = vel.z;
case 'Velocity Random': case 'Velocity Random':
psys.r.factor_random = inputs[3].get(); @:privateAccess psys.r.factor_random = inputs[3].get();
case 'Weight Gravity': case 'Weight Gravity':
psys.r.weight_gravity = inputs[3].get(); @:privateAccess psys.r.weight_gravity = inputs[3].get();
if (iron.Scene.active.raw.gravity != null) { if (iron.Scene.active.raw.gravity != null) {
@:privateAccess psys.gx = iron.Scene.active.raw.gravity[0] * @:privateAccess psys.r.weight_gravity; @:privateAccess psys.gx = iron.Scene.active.raw.gravity[0] * @:privateAccess psys.r.weight_gravity;
@:privateAccess psys.gy = iron.Scene.active.raw.gravity[1] * @:privateAccess psys.r.weight_gravity; @:privateAccess psys.gy = iron.Scene.active.raw.gravity[1] * @:privateAccess psys.r.weight_gravity;

View File

@ -0,0 +1,39 @@
package leenkx.logicnode;
#if lnx_audio
import iron.object.SpeakerObject;
import kha.audio1.AudioChannel;
import iron.system.Audio;
#end
class SetPositionSpeakerNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if lnx_audio
var object: SpeakerObject = cast(inputs[1].get(), SpeakerObject);
if (object == null || object.sound == null) return;
var positionInSeconds:Float = inputs[2].get();
if (positionInSeconds < 0) positionInSeconds = 0;
var volume = object.data.volume;
var loop = object.data.loop;
var stream = object.data.stream;
object.stop();
var channel = Audio.play(object.sound, loop, stream);
if (channel != null) {
object.channels.push(channel);
channel.volume = volume;
@:privateAccess channel.set_position(positionInSeconds);
}
#end
runOutput(0);
}
}

View File

@ -1,5 +1,7 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.data.SceneFormat;
class SetWorldNode extends LogicNode { class SetWorldNode extends LogicNode {
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
@ -10,25 +12,6 @@ class SetWorldNode extends LogicNode {
var world: String = inputs[1].get(); var world: String = inputs[1].get();
if (world != null){ if (world != null){
//check if world shader data exists
var file: String = 'World_'+world+'_data';
#if lnx_json
file += ".json";
#elseif lnx_compress
file += ".lz4";
#else
file += '.lnx';
#end
var exists: Bool = false;
iron.data.Data.getBlob(file, function(b: kha.Blob) {
if (b != null) exists = true;
});
assert(Error, exists == true, "World must be either associated to a scene or have fake user");
iron.Scene.active.raw.world_ref = world; iron.Scene.active.raw.world_ref = world;
var npath = leenkx.renderpath.RenderPathCreator.get(); var npath = leenkx.renderpath.RenderPathCreator.get();
npath.loadShader("shader_datas/World_" + world + "/World_" + world); npath.loadShader("shader_datas/World_" + world + "/World_" + world);

View File

@ -641,18 +641,20 @@ class RenderPathForward {
var framebuffer = ""; var framebuffer = "";
#end #end
#if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA")) RenderPathCreator.finalTarget = path.currentTarget;
var target = "";
#if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA") || (!rp_render_to_texture))
{ {
RenderPathCreator.finalTarget = path.currentTarget; target = framebuffer;
path.setTarget(framebuffer);
} }
#else #else
{ {
path.setTarget("buf"); target = "buf";
RenderPathCreator.finalTarget = path.currentTarget;
} }
#end #end
path.setTarget(target);
#if rp_compositordepth #if rp_compositordepth
{ {
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
@ -671,6 +673,15 @@ class RenderPathForward {
} }
#end #end
#if rp_overlays
{
path.setTarget(target);
path.clearTarget(null, 1.0);
path.drawMeshes("overlay");
}
#end
#if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA"))
{ {
path.setTarget("bufa"); path.setTarget("bufa");
@ -701,12 +712,6 @@ class RenderPathForward {
} }
#end #end
#if rp_overlays
{
path.clearTarget(null, 1.0);
path.drawMeshes("overlay");
}
#end
} }
public static function setupDepthTexture() { public static function setupDepthTexture() {

View File

@ -3,33 +3,35 @@ package leenkx.system;
import haxe.Constraints.Function; import haxe.Constraints.Function;
class Signal { class Signal {
var callbacks:Array<Function> = []; var callbacks: Array<Function> = [];
public function new() { public function new() {
} }
public function connect(callback:Function) { public function connect(callback: Function) {
if (!callbacks.contains(callback)) callbacks.push(callback); if (!callbacks.contains(callback)) callbacks.push(callback);
} }
public function disconnect(callback:Function) { public function disconnect(callback: Function) {
if (callbacks.contains(callback)) callbacks.remove(callback); if (callbacks.contains(callback)) callbacks.remove(callback);
} }
public function emit(...args:Any) { public function emit(...args: Any) {
for (callback in callbacks) Reflect.callMethod(this, callback, args); for (callback in callbacks.copy()) {
if (callbacks.contains(callback)) Reflect.callMethod(null, callback, args);
}
} }
public function getConnections():Array<Function> { public function getConnections(): Array<Function> {
return callbacks; return callbacks;
} }
public function isConnected(callBack:Function):Bool { public function isConnected(callBack: Function):Bool {
return callbacks.contains(callBack); return callbacks.contains(callBack);
} }
public function isNull():Bool { public function isNull(): Bool {
return callbacks.length == 0; return callbacks.length == 0;
} }
} }

View File

@ -57,7 +57,7 @@ class Starter {
iron.Scene.getRenderPath = getRenderPath; iron.Scene.getRenderPath = getRenderPath;
#end #end
#if lnx_draworder_shader #if lnx_draworder_shader
iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Shader; iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Index;
#end // else Distance #end // else Distance
}); });
}); });

View File

@ -1,87 +1,243 @@
package leenkx.trait; package leenkx.trait;
import iron.Trait;
import iron.math.Vec4; import iron.math.Vec4;
import iron.system.Input; import iron.system.Input;
import iron.object.Object; import iron.object.Object;
import iron.object.CameraObject; import iron.object.CameraObject;
import leenkx.trait.physics.PhysicsWorld; import leenkx.trait.physics.PhysicsWorld;
import leenkx.trait.internal.CameraController; import leenkx.trait.physics.RigidBody;
import kha.FastFloat;
class FirstPersonController extends CameraController { class FirstPersonController extends Trait {
#if (!lnx_physics) #if (!lnx_physics)
public function new() { super(); } public function new() { super(); }
#else #else
var head: Object; @prop public var rotationSpeed:Float = 0.15;
static inline var rotationSpeed = 2.0; @prop public var maxPitch:Float = 2.2;
@prop public var minPitch:Float = 0.5;
@prop public var enableJump:Bool = true;
@prop public var jumpForce:Float = 22.0;
@prop public var moveSpeed:Float = 500.0;
public function new() { @prop public var forwardKey:String = "w";
super(); @prop public var backwardKey:String = "s";
@prop public var leftKey:String = "a";
@prop public var rightKey:String = "d";
@prop public var jumpKey:String = "space";
iron.Scene.active.notifyOnInit(init); @prop public var allowAirJump:Bool = false;
}
function init() { @prop public var canRun:Bool = true;
head = object.getChildOfType(CameraObject); @prop public var runKey:String = "shift";
@prop public var runSpeed:Float = 1000.0;
PhysicsWorld.active.notifyOnPreUpdate(preUpdate); // Sistema de estamina
notifyOnUpdate(update); @prop public var stamina:Bool = false;
notifyOnRemove(removed); @prop public var staminaBase:Float = 75.0;
} @prop public var staRecoverPerSec:Float = 5.0;
@prop public var staDecreasePerSec:Float = 5.0;
@prop public var staRecoverTime:Float = 2.0;
@prop public var staDecreasePerJump:Float = 5.0;
@prop public var enableFatigue:Bool = false;
@prop public var fatigueSpeed:Float = 0.5; // the reduction of movement when fatigue is activated...
@prop public var fatigueThreshold:Float = 30.0; // Tiempo corriendo sin parar para la activacion // Time running non-stop for activation...
@prop public var fatRecoveryThreshold:Float = 7.5; // Tiempo sin correr/saltar para salir de fatiga // Time without running/jumping to get rid of fatigue...
var xVec = Vec4.xAxis(); // Var Privadas
var zVec = Vec4.zAxis(); var head:CameraObject;
function preUpdate() { var pitch:Float = 0.0;
if (Input.occupied || !body.ready) return; var body:RigidBody;
var mouse = Input.getMouse(); var moveForward:Bool = false;
var kb = Input.getKeyboard(); var moveBackward:Bool = false;
var moveLeft:Bool = false;
var moveRight:Bool = false;
var isRunning:Bool = false;
if (mouse.started() && !mouse.locked) mouse.lock(); var canJump:Bool = true;
else if (kb.started("escape") && mouse.locked) mouse.unlock(); var staminaValue:Float = 0.0;
var timeSinceStop:Float = 0.0;
if (mouse.locked || mouse.down()) { var fatigueTimer:Float = 0.0;
head.transform.rotate(xVec, -mouse.movementY / 250 * rotationSpeed); var fatigueCooldown:Float = 0.0;
transform.rotate(zVec, -mouse.movementX / 250 * rotationSpeed); var isFatigueActive:Bool = false;
body.syncTransform();
public function new() {
super();
iron.Scene.active.notifyOnInit(init);
}
function init() {
body = object.getTrait(RigidBody);
head = object.getChildOfType(CameraObject);
PhysicsWorld.active.notifyOnPreUpdate(preUpdate);
notifyOnUpdate(update);
notifyOnRemove(removed);
staminaValue = staminaBase;
}
function removed() {
PhysicsWorld.active.removePreUpdate(preUpdate);
}
var zVec = Vec4.zAxis();
function preUpdate() {
if (Input.occupied || body == null) 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()) {
var deltaTime:Float = iron.system.Time.delta;
object.transform.rotate(zVec, -mouse.movementX * rotationSpeed * deltaTime);
var deltaPitch:Float = -(mouse.movementY * rotationSpeed * deltaTime);
pitch += deltaPitch;
pitch = Math.max(minPitch, Math.min(maxPitch, pitch));
head.transform.setRotation(pitch, 0.0, 0.0);
body.syncTransform();
}
}
var dir:Vec4 = new Vec4();
function isFatigued():Bool {
return enableFatigue && isFatigueActive;
}
function update() {
if (body == null) return;
var deltaTime:Float = iron.system.Time.delta;
var kb = Input.getKeyboard();
moveForward = kb.down(forwardKey);
moveBackward = kb.down(backwardKey);
moveLeft = kb.down(leftKey);
moveRight = kb.down(rightKey);
var isMoving = moveForward || moveBackward || moveLeft || moveRight;
var isGrounded:Bool = false;
#if lnx_physics
var vel = body.getLinearVelocity();
if (Math.abs(vel.z) < 0.1) {
isGrounded = true;
}
#end
// Dejo establecido el salto para tener en cuenta la (enableFatigue) si es que es false/true....
if (isGrounded && !isFatigued()) {
canJump = true;
} }
} // Saltar con estamina
if (enableJump && kb.started(jumpKey) && canJump) {
var jumpPower = jumpForce;
// Disminuir el salto al 50% si la (stamina) esta por debajo o en el 20%.
if (stamina) {
if (staminaValue <= 0) {
jumpPower = 0;
} else if (staminaValue <= staminaBase * 0.2) {
jumpPower *= 0.5;
}
function removed() { staminaValue -= staDecreasePerJump;
PhysicsWorld.active.removePreUpdate(preUpdate); if (staminaValue < 0.0) staminaValue = 0.0;
} timeSinceStop = 0.0;
}
var dir = new Vec4(); if (jumpPower > 0) {
function update() { body.applyImpulse(new Vec4(0, 0, jumpPower));
if (!body.ready) return; if (!allowAirJump) canJump = false;
}
}
if (jump) { // Control de estamina y correr
body.applyImpulse(new Vec4(0, 0, 16)); if (canRun && kb.down(runKey) && isMoving) {
jump = false; if (stamina) {
if (staminaValue > 0.0) {
isRunning = true;
staminaValue -= staDecreasePerSec * deltaTime;
if (staminaValue < 0.0) staminaValue = 0.0;
} else {
isRunning = false;
}
} else {
isRunning = true;
}
} else {
isRunning = false;
}
// (temporizadores aparte)
if (isRunning) {
timeSinceStop = 0.0;
fatigueTimer += deltaTime;
fatigueCooldown = 0.0;
} else {
timeSinceStop += deltaTime;
fatigueCooldown += deltaTime;
}
// Evitar correr y saltar al estar fatigado...
if (isFatigued()) {
isRunning = false;
canJump = false;
} }
// Move // Activar fatiga despues de correr continuamente durante cierto umbral
dir.set(0, 0, 0); if (enableFatigue && fatigueTimer >= fatigueThreshold) {
if (moveForward) dir.add(transform.look()); isFatigueActive = true;
if (moveBackward) dir.add(transform.look().mult(-1)); }
if (moveLeft) dir.add(transform.right().mult(-1));
if (moveRight) dir.add(transform.right());
// Push down // Eliminar la fatiga despues de recuperarse
var btvec = body.getLinearVelocity(); if (enableFatigue && isFatigueActive && fatigueCooldown >= fatRecoveryThreshold) {
body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); isFatigueActive = false;
fatigueTimer = 0.0;
}
if (moveForward || moveBackward || moveLeft || moveRight) { // Recuperar estamina si no esta corriendo
var dirN = dir.normalize(); if (stamina && !isRunning && staminaValue < staminaBase && !isFatigued()) {
dirN.mult(6); if (timeSinceStop >= staRecoverTime) {
body.activate(); staminaValue += staRecoverPerSec * deltaTime;
body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0); if (staminaValue > staminaBase) staminaValue = staminaBase;
} }
}
// Keep vertical // Movimiento ejes (local)
body.setAngularFactor(0, 0, 0); dir.set(0, 0, 0);
camera.buildMatrix(); if (moveForward) dir.add(object.transform.look());
} if (moveBackward) dir.add(object.transform.look().mult(-1));
#end if (moveLeft) dir.add(object.transform.right().mult(-1));
if (moveRight) dir.add(object.transform.right());
var btvec = body.getLinearVelocity();
body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0);
if (isMoving) {
var dirN = dir.normalize();
var baseSpeed = moveSpeed;
if (isRunning && moveForward) {
baseSpeed = runSpeed;
}
var currentSpeed = isFatigued() ? baseSpeed * fatigueSpeed : baseSpeed;
dirN.mult(currentSpeed * deltaTime);
body.activate();
body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0);
}
body.setAngularFactor(0, 0, 0);
head.buildMatrix();
}
#end
} }
// Stamina and fatigue system.....

View File

@ -73,7 +73,17 @@ class PhysicsBreak extends Trait {
collisionMargin: 0.04, collisionMargin: 0.04,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThrshold: 0.0,
deactivationTime: 0.0 deactivationTime: 0.0,
linearVelocityMin: 0.0,
linearVelocityMax: 0.0,
angularVelocityMin: 0.0,
angularVelocityMax: 0.0,
lockTranslationX: false,
lockTranslationY: false,
lockTranslationZ: false,
lockRotationX: false,
lockRotationY: false,
lockRotationZ: false
}; };
o.addTrait(new RigidBody(Shape.ConvexHull, ud.mass, ud.friction, 0, 1, params)); o.addTrait(new RigidBody(Shape.ConvexHull, ud.mass, ud.friction, 0, 1, params));
if (cast(o, MeshObject).data.geom.positions.values.length < 600) { if (cast(o, MeshObject).data.geom.positions.values.length < 600) {

View File

@ -280,7 +280,11 @@ class DebugConsole extends Trait {
function drawObjectNameInList(object: iron.object.Object, selected: Bool) { function drawObjectNameInList(object: iron.object.Object, selected: Bool) {
var _y = ui._y; var _y = ui._y;
ui.text(object.uid+'_'+object.name);
if (object.parent.name == 'Root' && object.raw == null)
ui.text(object.uid+'_'+object.name+' ('+iron.Scene.active.raw.world_ref+')');
else
ui.text(object.uid+'_'+object.name);
if (object == iron.Scene.active.camera) { if (object == iron.Scene.active.camera) {
var tagWidth = 100; var tagWidth = 100;

View File

@ -0,0 +1,98 @@
package leenkx.trait.physics;
import iron.object.Object;
class PhysicsCache {
#if lnx_physics
static var rbCache: Map<Int, Dynamic> = new Map();
static var contactsCache: Map<Int, Array<Dynamic>> = new Map();
static var physicsFrame: Int = 0;
static var lastQueryFrame: Int = -1;
#end
public static function getCachedRigidBody(object: Object): Dynamic {
#if (!lnx_physics)
return null;
#else
if (object == null) return null;
var cached = rbCache.get(object.uid);
if (cached != null) return cached;
#if lnx_bullet
var rb = object.getTrait(leenkx.trait.physics.bullet.RigidBody);
#else
var rb = object.getTrait(leenkx.trait.physics.oimo.RigidBody);
#end
if (rb != null) rbCache.set(object.uid, rb);
return rb;
#end
}
public static function getCachedContacts(rb: Dynamic): Array<Dynamic> {
#if (!lnx_physics)
return null;
#else
if (rb == null) return null;
var rbObjectId = (rb.object != null) ? rb.object.uid : -1;
if (rbObjectId == -1) {
#if lnx_bullet
if (leenkx.trait.physics.bullet.PhysicsWorld.active == null) return null;
return leenkx.trait.physics.bullet.PhysicsWorld.active.getContacts(rb);
#else
if (leenkx.trait.physics.oimo.PhysicsWorld.active == null) return null;
return leenkx.trait.physics.oimo.PhysicsWorld.active.getContacts(rb);
#end
}
if (lastQueryFrame == physicsFrame) {
var cached = contactsCache.get(rbObjectId);
if (cached != null) return cached;
}
lastQueryFrame = physicsFrame;
var cached = contactsCache.get(rbObjectId);
if (cached != null) return cached;
#if lnx_bullet
if (leenkx.trait.physics.bullet.PhysicsWorld.active == null) return null;
var contacts = leenkx.trait.physics.bullet.PhysicsWorld.active.getContacts(rb);
#else
if (leenkx.trait.physics.oimo.PhysicsWorld.active == null) return null;
var contacts = leenkx.trait.physics.oimo.PhysicsWorld.active.getContacts(rb);
#end
if (contacts != null) {
contactsCache.set(rbObjectId, contacts);
}
return contacts;
#end
}
public static inline function hasContactWith(contacts: Array<Dynamic>, target: Dynamic): Bool {
#if (!lnx_physics)
return false;
#else
return contacts != null && target != null && contacts.indexOf(target) >= 0;
#end
}
public static function clearCache() {
#if lnx_physics
rbCache.clear();
contactsCache.clear();
#end
}
public static function clearContactsCache() {
#if lnx_physics
physicsFrame++;
contactsCache.clear();
#end
}
}

View File

@ -8,11 +8,9 @@ class PhysicsWorld extends iron.Trait { public function new() { super(); } }
#else #else
#if lnx_bullet #if lnx_bullet
typedef PhysicsWorld = leenkx.trait.physics.bullet.PhysicsWorld; typedef PhysicsWorld = leenkx.trait.physics.bullet.PhysicsWorld;
typedef Hit = leenkx.trait.physics.bullet.PhysicsWorld.Hit; typedef Hit = leenkx.trait.physics.bullet.PhysicsWorld.Hit;
#else #else
typedef PhysicsWorld = leenkx.trait.physics.oimo.PhysicsWorld; typedef PhysicsWorld = leenkx.trait.physics.oimo.PhysicsWorld;
typedef Hit = leenkx.trait.physics.oimo.PhysicsWorld.Hit; typedef Hit = leenkx.trait.physics.oimo.PhysicsWorld.Hit;
#end #end

View File

@ -7,6 +7,7 @@ import iron.system.Time;
import iron.math.Vec4; import iron.math.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.math.RayCaster; import iron.math.RayCaster;
import leenkx.trait.physics.PhysicsCache;
class Hit { class Hit {
@ -145,6 +146,7 @@ class PhysicsWorld extends Trait {
iron.Scene.active.notifyOnRemove(function() { iron.Scene.active.notifyOnRemove(function() {
sceneRemoved = true; sceneRemoved = true;
PhysicsCache.clearCache();
}); });
} }
@ -303,6 +305,8 @@ class PhysicsWorld extends Trait {
var t = Time.fixedStep * timeScale * Time.scale; var t = Time.fixedStep * timeScale * Time.scale;
if (t == 0.0) return; // Simulation paused if (t == 0.0) return; // Simulation paused
PhysicsCache.clearContactsCache();
#if lnx_debug #if lnx_debug
var startTime = kha.Scheduler.realTime(); var startTime = kha.Scheduler.realTime();
#end #end

View File

@ -36,6 +36,18 @@ class RigidBody extends iron.Trait {
var useDeactivation: Bool; var useDeactivation: Bool;
var deactivationParams: Array<Float>; var deactivationParams: Array<Float>;
var ccd = false; // Continuous collision detection var ccd = false; // Continuous collision detection
// New velocity limiting properties
var linearVelocityMin: Float;
var linearVelocityMax: Float;
var angularVelocityMin: Float;
var angularVelocityMax: Float;
// New lock properties
var lockTranslationX: Bool;
var lockTranslationY: Bool;
var lockTranslationZ: Bool;
var lockRotationX: Bool;
var lockRotationY: Bool;
var lockRotationZ: Bool;
public var group = 1; public var group = 1;
public var mask = 1; public var mask = 1;
var trigger = false; var trigger = false;
@ -120,7 +132,17 @@ class RigidBody extends iron.Trait {
collisionMargin: 0.0, collisionMargin: 0.0,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThrshold: 0.0,
deactivationTime: 0.0 deactivationTime: 0.0,
linearVelocityMin: 0.0,
linearVelocityMax: 0.0,
angularVelocityMin: 0.0,
angularVelocityMax: 0.0,
lockTranslationX: false,
lockTranslationY: false,
lockTranslationZ: false,
lockRotationX: false,
lockRotationY: false,
lockRotationZ: false
}; };
if (flags == null) flags = { if (flags == null) flags = {
@ -139,6 +161,18 @@ class RigidBody extends iron.Trait {
this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ];
this.collisionMargin = params.collisionMargin; this.collisionMargin = params.collisionMargin;
this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime];
// New velocity limiting properties
this.linearVelocityMin = params.linearVelocityMin;
this.linearVelocityMax = params.linearVelocityMax;
this.angularVelocityMin = params.angularVelocityMin;
this.angularVelocityMax = params.angularVelocityMax;
// New lock properties
this.lockTranslationX = params.lockTranslationX;
this.lockTranslationY = params.lockTranslationY;
this.lockTranslationZ = params.lockTranslationZ;
this.lockRotationX = params.lockRotationX;
this.lockRotationY = params.lockRotationY;
this.lockRotationZ = params.lockRotationZ;
this.animated = flags.animated; this.animated = flags.animated;
this.trigger = flags.trigger; this.trigger = flags.trigger;
this.ccd = flags.ccd; this.ccd = flags.ccd;
@ -291,11 +325,25 @@ class RigidBody extends iron.Trait {
} }
if (linearFactors != null) { if (linearFactors != null) {
setLinearFactor(linearFactors[0], linearFactors[1], linearFactors[2]); // Apply lock properties by overriding factors
var lx = linearFactors[0];
var ly = linearFactors[1];
var lz = linearFactors[2];
if (lockTranslationX) lx = 0.0;
if (lockTranslationY) ly = 0.0;
if (lockTranslationZ) lz = 0.0;
setLinearFactor(lx, ly, lz);
} }
if (angularFactors != null) { if (angularFactors != null) {
setAngularFactor(angularFactors[0], angularFactors[1], angularFactors[2]); // Apply lock properties by overriding factors
var ax = angularFactors[0];
var ay = angularFactors[1];
var az = angularFactors[2];
if (lockRotationX) ax = 0.0;
if (lockRotationY) ay = 0.0;
if (lockRotationZ) az = 0.0;
setAngularFactor(ax, ay, az);
} }
if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE); if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE);
@ -411,6 +459,55 @@ class RigidBody extends iron.Trait {
var rbs = physics.getContacts(this); var rbs = physics.getContacts(this);
if (rbs != null) for (rb in rbs) for (f in onContact) f(rb); if (rbs != null) for (rb in rbs) for (f in onContact) f(rb);
} }
// Apply velocity limiting if enabled
if (!animated && !staticObj) {
applyVelocityLimits();
}
}
function applyVelocityLimits() {
if (!ready) return;
// Check linear velocity limits
if (linearVelocityMin > 0.0 || linearVelocityMax > 0.0) {
var velocity = getLinearVelocity();
var speed = velocity.length();
if (linearVelocityMin > 0.0 && speed < linearVelocityMin) {
// Increase velocity to minimum
if (speed > 0.0) {
velocity.normalize();
velocity.mult(linearVelocityMin);
setLinearVelocity(velocity.x, velocity.y, velocity.z);
}
} else if (linearVelocityMax > 0.0 && speed > linearVelocityMax) {
// Clamp velocity to maximum
velocity.normalize();
velocity.mult(linearVelocityMax);
setLinearVelocity(velocity.x, velocity.y, velocity.z);
}
}
// Check angular velocity limits
if (angularVelocityMin > 0.0 || angularVelocityMax > 0.0) {
var angularVel = getAngularVelocity();
var angularSpeed = angularVel.length();
if (angularVelocityMin > 0.0 && angularSpeed < angularVelocityMin) {
// Increase angular velocity to minimum
if (angularSpeed > 0.0) {
angularVel.normalize();
angularVel.mult(angularVelocityMin);
setAngularVelocity(angularVel.x, angularVel.y, angularVel.z);
}
} else if (angularVelocityMax > 0.0 && angularSpeed > angularVelocityMax) {
// Clamp angular velocity to maximum
angularVel.normalize();
angularVel.mult(angularVelocityMax);
setAngularVelocity(angularVel.x, angularVel.y, angularVel.z);
}
}
} }
public function disableCollision() { public function disableCollision() {
@ -745,6 +842,16 @@ typedef RigidBodyParams = {
var linearDeactivationThreshold: Float; var linearDeactivationThreshold: Float;
var angularDeactivationThrshold: Float; var angularDeactivationThrshold: Float;
var deactivationTime: Float; var deactivationTime: Float;
var linearVelocityMin: Float;
var linearVelocityMax: Float;
var angularVelocityMin: Float;
var angularVelocityMax: Float;
var lockTranslationX: Bool;
var lockTranslationY: Bool;
var lockTranslationZ: Bool;
var lockRotationX: Bool;
var lockRotationY: Bool;
var lockRotationZ: Bool;
} }
typedef RigidBodyFlags = { typedef RigidBodyFlags = {

Binary file not shown.

View File

@ -1,9 +1,17 @@
import importlib import importlib
import sys import sys
import types import types
import bpy
# This gets cleared if this package/the __init__ module is reloaded # This gets cleared if this package/the __init__ module is reloaded
_module_cache: dict[str, types.ModuleType] = {} if bpy.app.version < (2, 92, 0):
from typing import Dict
ModuleCacheType = Dict[str, types.ModuleType]
else:
ModuleCacheType = dict[str, types.ModuleType]
_module_cache: ModuleCacheType = {}
def enable_reload(module_name: str): def enable_reload(module_name: str):

View File

@ -15,7 +15,14 @@ from enum import Enum, unique
import math import math
import os import os
import time import time
from typing import Any, Dict, List, Tuple, Union, Optional from typing import Any, Dict, List, Tuple, Union, Optional, TYPE_CHECKING
import bpy
if bpy.app.version >= (3, 0, 0):
VertexColorType = bpy.types.Attribute
else:
VertexColorType = bpy.types.MeshLoopColorLayer
import numpy as np import numpy as np
@ -138,7 +145,7 @@ class LeenkxExporter:
self.world_array = [] self.world_array = []
self.particle_system_array = {} self.particle_system_array = {}
self.referenced_collections: list[bpy.types.Collection] = [] self.referenced_collections: List[bpy.types.Collection] = []
"""Collections referenced by collection instances""" """Collections referenced by collection instances"""
self.has_spawning_camera = False self.has_spawning_camera = False
@ -151,7 +158,7 @@ class LeenkxExporter:
self.default_part_material_objects = [] self.default_part_material_objects = []
self.material_to_lnx_object_dict = {} self.material_to_lnx_object_dict = {}
# Stores the link between a blender object and its # Stores the link between a blender object and its
# corresponding export data (arm object) # corresponding export data (lnx object)
self.object_to_lnx_object_dict: Dict[bpy.types.Object, Dict] = {} self.object_to_lnx_object_dict: Dict[bpy.types.Object, Dict] = {}
self.bone_tracks = [] self.bone_tracks = []
@ -540,8 +547,16 @@ class LeenkxExporter:
o['material_refs'].append(lnx.utils.asset_name(material)) o['material_refs'].append(lnx.utils.asset_name(material))
def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): 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.lnx_export or not bpy.data.objects[out_object['name']].modifiers[psys.name].show_render: if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT' or not psys.settings.instance_object.lnx_export:
return return
for obj in bpy.data.objects:
if obj.name == out_object['name']:
for mod in obj.modifiers:
if mod.type == 'PARTICLE_SYSTEM':
if mod.particle_system.name == psys.name:
if not mod.show_render:
return
self.particle_system_array[psys.settings] = {"structName": psys.settings.name} self.particle_system_array[psys.settings] = {"structName": psys.settings.name}
pref = { pref = {
@ -630,7 +645,10 @@ class LeenkxExporter:
continue continue
for slot in bobject.material_slots: for slot in bobject.material_slots:
if slot.material is None or slot.material.library is not None: if slot.material is None:
continue
if slot.material.library is not None:
slot.material.lnx_particle_flag = True
continue continue
if slot.material.name.endswith(variant_suffix): if slot.material.name.endswith(variant_suffix):
continue continue
@ -835,6 +853,13 @@ class LeenkxExporter:
} }
out_object['vertex_groups'].append(out_vertex_groups) out_object['vertex_groups'].append(out_vertex_groups)
if len(bobject.lnx_camera_list) > 0:
out_camera_list = []
for camera in bobject.lnx_camera_list:
if camera.lnx_camera_object_ptr != None:
out_camera_list.append(camera.lnx_camera_object_ptr.name)
if len(out_camera_list) > 0:
out_object['camera_list'] = out_camera_list
if len(bobject.lnx_propertylist) > 0: if len(bobject.lnx_propertylist) > 0:
out_object['properties'] = [] out_object['properties'] = []
@ -910,8 +935,12 @@ class LeenkxExporter:
out_object['particle_refs'] = [] out_object['particle_refs'] = []
out_object['render_emitter'] = bobject.show_instancer_for_render out_object['render_emitter'] = bobject.show_instancer_for_render
for i in range(num_psys): for i in range(num_psys):
if bobject.modifiers[bobject.particle_systems[i].name].show_render: for obj in bpy.data.objects:
self.export_particle_system_ref(bobject.particle_systems[i], out_object) for mod in obj.modifiers:
if mod.type == 'PARTICLE_SYSTEM':
if mod.particle_system.name == bobject.particle_systems[i].name:
if mod.show_render:
self.export_particle_system_ref(bobject.particle_systems[i], out_object)
aabb = bobject.data.lnx_aabb aabb = bobject.data.lnx_aabb
if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0:
@ -1427,31 +1456,38 @@ class LeenkxExporter:
@staticmethod @staticmethod
def get_num_vertex_colors(mesh: bpy.types.Mesh) -> int: def get_num_vertex_colors(mesh: bpy.types.Mesh) -> int:
"""Return the amount of vertex color attributes of the given mesh.""" """Return the amount of vertex color attributes of the given mesh."""
num = 0 if bpy.app.version >= (3, 0, 0):
for attr in mesh.attributes: num = 0
if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): for attr in mesh.attributes:
if attr.domain == 'CORNER': if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'):
num += 1 if attr.domain == 'CORNER':
else: num += 1
log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') else:
log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"')
return num return num
else:
return len(mesh.vertex_colors)
@staticmethod @staticmethod
def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[bpy.types.Attribute]: def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[VertexColorType]:
"""Return the n-th vertex color attribute from the given mesh, """Return the n-th vertex color attribute from the given mesh,
ignoring all other attribute types and unsupported domains. ignoring all other attribute types and unsupported domains.
""" """
i = 0 if bpy.app.version >= (3, 0, 0):
for attr in mesh.attributes: i = 0
if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): for attr in mesh.attributes:
if attr.domain != 'CORNER': if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'):
log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') if attr.domain != 'CORNER':
continue log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"')
if i == n: continue
return attr if i == n:
i += 1 return attr
return None i += 1
return None
else:
if 0 <= n < len(mesh.vertex_colors):
return mesh.vertex_colors[n]
return None
@staticmethod @staticmethod
def check_uv_precision(mesh: bpy.types.Mesh, uv_max_dim: float, max_dim_uvmap: bpy.types.MeshUVLoopLayer, invscale_tex: float): def check_uv_precision(mesh: bpy.types.Mesh, uv_max_dim: float, max_dim_uvmap: bpy.types.MeshUVLoopLayer, invscale_tex: float):
@ -1705,6 +1741,7 @@ class LeenkxExporter:
tangdata = np.array(tangdata, dtype='<i2') tangdata = np.array(tangdata, dtype='<i2')
# Output # Output
o['sorting_index'] = bobject.lnx_sorting_index
o['vertex_arrays'] = [] o['vertex_arrays'] = []
o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' }) o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' })
o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' }) o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' })
@ -1957,7 +1994,7 @@ class LeenkxExporter:
if bobject.parent is None or bobject.parent.name not in collection.objects: if bobject.parent is None or bobject.parent.name not in collection.objects:
asset_name = lnx.utils.asset_name(bobject) asset_name = lnx.utils.asset_name(bobject)
if collection.library: if collection.library and not collection.name in self.scene.collection.children:
# Add external linked objects # Add external linked objects
# Iron differentiates objects based on their names, # Iron differentiates objects based on their names,
# so errors will happen if two objects with the # so errors will happen if two objects with the
@ -2186,6 +2223,9 @@ class LeenkxExporter:
elif material.lnx_cull_mode != 'clockwise': elif material.lnx_cull_mode != 'clockwise':
o['override_context'] = {} o['override_context'] = {}
o['override_context']['cull_mode'] = material.lnx_cull_mode o['override_context']['cull_mode'] = material.lnx_cull_mode
if material.lnx_compare_mode != 'less':
o['override_context'] = {}
o['override_context']['compare_mode'] = material.lnx_compare_mode
o['contexts'] = [] o['contexts'] = []
@ -2282,12 +2322,12 @@ class LeenkxExporter:
self.output['particle_datas'] = [] self.output['particle_datas'] = []
for particleRef in self.particle_system_array.items(): for particleRef in self.particle_system_array.items():
padd = False; padd = False;
for obj in self.output['objects']: for obj in bpy.data.objects:
if 'particle_refs' in obj: for mod in obj.modifiers:
for pref in obj['particle_refs']: if mod.type == 'PARTICLE_SYSTEM':
if pref['particle'] == particleRef[1]["structName"]: if mod.particle_system.settings.name == particleRef[1]["structName"]:
if bpy.data.objects[obj['name']].modifiers[pref['name']].show_render == True: if mod.show_render:
padd = True; padd = True
if not padd: if not padd:
continue; continue;
psettings = particleRef[0] psettings = particleRef[0]
@ -2308,6 +2348,7 @@ class LeenkxExporter:
'name': particleRef[1]["structName"], 'name': particleRef[1]["structName"],
'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR
'auto_start': psettings.lnx_auto_start, 'auto_start': psettings.lnx_auto_start,
'dynamic_emitter': psettings.lnx_dynamic_emitter,
'is_unique': psettings.lnx_is_unique, 'is_unique': psettings.lnx_is_unique,
'loop': psettings.lnx_loop, 'loop': psettings.lnx_loop,
# Emission # Emission
@ -2373,7 +2414,7 @@ class LeenkxExporter:
world = self.scene.world world = self.scene.world
if world is not None: if world is not None:
world_name = lnx.utils.safestr(world.name) world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name)
if world_name not in self.world_array: if world_name not in self.world_array:
self.world_array.append(world_name) self.world_array.append(world_name)
@ -2522,12 +2563,12 @@ class LeenkxExporter:
if collection.name.startswith(('RigidBodyWorld', 'Trait|')): if collection.name.startswith(('RigidBodyWorld', 'Trait|')):
continue continue
if self.scene.user_of_id(collection) or collection.library or collection in self.referenced_collections: if self.scene.user_of_id(collection) or collection in self.referenced_collections:
self.export_collection(collection) self.export_collection(collection)
if not LeenkxExporter.option_mesh_only: if not LeenkxExporter.option_mesh_only:
if self.scene.camera is not None: if self.scene.camera is not None:
self.output['camera_ref'] = self.scene.camera.name self.output['camera_ref'] = lnx.utils.asset_name(self.scene.camera) if self.scene.library else self.scene.camera.name
else: else:
if self.scene.name == lnx.utils.get_project_scene_name(): if self.scene.name == lnx.utils.get_project_scene_name():
log.warn(f'Scene "{self.scene.name}" is missing a camera') log.warn(f'Scene "{self.scene.name}" is missing a camera')
@ -2551,7 +2592,7 @@ class LeenkxExporter:
self.export_tilesheets() self.export_tilesheets()
if self.scene.world is not None: if self.scene.world is not None:
self.output['world_ref'] = lnx.utils.safestr(self.scene.world.name) self.output['world_ref'] = lnx.utils.safestr(lnx.utils.asset_name(self.scene.world) if self.scene.world.library else self.scene.world.name)
if self.scene.use_gravity: if self.scene.use_gravity:
self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]] self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]]
@ -2821,6 +2862,18 @@ class LeenkxExporter:
body_params['linearDeactivationThreshold'] = deact_lv body_params['linearDeactivationThreshold'] = deact_lv
body_params['angularDeactivationThrshold'] = deact_av body_params['angularDeactivationThrshold'] = deact_av
body_params['deactivationTime'] = deact_time body_params['deactivationTime'] = deact_time
# New velocity limit properties
body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min
body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max
body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min
body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max
# New lock properties
body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x
body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y
body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z
body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x
body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y
body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z
body_flags = {} body_flags = {}
body_flags['animated'] = rb.kinematic body_flags['animated'] = rb.kinematic
body_flags['trigger'] = bobject.lnx_rb_trigger body_flags['trigger'] = bobject.lnx_rb_trigger
@ -2981,7 +3034,10 @@ class LeenkxExporter:
# mesh = obj.data # mesh = obj.data
# for face in mesh.faces: # for face in mesh.faces:
# face.v.reverse() # 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) # if bpy.app.version[0] >= 4:
# bpy.ops.wm.obj_export(override, use_selection=True, filepath=nav_filepath, check_existing=False, use_normals=False, use_uvs=False, use_materials=False)
# else:
# 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 # bobject.scale.y *= -1
armature = bobject.find_armature() armature = bobject.find_armature()
apply_modifiers = not armature apply_modifiers = not armature
@ -3020,6 +3076,8 @@ class LeenkxExporter:
if trait_prop.type.endswith("Object"): if trait_prop.type.endswith("Object"):
value = lnx.utils.asset_name(trait_prop.value_object) value = lnx.utils.asset_name(trait_prop.value_object)
elif trait_prop.type == "TSceneFormat":
value = lnx.utils.asset_name(trait_prop.value_scene)
else: else:
value = trait_prop.get_value() value = trait_prop.get_value()
@ -3050,7 +3108,18 @@ class LeenkxExporter:
rbw = self.scene.rigidbody_world rbw = self.scene.rigidbody_world
if rbw is not None and rbw.enabled: if rbw is not None and rbw.enabled:
out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)] if hasattr(rbw, 'substeps_per_frame'):
substeps = str(rbw.substeps_per_frame)
elif hasattr(rbw, 'steps_per_second'):
scene_fps = bpy.context.scene.render.fps
substeps_per_frame = rbw.steps_per_second / scene_fps
substeps = str(int(round(substeps_per_frame)))
else:
print("WARNING: Physics rigid body world cannot determine steps/substeps. Please report this for further investigation.")
print("Setting steps to 10 [ low ]")
substeps = '10'
out_trait['parameters'] = [str(rbw.time_scale), substeps, str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)]
if phys_pkg == 'bullet' or phys_pkg == 'oimo': if phys_pkg == 'bullet' or phys_pkg == 'oimo':
debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0 debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0
@ -3337,7 +3406,7 @@ class LeenkxExporter:
if mobile_mat: if mobile_mat:
lnx_radiance = False lnx_radiance = False
out_probe = {'name': world.name} out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name}
if lnx_irradiance: if lnx_irradiance:
ext = '' if wrd.lnx_minimize else '.json' ext = '' if wrd.lnx_minimize else '.json'
out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext

View File

@ -1,445 +1,450 @@
""" """
Exports smaller geometry but is slower. Exports smaller geometry but is slower.
To be replaced with https://github.com/zeux/meshoptimizer To be replaced with https://github.com/zeux/meshoptimizer
""" """
from typing import Optional from typing import Optional, TYPE_CHECKING
import bpy
import bpy from mathutils import Vector
from mathutils import Vector import numpy as np
import numpy as np
import lnx.utils
import lnx.utils from lnx import log
from lnx import log
if lnx.is_reload(__name__):
if lnx.is_reload(__name__): log = lnx.reload_module(log)
log = lnx.reload_module(log) lnx.utils = lnx.reload_module(lnx.utils)
lnx.utils = lnx.reload_module(lnx.utils) else:
else: lnx.enable_reload(__name__)
lnx.enable_reload(__name__)
class Vertex:
class Vertex: __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index")
__slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index")
def __init__(
def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): self,
self.vertex_index = loop.vertex_index mesh: 'bpy.types.Mesh',
loop_idx = loop.index loop: 'bpy.types.MeshLoop',
self.co = mesh.vertices[self.vertex_index].co[:] vcol0: Optional['bpy.types.MeshLoopColor' if bpy.app.version < (3, 0, 0) else 'bpy.types.Attribute']
self.normal = loop.normal[:] ):
self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) self.vertex_index = loop.vertex_index
self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] loop_idx = loop.index
self.loop_indices = [loop_idx] self.co = mesh.vertices[self.vertex_index].co[:]
self.index = 0 self.normal = loop.normal[:]
self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers)
def __hash__(self): self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:]
return hash((self.co, self.normal, self.uvs)) self.loop_indices = [loop_idx]
self.index = 0
def __eq__(self, other):
eq = ( def __hash__(self):
(self.co == other.co) and return hash((self.co, self.normal, self.uvs))
(self.normal == other.normal) and
(self.uvs == other.uvs) and def __eq__(self, other):
(self.col == other.col) eq = (
) (self.co == other.co) and
if eq: (self.normal == other.normal) and
indices = self.loop_indices + other.loop_indices (self.uvs == other.uvs) and
self.loop_indices = indices (self.col == other.col)
other.loop_indices = indices )
return eq if eq:
indices = self.loop_indices + other.loop_indices
self.loop_indices = indices
def calc_tangents(posa, nora, uva, ias, scale_pos): other.loop_indices = indices
num_verts = int(len(posa) / 4) return eq
tangents = np.empty(num_verts * 3, dtype='<f4')
# bitangents = np.empty(num_verts * 3, dtype='<f4')
for ar in ias: def calc_tangents(posa, nora, uva, ias, scale_pos):
ia = ar['values'] num_verts = int(len(posa) / 4)
num_tris = int(len(ia) / 3) tangents = np.empty(num_verts * 3, dtype='<f4')
for i in range(0, num_tris): # bitangents = np.empty(num_verts * 3, dtype='<f4')
i0 = ia[i * 3 ] for ar in ias:
i1 = ia[i * 3 + 1] ia = ar['values']
i2 = ia[i * 3 + 2] num_tris = int(len(ia) / 3)
v0 = Vector((posa[i0 * 4], posa[i0 * 4 + 1], posa[i0 * 4 + 2])) for i in range(0, num_tris):
v1 = Vector((posa[i1 * 4], posa[i1 * 4 + 1], posa[i1 * 4 + 2])) i0 = ia[i * 3 ]
v2 = Vector((posa[i2 * 4], posa[i2 * 4 + 1], posa[i2 * 4 + 2])) i1 = ia[i * 3 + 1]
uv0 = Vector((uva[i0 * 2], uva[i0 * 2 + 1])) i2 = ia[i * 3 + 2]
uv1 = Vector((uva[i1 * 2], uva[i1 * 2 + 1])) v0 = Vector((posa[i0 * 4], posa[i0 * 4 + 1], posa[i0 * 4 + 2]))
uv2 = Vector((uva[i2 * 2], uva[i2 * 2 + 1])) v1 = Vector((posa[i1 * 4], posa[i1 * 4 + 1], posa[i1 * 4 + 2]))
v2 = Vector((posa[i2 * 4], posa[i2 * 4 + 1], posa[i2 * 4 + 2]))
deltaPos1 = v1 - v0 uv0 = Vector((uva[i0 * 2], uva[i0 * 2 + 1]))
deltaPos2 = v2 - v0 uv1 = Vector((uva[i1 * 2], uva[i1 * 2 + 1]))
deltaUV1 = uv1 - uv0 uv2 = Vector((uva[i2 * 2], uva[i2 * 2 + 1]))
deltaUV2 = uv2 - uv0
d = (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x) deltaPos1 = v1 - v0
if d != 0: deltaPos2 = v2 - v0
r = 1.0 / d deltaUV1 = uv1 - uv0
else: deltaUV2 = uv2 - uv0
r = 1.0 d = (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x)
tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r if d != 0:
# bitangent = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r r = 1.0 / d
else:
tangents[i0 * 3 ] += tangent.x r = 1.0
tangents[i0 * 3 + 1] += tangent.y tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r
tangents[i0 * 3 + 2] += tangent.z # bitangent = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r
tangents[i1 * 3 ] += tangent.x
tangents[i1 * 3 + 1] += tangent.y tangents[i0 * 3 ] += tangent.x
tangents[i1 * 3 + 2] += tangent.z tangents[i0 * 3 + 1] += tangent.y
tangents[i2 * 3 ] += tangent.x tangents[i0 * 3 + 2] += tangent.z
tangents[i2 * 3 + 1] += tangent.y tangents[i1 * 3 ] += tangent.x
tangents[i2 * 3 + 2] += tangent.z tangents[i1 * 3 + 1] += tangent.y
# bitangents[i0 * 3 ] += bitangent.x tangents[i1 * 3 + 2] += tangent.z
# bitangents[i0 * 3 + 1] += bitangent.y tangents[i2 * 3 ] += tangent.x
# bitangents[i0 * 3 + 2] += bitangent.z tangents[i2 * 3 + 1] += tangent.y
# bitangents[i1 * 3 ] += bitangent.x tangents[i2 * 3 + 2] += tangent.z
# bitangents[i1 * 3 + 1] += bitangent.y # bitangents[i0 * 3 ] += bitangent.x
# bitangents[i1 * 3 + 2] += bitangent.z # bitangents[i0 * 3 + 1] += bitangent.y
# bitangents[i2 * 3 ] += bitangent.x # bitangents[i0 * 3 + 2] += bitangent.z
# bitangents[i2 * 3 + 1] += bitangent.y # bitangents[i1 * 3 ] += bitangent.x
# bitangents[i2 * 3 + 2] += bitangent.z # bitangents[i1 * 3 + 1] += bitangent.y
# Orthogonalize # bitangents[i1 * 3 + 2] += bitangent.z
for i in range(0, num_verts): # bitangents[i2 * 3 ] += bitangent.x
t = Vector((tangents[i * 3], tangents[i * 3 + 1], tangents[i * 3 + 2])) # bitangents[i2 * 3 + 1] += bitangent.y
# b = Vector((bitangents[i * 3], bitangents[i * 3 + 1], bitangents[i * 3 + 2])) # bitangents[i2 * 3 + 2] += bitangent.z
n = Vector((nora[i * 2], nora[i * 2 + 1], posa[i * 4 + 3] / scale_pos)) # Orthogonalize
v = t - n * n.dot(t) for i in range(0, num_verts):
v.normalize() t = Vector((tangents[i * 3], tangents[i * 3 + 1], tangents[i * 3 + 2]))
# Calculate handedness # b = Vector((bitangents[i * 3], bitangents[i * 3 + 1], bitangents[i * 3 + 2]))
# cnv = n.cross(v) n = Vector((nora[i * 2], nora[i * 2 + 1], posa[i * 4 + 3] / scale_pos))
# if cnv.dot(b) < 0.0: v = t - n * n.dot(t)
# v = v * -1.0 v.normalize()
tangents[i * 3 ] = v.x # Calculate handedness
tangents[i * 3 + 1] = v.y # cnv = n.cross(v)
tangents[i * 3 + 2] = v.z # if cnv.dot(b) < 0.0:
return tangents # v = v * -1.0
tangents[i * 3 ] = v.x
tangents[i * 3 + 1] = v.y
def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Object, o, has_armature=False): tangents[i * 3 + 2] = v.z
if bpy.app.version < (4, 1, 0): return tangents
export_mesh.calc_normals_split()
else:
updated_normals = export_mesh.corner_normals def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Object, o, has_armature=False):
# exportMesh.calc_loop_triangles() if bpy.app.version < (4, 1, 0):
vcol0 = self.get_nth_vertex_colors(export_mesh, 0) export_mesh.calc_normals_split()
vert_list = {Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}.keys() else:
num_verts = len(vert_list) updated_normals = export_mesh.corner_normals
num_uv_layers = len(export_mesh.uv_layers) # exportMesh.calc_loop_triangles()
# Check if shape keys were exported vcol0 = self.get_nth_vertex_colors(export_mesh, 0)
has_morph_target = self.get_shape_keys(bobject.data) vert_list = {Vertex(export_mesh, loop, vcol0): 0 for loop in export_mesh.loops}.keys()
if has_morph_target: num_verts = len(vert_list)
# Shape keys UV are exported separately, so reduce UV count by 1 num_uv_layers = len(export_mesh.uv_layers)
num_uv_layers -= 1 # Check if shape keys were exported
morph_uv_index = self.get_morph_uv_index(bobject.data) has_morph_target = self.get_shape_keys(bobject.data)
has_tex = self.get_export_uvs(export_mesh) and num_uv_layers > 0 if has_morph_target:
if self.has_baked_material(bobject, export_mesh.materials): # Shape keys UV are exported separately, so reduce UV count by 1
has_tex = True num_uv_layers -= 1
has_tex1 = has_tex and num_uv_layers > 1 morph_uv_index = self.get_morph_uv_index(bobject.data)
num_colors = self.get_num_vertex_colors(export_mesh) has_tex = self.get_export_uvs(export_mesh) or num_uv_layers > 0 # TODO FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects.
has_col = self.get_export_vcols(export_mesh) and num_colors > 0 if self.has_baked_material(bobject, export_mesh.materials):
has_tang = self.has_tangents(export_mesh) has_tex = True
has_tex1 = has_tex and num_uv_layers > 1
pdata = np.empty(num_verts * 4, dtype='<f4') # p.xyz, n.z num_colors = self.get_num_vertex_colors(export_mesh)
ndata = np.empty(num_verts * 2, dtype='<f4') # n.xy has_col = self.get_export_vcols(export_mesh) and num_colors > 0
if has_tex or has_morph_target: has_tang = self.has_tangents(export_mesh)
uv_layers = export_mesh.uv_layers
maxdim = 1.0 pdata = np.empty(num_verts * 4, dtype='<f4') # p.xyz, n.z
maxdim_uvlayer = None ndata = np.empty(num_verts * 2, dtype='<f4') # n.xy
if has_tex: if has_tex or has_morph_target:
t0map = 0 # Get active uvmap uv_layers = export_mesh.uv_layers
t0data = np.empty(num_verts * 2, dtype='<f4') maxdim = 1.0
if uv_layers is not None: maxdim_uvlayer = None
if 'UVMap_baked' in uv_layers: if has_tex:
for i in range(0, len(uv_layers)): t0map = 0 # Get active uvmap
if uv_layers[i].name == 'UVMap_baked': t0data = np.empty(num_verts * 2, dtype='<f4')
t0map = i if uv_layers is not None:
break if 'UVMap_baked' in uv_layers:
else: for i in range(0, len(uv_layers)):
for i in range(0, len(uv_layers)): if uv_layers[i].name == 'UVMap_baked':
if uv_layers[i].active_render and uv_layers[i].name != 'UVMap_shape_key': t0map = i
t0map = i break
break else:
if has_tex1: for i in range(0, len(uv_layers)):
for i in range(0, len(uv_layers)): if uv_layers[i].active_render and uv_layers[i].name != 'UVMap_shape_key':
# Not UVMap 0 t0map = i
if i != t0map: break
# Not Shape Key UVMap if has_tex1:
if has_morph_target and uv_layers[i].name == 'UVMap_shape_key': for i in range(0, len(uv_layers)):
continue # Not UVMap 0
# Neither UVMap 0 Nor Shape Key Map if i != t0map:
t1map = i # Not Shape Key UVMap
t1data = np.empty(num_verts * 2, dtype='<f4') if has_morph_target and uv_layers[i].name == 'UVMap_shape_key':
# Scale for packed coords continue
lay0 = uv_layers[t0map] # Neither UVMap 0 Nor Shape Key Map
maxdim_uvlayer = lay0 t1map = i
for v in lay0.data: t1data = np.empty(num_verts * 2, dtype='<f4')
if abs(v.uv[0]) > maxdim: # Scale for packed coords
maxdim = abs(v.uv[0]) lay0 = uv_layers[t0map]
if abs(v.uv[1]) > maxdim: maxdim_uvlayer = lay0
maxdim = abs(v.uv[1]) for v in lay0.data:
if has_tex1: if abs(v.uv[0]) > maxdim:
lay1 = uv_layers[t1map] maxdim = abs(v.uv[0])
for v in lay1.data: if abs(v.uv[1]) > maxdim:
if abs(v.uv[0]) > maxdim: maxdim = abs(v.uv[1])
maxdim = abs(v.uv[0]) if has_tex1:
maxdim_uvlayer = lay1 lay1 = uv_layers[t1map]
if abs(v.uv[1]) > maxdim: for v in lay1.data:
maxdim = abs(v.uv[1]) if abs(v.uv[0]) > maxdim:
maxdim_uvlayer = lay1 maxdim = abs(v.uv[0])
if has_morph_target: maxdim_uvlayer = lay1
morph_data = np.empty(num_verts * 2, dtype='<f4') if abs(v.uv[1]) > maxdim:
lay2 = uv_layers[morph_uv_index] maxdim = abs(v.uv[1])
for v in lay2.data: maxdim_uvlayer = lay1
if abs(v.uv[0]) > maxdim: if has_morph_target:
maxdim = abs(v.uv[0]) morph_data = np.empty(num_verts * 2, dtype='<f4')
maxdim_uvlayer = lay2 lay2 = uv_layers[morph_uv_index]
if abs(v.uv[1]) > maxdim: for v in lay2.data:
maxdim = abs(v.uv[1]) if abs(v.uv[0]) > maxdim:
maxdim_uvlayer = lay2 maxdim = abs(v.uv[0])
if maxdim > 1: maxdim_uvlayer = lay2
o['scale_tex'] = maxdim if abs(v.uv[1]) > maxdim:
invscale_tex = (1 / o['scale_tex']) * 32767 maxdim = abs(v.uv[1])
else: maxdim_uvlayer = lay2
invscale_tex = 1 * 32767 if maxdim > 1:
self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) o['scale_tex'] = maxdim
invscale_tex = (1 / o['scale_tex']) * 32767
if has_col: else:
cdata = np.empty(num_verts * 3, dtype='<f4') invscale_tex = 1 * 32767
self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex)
# Save aabb
self.calc_aabb(bobject) if has_col:
cdata = np.empty(num_verts * 3, dtype='<f4')
# Scale for packed coords
maxdim = max(bobject.data.lnx_aabb[0], max(bobject.data.lnx_aabb[1], bobject.data.lnx_aabb[2])) # Save aabb
if maxdim > 2: self.calc_aabb(bobject)
o['scale_pos'] = maxdim / 2
else: # Scale for packed coords
o['scale_pos'] = 1.0 maxdim = max(bobject.data.lnx_aabb[0], max(bobject.data.lnx_aabb[1], bobject.data.lnx_aabb[2]))
if has_armature: # Allow up to 2x bigger bounds for skinned mesh if maxdim > 2:
o['scale_pos'] *= 2.0 o['scale_pos'] = maxdim / 2
else:
scale_pos = o['scale_pos'] o['scale_pos'] = 1.0
invscale_pos = (1 / scale_pos) * 32767 if has_armature: # Allow up to 2x bigger bounds for skinned mesh
o['scale_pos'] *= 2.0
# Make arrays
for i, v in enumerate(vert_list): scale_pos = o['scale_pos']
v.index = i invscale_pos = (1 / scale_pos) * 32767
co = v.co
normal = v.normal # Make arrays
i4 = i * 4 for i, v in enumerate(vert_list):
i2 = i * 2 v.index = i
pdata[i4 ] = co[0] co = v.co
pdata[i4 + 1] = co[1] normal = v.normal
pdata[i4 + 2] = co[2] i4 = i * 4
pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale i2 = i * 2
ndata[i2 ] = normal[0] pdata[i4 ] = co[0]
ndata[i2 + 1] = normal[1] pdata[i4 + 1] = co[1]
if has_tex: pdata[i4 + 2] = co[2]
uv = v.uvs[t0map] pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale
t0data[i2 ] = uv[0] ndata[i2 ] = normal[0]
t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y ndata[i2 + 1] = normal[1]
if has_tex1: if has_tex:
uv = v.uvs[t1map] uv = v.uvs[t0map]
t1data[i2 ] = uv[0] t0data[i2 ] = uv[0]
t1data[i2 + 1] = 1.0 - uv[1] t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y
if has_morph_target: if has_tex1:
uv = v.uvs[morph_uv_index] uv = v.uvs[t1map]
morph_data[i2 ] = uv[0] t1data[i2 ] = uv[0]
morph_data[i2 + 1] = 1.0 - uv[1] t1data[i2 + 1] = 1.0 - uv[1]
if has_col: if has_morph_target:
i3 = i * 3 uv = v.uvs[morph_uv_index]
cdata[i3 ] = v.col[0] morph_data[i2 ] = uv[0]
cdata[i3 + 1] = v.col[1] morph_data[i2 + 1] = 1.0 - uv[1]
cdata[i3 + 2] = v.col[2] if has_col:
i3 = i * 3
# Indices cdata[i3 ] = v.col[0]
# Create dict for every material slot cdata[i3 + 1] = v.col[1]
prims = {ma.name if ma else '': [] for ma in export_mesh.materials} cdata[i3 + 2] = v.col[2]
v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials}
if not prims: # Indices
# No materials # Create dict for every material slot
prims = {'': []} prims = {ma.name if ma else '': [] for ma in export_mesh.materials}
v_maps = {'': []} v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials}
if not prims:
# Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list # No materials
vert_dict = {i : v for v in vert_list for i in v.loop_indices} prims = {'': []}
# For each polygon in a mesh v_maps = {'': []}
for poly in export_mesh.polygons:
# Index of the first loop of this polygon # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list
first = poly.loop_start vert_dict = {i : v for v in vert_list for i in v.loop_indices}
# No materials assigned # For each polygon in a mesh
if len(export_mesh.materials) == 0: for poly in export_mesh.polygons:
# Get prim # Index of the first loop of this polygon
prim = prims[''] first = poly.loop_start
v_map = v_maps[''] # No materials assigned
else: if len(export_mesh.materials) == 0:
# First material # Get prim
mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] prim = prims['']
# Get prim for this material v_map = v_maps['']
prim = prims[mat.name if mat else ''] else:
v_map = v_maps[mat.name if mat else ''] # First material
# List of indices for each loop_index belonging to this polygon mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)]
indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] # Get prim for this material
v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] prim = prims[mat.name if mat else '']
v_map = v_maps[mat.name if mat else '']
# If 3 loops per polygon (Triangle?) # List of indices for each loop_index belonging to this polygon
if poly.loop_total == 3: indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)]
prim += indices v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)]
v_map += v_indices
# If > 3 loops per polygon (Non-Triangular?) # If 3 loops per polygon (Triangle?)
elif poly.loop_total > 3: if poly.loop_total == 3:
for i in range(poly.loop_total-2): prim += indices
prim += (indices[-1], indices[i], indices[i + 1]) v_map += v_indices
v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) # If > 3 loops per polygon (Non-Triangular?)
elif poly.loop_total > 3:
# Write indices for i in range(poly.loop_total-2):
o['index_arrays'] = [] prim += (indices[-1], indices[i], indices[i + 1])
for mat, prim in prims.items(): v_map += (v_indices[-1], v_indices[i], v_indices[i + 1])
idata = [0] * len(prim)
v_map_data = [0] * len(prim) # Write indices
v_map_sub = v_maps[mat] o['index_arrays'] = []
for i, v in enumerate(prim): for mat, prim in prims.items():
idata[i] = v idata = [0] * len(prim)
v_map_data[i] = v_map_sub[i] v_map_data = [0] * len(prim)
if len(idata) == 0: # No face assigned v_map_sub = v_maps[mat]
continue for i, v in enumerate(prim):
ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} idata[i] = v
# Find material index for multi-mat mesh v_map_data[i] = v_map_sub[i]
if len(export_mesh.materials) > 1: if len(idata) == 0: # No face assigned
for i in range(0, len(export_mesh.materials)): continue
if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data}
(export_mesh.materials[i] is None and mat == ''): # Default material for empty slots # Find material index for multi-mat mesh
ia['material'] = i if len(export_mesh.materials) > 1:
break for i in range(0, len(export_mesh.materials)):
o['index_arrays'].append(ia) 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
if has_tang: ia['material'] = i
tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) break
o['index_arrays'].append(ia)
pdata *= invscale_pos
ndata *= 32767 if has_tang:
pdata = np.array(pdata, dtype='<i2') tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos)
ndata = np.array(ndata, dtype='<i2')
if has_tex: pdata *= invscale_pos
t0data *= invscale_tex ndata *= 32767
t0data = np.array(t0data, dtype='<i2') pdata = np.array(pdata, dtype='<i2')
if has_tex1: ndata = np.array(ndata, dtype='<i2')
t1data *= invscale_tex if has_tex:
t1data = np.array(t1data, dtype='<i2') t0data *= invscale_tex
if has_morph_target: t0data = np.array(t0data, dtype='<i2')
morph_data *= invscale_tex if has_tex1:
morph_data = np.array(morph_data, dtype='<i2') t1data *= invscale_tex
if has_col: t1data = np.array(t1data, dtype='<i2')
cdata *= 32767 if has_morph_target:
cdata = np.array(cdata, dtype='<i2') morph_data *= invscale_tex
if has_tang: morph_data = np.array(morph_data, dtype='<i2')
tangdata *= 32767 if has_col:
tangdata = np.array(tangdata, dtype='<i2') cdata *= 32767
cdata = np.array(cdata, dtype='<i2')
# Output if has_tang:
o['vertex_arrays'] = [] tangdata *= 32767
o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' }) tangdata = np.array(tangdata, dtype='<i2')
o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' })
if has_tex: # Output
o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data, 'data': 'short2norm' }) o['sorting_index'] = bobject.lnx_sorting_index
if has_tex1: o['vertex_arrays'] = []
o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data, 'data': 'short2norm' }) o['vertex_arrays'].append({ 'attrib': 'pos', 'values': pdata, 'data': 'short4norm' })
if has_morph_target: o['vertex_arrays'].append({ 'attrib': 'nor', 'values': ndata, 'data': 'short2norm' })
o['vertex_arrays'].append({ 'attrib': 'morph', 'values': morph_data, 'data': 'short2norm' }) if has_tex:
if has_col: o['vertex_arrays'].append({ 'attrib': 'tex', 'values': t0data, 'data': 'short2norm' })
o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata, 'data': 'short4norm', 'padding': 1 }) if has_tex1:
if has_tang: o['vertex_arrays'].append({ 'attrib': 'tex1', 'values': t1data, 'data': 'short2norm' })
o['vertex_arrays'].append({ 'attrib': 'tang', 'values': tangdata, 'data': 'short4norm', 'padding': 1 }) if has_morph_target:
o['vertex_arrays'].append({ 'attrib': 'morph', 'values': morph_data, 'data': 'short2norm' })
return vert_list if has_col:
o['vertex_arrays'].append({ 'attrib': 'col', 'values': cdata, 'data': 'short4norm', 'padding': 1 })
def export_skin(self, bobject, armature, vert_list, o): if has_tang:
# This function exports all skinning data, which includes the skeleton o['vertex_arrays'].append({ 'attrib': 'tang', 'values': tangdata, 'data': 'short4norm', 'padding': 1 })
# and per-vertex bone influence data
oskin = {} return vert_list
o['skin'] = oskin
def export_skin(self, bobject, armature, vert_list, o):
# Write the skin bind pose transform # This function exports all skinning data, which includes the skeleton
otrans = {} # and per-vertex bone influence data
oskin['transform'] = otrans oskin = {}
otrans['values'] = self.write_matrix(bobject.matrix_world) o['skin'] = oskin
# Write the bone object reference array # Write the skin bind pose transform
oskin['bone_ref_array'] = [] otrans = {}
oskin['bone_len_array'] = [] oskin['transform'] = otrans
otrans['values'] = self.write_matrix(bobject.matrix_world)
bone_array = armature.data.bones
bone_count = len(bone_array) # Write the bone object reference array
rpdat = lnx.utils.get_rp() oskin['bone_ref_array'] = []
max_bones = rpdat.lnx_skin_max_bones oskin['bone_len_array'] = []
if bone_count > 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 - Leenkx Render Props - Max Bones') bone_array = armature.data.bones
bone_count = len(bone_array)
for i in range(bone_count): rpdat = lnx.utils.get_rp()
boneRef = self.find_bone(bone_array[i].name) max_bones = rpdat.lnx_skin_max_bones
if boneRef: if bone_count > max_bones:
oskin['bone_ref_array'].append(boneRef[1]["structName"]) log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Leenkx Render Props - Max Bones')
oskin['bone_len_array'].append(bone_array[i].length)
else: for i in range(bone_count):
oskin['bone_ref_array'].append("") boneRef = self.find_bone(bone_array[i].name)
oskin['bone_len_array'].append(0.0) if boneRef:
oskin['bone_ref_array'].append(boneRef[1]["structName"])
# Write the bind pose transform array oskin['bone_len_array'].append(bone_array[i].length)
oskin['transformsI'] = [] else:
for i in range(bone_count): oskin['bone_ref_array'].append("")
skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() oskin['bone_len_array'].append(0.0)
skeletonI = (skeletonI @ bobject.matrix_world)
oskin['transformsI'].append(self.write_matrix(skeletonI)) # Write the bind pose transform array
oskin['transformsI'] = []
# Export the per-vertex bone influence data for i in range(bone_count):
group_remap = [] skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe()
for group in bobject.vertex_groups: skeletonI = (skeletonI @ bobject.matrix_world)
for i in range(bone_count): oskin['transformsI'].append(self.write_matrix(skeletonI))
if bone_array[i].name == group.name:
group_remap.append(i) # Export the per-vertex bone influence data
break group_remap = []
else: for group in bobject.vertex_groups:
group_remap.append(-1) for i in range(bone_count):
if bone_array[i].name == group.name:
bone_count_array = np.empty(len(vert_list), dtype='<i2') group_remap.append(i)
bone_index_array = np.empty(len(vert_list) * 4, dtype='<i2') break
bone_weight_array = np.empty(len(vert_list) * 4, dtype='<i2') else:
group_remap.append(-1)
vertices = bobject.data.vertices
count = 0 bone_count_array = np.empty(len(vert_list), dtype='<i2')
for index, v in enumerate(vert_list): bone_index_array = np.empty(len(vert_list) * 4, dtype='<i2')
bone_count = 0 bone_weight_array = np.empty(len(vert_list) * 4, dtype='<i2')
total_weight = 0.0
bone_values = [] vertices = bobject.data.vertices
for g in vertices[v.vertex_index].groups: count = 0
bone_index = group_remap[g.group] for index, v in enumerate(vert_list):
bone_weight = g.weight bone_count = 0
if bone_index >= 0: #and bone_weight != 0.0: total_weight = 0.0
bone_values.append((bone_weight, bone_index)) bone_values = []
total_weight += bone_weight for g in vertices[v.vertex_index].groups:
bone_count += 1 bone_index = group_remap[g.group]
bone_weight = g.weight
if bone_count > 4: if bone_index >= 0: #and bone_weight != 0.0:
bone_count = 4 bone_values.append((bone_weight, bone_index))
bone_values.sort(reverse=True) total_weight += bone_weight
bone_values = bone_values[:4] bone_count += 1
bone_count_array[index] = bone_count if bone_count > 4:
for bv in bone_values: bone_count = 4
bone_weight_array[count] = bv[0] * 32767 bone_values.sort(reverse=True)
bone_index_array[count] = bv[1] bone_values = bone_values[:4]
count += 1
bone_count_array[index] = bone_count
if total_weight not in (0.0, 1.0): for bv in bone_values:
normalizer = 1.0 / total_weight bone_weight_array[count] = bv[0] * 32767
for i in range(bone_count): bone_index_array[count] = bv[1]
bone_weight_array[count - i - 1] *= normalizer count += 1
oskin['bone_count_array'] = bone_count_array if total_weight not in (0.0, 1.0):
oskin['bone_index_array'] = bone_index_array[:count] normalizer = 1.0 / total_weight
oskin['bone_weight_array'] = bone_weight_array[:count] for i in range(bone_count):
bone_weight_array[count - i - 1] *= normalizer
# Bone constraints
for bone in armature.pose.bones: oskin['bone_count_array'] = bone_count_array
if len(bone.constraints) > 0: oskin['bone_index_array'] = bone_index_array[:count]
if 'constraints' not in oskin: oskin['bone_weight_array'] = bone_weight_array[:count]
oskin['constraints'] = []
self.add_constraints(bone, oskin, bone=True) # 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)

View File

@ -2,7 +2,10 @@ import importlib
import os import os
import queue import queue
import sys import sys
import threading
import time
import types import types
from typing import Dict, Tuple, Callable, Set
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
@ -30,6 +33,10 @@ if lnx.is_reload(__name__):
else: else:
lnx.enable_reload(__name__) lnx.enable_reload(__name__)
# Module-level storage for active threads (eliminates re-queuing overhead)
_active_threads: Dict[threading.Thread, Callable] = {}
_last_poll_time = 0.0
_consecutive_empty_polls = 0
@persistent @persistent
def on_depsgraph_update_post(self): def on_depsgraph_update_post(self):
@ -91,7 +98,7 @@ def on_operator_post(operator_id: str) -> None:
target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask
elif operator_id == "NODE_OT_new_node_tree": elif operator_id == "NODE_OT_new_node_tree":
if bpy.context.space_data.tree_type == lnx.nodes_logic.LnxLogicTree.bl_idname: if bpy.context.space_data is not None and bpy.context.space_data.tree_type == lnx.nodes_logic.LnxLogicTree.bl_idname:
# In Blender 3.5+, new node trees are no longer called "NodeTree" # In Blender 3.5+, new node trees are no longer called "NodeTree"
# but follow the bl_label attribute by default. New logic trees # but follow the bl_label attribute by default. New logic trees
# are thus called "Leenkx Logic Editor" which conflicts with Haxe's # are thus called "Leenkx Logic Editor" which conflicts with Haxe's
@ -125,9 +132,10 @@ def send_operator(op):
def always() -> float: def always() -> float:
# Force ui redraw # Force ui redraw
if state.redraw_ui: if state.redraw_ui:
for area in bpy.context.screen.areas: if bpy.context.screen is not None:
if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): for area in bpy.context.screen.areas:
area.tag_redraw() if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'):
area.tag_redraw()
state.redraw_ui = False state.redraw_ui = False
return 0.5 return 0.5
@ -135,38 +143,116 @@ def always() -> float:
def poll_threads() -> float: 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.
""" """
Improved thread polling with:
- No re-queuing overhead
- Batch processing of completed threads
- Adaptive timing based on activity
- Better memory management
- Simplified logic flow
"""
global _last_poll_time, _consecutive_empty_polls
current_time = time.time()
# Process all new threads from queue at once (batch processing)
new_threads_added = 0
try: try:
thread, callback = make.thread_callback_queue.get(block=False) while True:
thread, callback = make.thread_callback_queue.get(block=False)
_active_threads[thread] = callback
new_threads_added += 1
except queue.Empty: except queue.Empty:
pass
# Early return if no active threads
if not _active_threads:
_consecutive_empty_polls += 1
# Adaptive timing: longer intervals when consistently empty
if _consecutive_empty_polls > 10:
return 0.5 # Back off when no activity
return 0.25 return 0.25
if thread.is_alive():
try: # Reset empty poll counter when we have active threads
make.thread_callback_queue.put((thread, callback), block=False) _consecutive_empty_polls = 0
except queue.Full:
return 0.5 # Find completed threads (single pass, no re-queuing)
return 0.1 completed_threads = []
for thread in list(_active_threads.keys()):
if not thread.is_alive():
completed_threads.append(thread)
# Batch process all completed threads
if completed_threads:
_process_completed_threads(completed_threads)
# Adaptive timing based on activity level
active_count = len(_active_threads)
if active_count == 0:
return 0.25
elif active_count <= 3:
return 0.05 # Medium frequency for low activity
else: else:
return 0.01 # High frequency for high activity
def _process_completed_threads(completed_threads: list) -> None:
"""Process a batch of completed threads with robust error handling."""
for thread in completed_threads:
callback = _active_threads.pop(thread) # Remove from tracking
try: try:
thread.join() thread.join() # Should be instant since thread is dead
callback() callback()
except Exception as e: except Exception as e:
# If there is an exception, we can no longer return the time to # Robust error recovery
# the next call to this polling function, so to keep it running _handle_callback_error(e)
# we re-register it and then raise the original exception. continue # Continue processing other threads
try:
bpy.app.timers.unregister(poll_threads) # Explicit cleanup for better memory management
except ValueError: del thread, callback
pass
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True) def _handle_callback_error(exception: Exception) -> None:
# Quickly check if another thread has finished """Centralized error handling with better recovery."""
return 0.01 try:
# Try to unregister existing timer
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass # Timer wasn't registered, that's fine
# Re-register timer with slightly longer interval for stability
bpy.app.timers.register(poll_threads, first_interval=0.1, persistent=True)
# Re-raise the original exception after ensuring timer continuity
raise exception
def cleanup_polling_system() -> None:
"""Optional cleanup function for proper shutdown."""
global _active_threads, _consecutive_empty_polls
# Wait for remaining threads to complete (with timeout)
for thread in list(_active_threads.keys()):
if thread.is_alive():
thread.join(timeout=1.0) # 1 second timeout
# Clear tracking structures
_active_threads.clear()
_consecutive_empty_polls = 0
# Unregister timer
try:
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass
def get_polling_stats() -> dict:
"""Get statistics about the polling system for monitoring."""
return {
'active_threads': len(_active_threads),
'consecutive_empty_polls': _consecutive_empty_polls,
'thread_ids': [t.ident for t in _active_threads.keys()]
}
loaded_py_libraries: dict[str, types.ModuleType] = {} loaded_py_libraries: Dict[str, types.ModuleType] = {}
context_screen = None context_screen = None
@ -262,10 +348,18 @@ def reload_blend_data():
def load_library(asset_name): def load_library(asset_name):
if bpy.data.filepath.endswith('lnx_data.blend'): # Prevent load in library itself # Prevent load in library itself
return if bpy.app.version <= (2, 93, 0):
if bpy.data.filepath.endswith('lnx_data_2.blend'):
return
else:
if bpy.data.filepath.endswith('lnx_data.blend'):
return
sdk_path = lnx.utils.get_sdk_path() sdk_path = lnx.utils.get_sdk_path()
data_path = sdk_path + '/leenkx/blender/data/lnx_data.blend' if bpy.app.version <= (2, 93, 0):
data_path = sdk_path + '/leenkx/blender/data/lnx_data_2.blend'
else:
data_path = sdk_path + '/leenkx/blender/data/lnx_data.blend'
data_names = [asset_name] data_names = [asset_name]
# Import # Import

View File

@ -1,13 +1,15 @@
from typing import List, Dict, Optional, Any
import lnx.utils import lnx.utils
from lnx import assets from lnx import assets
def parse_context( def parse_context(
c: dict, c: Dict[str, Any],
sres: dict, sres: Dict[str, Any],
asset, asset: Any,
defs: list[str], defs: List[str],
vert: list[str] = None, vert: Optional[List[str]] = None,
frag: list[str] = None, frag: Optional[List[str]] = None,
): ):
con = { con = {
"name": c["name"], "name": c["name"],
@ -99,7 +101,12 @@ def parse_context(
def parse_shader( def parse_shader(
sres, c: dict, con: dict, defs: list[str], lines: list[str], parse_attributes: bool sres: Dict[str, Any],
c: Dict[str, Any],
con: Dict[str, Any],
defs: List[str],
lines: List[str],
parse_attributes: bool
): ):
"""Parses the given shader to get information about the used vertex """Parses the given shader to get information about the used vertex
elements, uniforms and constants. This information is later used in elements, uniforms and constants. This information is later used in
@ -229,7 +236,12 @@ def parse_shader(
check_link(c, defs, cid, const) check_link(c, defs, cid, const)
def check_link(source_context: dict, defs: list[str], cid: str, out: dict): def check_link(
source_context: Dict[str, Any],
defs: List[str],
cid: str,
out: Dict[str, Any]
):
"""Checks whether the uniform/constant with the given name (`cid`) """Checks whether the uniform/constant with the given name (`cid`)
has a link stated in the json (`source_context`) that can be safely 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, included based on the given defines (`defs`). If that is the case,
@ -273,7 +285,12 @@ def check_link(source_context: dict, defs: list[str], cid: str, out: dict):
def make( def make(
res: dict, base_name: str, json_data: dict, fp, defs: list[str], make_variants: bool res: Dict[str, Any],
base_name: str,
json_data: Dict[str, Any],
fp: Any,
defs: List[str],
make_variants: bool
): ):
sres = {"name": base_name, "contexts": []} sres = {"name": base_name, "contexts": []}
res["shader_datas"].append(sres) res["shader_datas"].append(sres)

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