Compare commits

..

63 Commits

Author SHA1 Message Date
f45ad72056 Scaled slope bias and transparent shadows fixes 2026-10-31 01:10:21 -07:00
29f0765864 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-10-29 22:36:39 -07:00
93e91a68d5 Revert PAR 2026-10-29 22:36:04 -07:00
4c1ec8078b Merge pull request 'main' (#151) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#151
2026-10-29 05:59:43 +00:00
d4a70b7882 Deprecate old navmesh generation 2026-10-28 22:46:28 -07:00
92d6637695 P2P cx index 2026-10-28 13:53:25 -07:00
84bab9bbc5 Batch mesh remove object 2026-10-28 01:29:21 -07:00
74997b1e23 P2P onLeft trigger onClose 2026-10-28 01:17:36 -07:00
6454d741d4 P2P fix raw dedup 2026-10-27 23:06:31 -07:00
7d63d60d99 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-10-27 22:42:45 -07:00
9f37c2bd8b P2P wire peer event order 2026-10-27 22:42:26 -07:00
083e7dbe7d Merge pull request 'main' (#150) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#150
2026-10-27 23:15:24 +00:00
5789e629f9 merge upstream 2026-10-27 23:02:20 +00:00
6c13722fc6 Consolidate and update 2026-10-27 15:58:22 -07:00
c3a27b2ee9 Delete Krom/Krom.app/Contents/MacOS/Krom_x64 2026-10-20 23:51:55 +00:00
cac6c7e7b8 Merge pull request 'main' (#149) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#149
2026-10-20 23:50:51 +00:00
eb211da457 merge upstream 2026-10-20 23:48:49 +00:00
7caa6f8ff5 Merge pull request 'MacOS P2P Hashlink and Krom' (#11) from Dante/LNXSDK:main into main
Reviewed-on: #11
2026-10-20 23:48:24 +00:00
478a97d2ed MacOS P2P Hashlink and Krom 2026-10-20 16:37:13 -07:00
6c537591e8 Merge pull request 'main' (#148) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#148
2026-10-15 09:56:20 +00:00
ee04bb87dd Indent Fix 2026-10-15 02:52:58 -07:00
612a3d0a09 Object Occlusion Culling 2026-10-15 02:41:03 -07:00
3273054766 Water Shader Update 2026-10-14 14:37:31 -07:00
beabf76562 Unclosed conditional 2026-10-09 16:33:14 -07:00
c1aa2837ca Deinterleaved Vertex Buffer Update 2026-10-09 16:30:11 -07:00
c320c4bb19 Batch Meshes 2026-10-09 15:02:20 -07:00
4c8c26bb33 Corrupted CUDA context + Empty sky enum 2026-10-09 00:42:19 -07:00
a133c5ee94 Update leenkx.py 2026-10-06 20:48:45 +00:00
4895d7cfa5 Merge pull request 'main' (#147) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#147
2026-10-06 20:47:54 +00:00
052ce82588 Deprecate LNXJS + Cleanup 2026-10-06 13:32:39 -07:00
211cba61a6 Export Network Update 2026-10-06 11:34:17 -07:00
4ab646015d P2P Export - Nodes - Patch 2026-10-06 02:10:08 -07:00
9fee67d231 Haxe Lib Datachannel 2026-10-04 17:14:28 -07:00
860b73a8f4 hxWebsocket Upstream and Network Updates 2026-10-03 22:05:53 -07:00
da38631584 Leenkx Network Torrent 2026-10-03 17:40:22 -07:00
6839203dc5 P2P RunT + Static WebView 2026-10-03 00:02:30 -07:00
d9ac74101f Merge pull request 'Fix background color intensity for 'Filmic2' tonemap' (#146) from moisesjpelaez/LNXSDK:filmic into main
Reviewed-on: LeenkxTeam/LNXSDK#146
2026-09-23 22:45:26 +00:00
e20400654c Merge pull request 'Add missing dll to prevent error 3221225781' (#145) from moisesjpelaez/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#145
2026-09-23 22:44:13 +00:00
058cbed20f Fix background color intensity for 'Filmic2' tonemap 2026-09-21 19:44:07 -04:00
e0c10318e5 Add missing dll to prevent error 3221225781 2026-09-21 18:27:29 -04:00
7d609b126d Merge pull request 'main' (#144) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#144
2026-09-20 18:30:58 +00:00
36ecaea919 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-20 11:15:57 -07:00
fac01a2849 Group Nodes, Shaders, Textures, Displacement 2026-09-20 11:15:14 -07:00
c71c1a98e4 Merge pull request 'MRT Index' (#143) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#143
2026-09-17 23:43:07 +00:00
f6d5f142e9 MRT Index 2026-09-17 16:38:36 -07:00
70961b0336 Merge pull request 'Fix thinWall and copy_pass in Forward rendering' (#141) from Moises/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#141
2026-09-17 21:16:33 +00:00
9c50274f29 Merge remote-tracking branch 'upstream/main' 2026-09-17 17:15:55 -04:00
a94b95afc8 Merge pull request 'main' (#142) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#142
2026-09-17 21:09:32 +00:00
cee36635e0 Cleanup 2026-09-17 16:07:10 -04:00
d0337f5dae Fix thinWall and passes 2026-09-17 15:59:45 -04:00
fca51c7e61 Water Level/Reflectopm 2026-09-16 22:44:16 -07:00
0bb4bceb57 Water Foam Fix 2026-09-16 18:12:12 -07:00
af9a259fbb Water Sun Direction 2026-09-16 17:36:31 -07:00
f1d8522f51 Bloom Isnan Isinf 2026-09-16 16:04:32 -07:00
56d1b0e4a7 Forward / Mobile / Solid 2026-09-16 12:40:39 -07:00
73e4a0ad30 Scene Gamma / Exposure 2026-09-13 23:34:36 -07:00
67d5e6b2c7 SSRS Fix 2026-09-13 21:52:13 -07:00
635f8f6645 SSRS WebGL Fix 2026-09-13 20:18:00 -07:00
b2444be7e2 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 20:16:57 -07:00
7d00036973 Version Bump 2026-09-13 23:11:41 +00:00
46814444af Merge pull request 'Update Theme' (#140) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#140
2026-09-13 23:09:55 +00:00
b4de377433 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 15:23:25 -07:00
d787f9a7fc Update Theme 2026-09-13 12:10:51 -07:00
370 changed files with 79194 additions and 60390 deletions

View File

@ -130,6 +130,11 @@ extern class Krom {
static function readStorage(name: String): haxe.io.BytesData;
static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void;
static function fileWriteBytes(path: String, bytes: haxe.io.BytesData, ?offset: Float, ?byteLength: Int): Void;
static function fileReadBytes(path: String, ?offset: Float, ?length: Int): haxe.io.BytesData;
static function fileSize(path: String): Float;
static function fileExists(path: String): Bool;
static function deleteFile(path: String): Void;
static function sysCommand(cmd: String, ?args: Array<String>): Int;
static function savePath(): String;
static function getArgCount(): Int;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +0,0 @@
# The zlib/libpng License
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
This notice may not be removed or altered from any source distribution.

View File

@ -1,7 +0,0 @@
* 2024-12-02: Submodules have been moved in to this repository (previously contained in https://github.com/leenkx3d/lnxsdk).
* 2019-06-30: Return value of `PhysicsWorld.rayCast()` changed, see https://github.com/leenkx3d/leenkx/commit/dfb7609a28cebf3a520e6a25a8563d01c32f2b01.
* 2019-04-06: Use voxelao instead of voxelgi, gi will be reworked into raytracing.
* 2019-01-13: If you are using Leenkx Updater, get Leenkx 0.6beta from https://leenkx.itch.io/leenkx3d first (or clone the sdk from https://github.com/leenkx3d/lnxsdk).
* 2018-08-28: `LampObject` and `LampData` is now `LightObject` and `LightData`
* 2018-06-01: 'Not Equal' has been removed from the Gate logic node. Use 'Equal' and 'False' output socket instead.
* 2017-11-20: Use `leenkx.trait.physics.*` instead of `leenkx.trait.internal.*` to access physics traits.

View File

@ -6,7 +6,7 @@ bl_info = {
"location": "Properties -> Render -> Leenkx Player",
"description": "Full Stack SDK",
"author": "Leenkx.com",
"version": (2026, 5, 0),
"version": (2026, 10, 6),
"blender": (5, 2, 0),
"doc_url": "https://leenkx.com/",
"tracker_url": "https://leenkx.com/support"
@ -361,7 +361,7 @@ class LeenkxAddonPreferences(AddonPreferences):
layout.label(text="Welcome to Leenkx!")
# Compare version Blender and Leenkx (major, minor)
if bpy.app.version[:2] not in [(4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]:
if bpy.app.version[:2] not in [(5, 2), (4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]:
box = layout.box().column()
box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version")

View File

@ -48,6 +48,10 @@ void main() {
}
if (currentMipLevel == 0) {
if (any(isnan(fragColor.rgb)) || any(isinf(fragColor.rgb))) {
fragColor.rgb = vec3(0.0);
}
// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.2
// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4

View File

@ -69,6 +69,7 @@ uniform vec4 PPComp15;
uniform vec4 PPComp16;
uniform vec4 PPComp18;
uniform vec4 PPComp19;
uniform vec4 PPComp20;
#endif
// #ifdef _CPos
@ -499,14 +500,15 @@ void main() {
#ifdef _CExposure
#ifdef _CPostprocess
fragColor.rgb+=fragColor.rgb*PPComp8.x;
fragColor.rgb *= pow(2.0, PPComp8.x);
#else
fragColor.rgb+= fragColor.rgb*compoExposureStrength;
fragColor.rgb *= pow(2.0, compoExposureStrength);
#endif
#endif
#ifdef _CPostprocess
fragColor.rgb *= ComputeEV(0.0);
fragColor.rgb *= pow(2.0, PPComp20.x); // exposure and gamma
#endif
#ifdef _AutoExposure
@ -582,14 +584,16 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
fragColor.rgb = tonemapAgXFull(fragColor.rgb);
} //else { fragColor.rgb = vec3(0,1,0); //ERROR}
#endif
#ifdef _CGamma
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / PPComp20.y));
#endif
#else
#ifdef _CToneFilmic
fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma
#endif
#ifdef _CToneFilmic2
fragColor.rgb = acesFilm(fragColor.rgb);
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 1.0));
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));
#endif
#ifdef _CToneReinhard
fragColor.rgb = tonemapReinhard(fragColor.rgb);
@ -615,6 +619,10 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
#endif
#endif
#ifdef _CGamma
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / compoGammaStrength));
#endif
#ifdef _CBW
// fragColor.rgb = vec3(clamp(dot(fragColor.rgb, fragColor.rgb), 0.0, 1.0));
fragColor.rgb = vec3((fragColor.r * 0.3 + fragColor.g * 0.59 + fragColor.b * 0.11) / 3.0) * 2.5;

View File

@ -636,7 +636,7 @@ void main() {
shadowMap
#endif
#endif
, eye, p + n * shadowsBias * 2, shadowsBias
, eye, p, n, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif
@ -907,26 +907,10 @@ tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[li], p, n, cLightDir, lightPlane.y, shadowMapAtlas, sssColorCL, sssRadiusCL, tileBoundsSpotArray[li]);
#endif
#else
if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
else if (li == 1) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[1], p, n, cLightDir, lightPlane.y, shadowMapSpot[1], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
else if (li == 2) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[2], p, n, cLightDir, lightPlane.y, shadowMapSpot[2], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
else if (li == 3) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[3], p, n, cLightDir, lightPlane.y, shadowMapSpot[3], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL);
else if (li == 1) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[1], p, n, cLightDir, lightPlane.y, shadowMapSpot[1], sssColorCL, sssRadiusCL);
else if (li == 2) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[2], p, n, cLightDir, lightPlane.y, shadowMapSpot[2], sssColorCL, sssRadiusCL);
else if (li == 3) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[3], p, n, cLightDir, lightPlane.y, shadowMapSpot[3], sssColorCL, sssRadiusCL);
#endif
} else {
#ifdef _ShadowMapAtlas

View File

@ -259,6 +259,11 @@
"link": "_biasLightWorldViewProjectionMatrixSpotArray",
"ifdef": ["_Clusters", "_ShadowMap", "_Spot"]
},
{
"name": "LWVPSpotArray",
"link": "_biasLightViewProjectionMatrixSpotArray",
"ifdef": ["_SinglePoint", "_ShadowMap", "_Spot"]
},
{
"name": "pointLightDataArray",
"link": "_pointLightsAtlasArray",

View File

@ -56,9 +56,15 @@ uniform vec2 cameraPlane;
#ifdef _SinglePoint
#ifdef _Spot
//!uniform sampler2DShadow shadowMapSpot[1];
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapSpotTransparent[1];
#endif
//!uniform mat4 LWVPSpotArray[1];
#else
//!uniform samplerCubeShadow shadowMapPoint[1];
#ifdef _ShadowMapTransparent
//!uniform samplerCube shadowMapPointTransparent[1];
#endif
//!uniform vec2 lightProj;
#endif
#endif
@ -66,26 +72,41 @@ uniform vec2 cameraPlane;
#ifdef _ShadowMapAtlas
#ifdef _SingleAtlas
uniform sampler2DShadow shadowMapAtlas;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasTransparent;
#endif
#endif
#endif
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlasPoint;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasPointTransparent;
#endif
//!uniform vec4 pointLightDataArray[4];
#endif
//!uniform vec4 pointLightDataArray[maxLightsCluster * 6];
#else
//!uniform samplerCubeShadow shadowMapPoint[4];
#ifdef _ShadowMapTransparent
//!uniform samplerCube shadowMapPointTransparent[4];
#endif
#endif
//!uniform vec2 lightProj;
#ifdef _Spot
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlasSpot;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasSpotTransparent;
#endif
#endif
#else
//!uniform sampler2DShadow shadowMapSpot[4];
//!uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
#endif
//!uniform mat4 LWVPSpotArray[4];
#endif
//!uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif
#endif
#endif
@ -97,10 +118,16 @@ uniform vec3 sunCol;
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSun;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasSunTransparent;
#endif
#endif
//!uniform vec4 tileBoundsSunArray[maxLights * shadowmapCascades];
#else
uniform sampler2DShadow shadowMap;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapTransparent;
#endif
#endif
uniform float shadowsBias;
#ifdef _CSM
@ -167,10 +194,11 @@ void main() {
vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ
vec2 occspec = unpackFloat2(g1.a);
vec3 albedo = surfaceAlbedo(g1.rgb, metallic); // g1.rgb - basecolor
vec3 f0 = surfaceF0(g1.rgb, metallic);
vec3 basecolor = min(g1.rgb, vec3(2.0));
vec3 albedo = surfaceAlbedo(basecolor, metallic);
vec3 f0 = surfaceF0(basecolor, metallic);
#ifdef _ExtBRDF
f0 = mix(f0, min(g1.rgb, vec3(2.0)), vec3(matp6.y, matp6.z, matp6.w));
f0 = mix(f0, basecolor, vec3(matp6.y, matp6.z, matp6.w));
#endif
float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0;
@ -206,7 +234,7 @@ void main() {
envl /= PI;
#endif
#else
vec3 envl = vec3(1.0);
vec3 envl = vec3(0.0);
#endif
#ifdef _Rad
@ -227,13 +255,23 @@ void main() {
#endif
#endif
#ifdef _Brdf
vec3 F = f0 * envBRDF.x + envBRDF.y;
#else
vec3 F = f0;
#endif
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
#ifdef _Brdf
envl.rgb *= 1.0 - F;
#endif
#ifdef _Rad // Indirect specular
envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y;
envl.rgb += prefilteredColor * F;
#else
#ifdef _EnvCol
envl.rgb += backgroundCol * surfaceF0(g1.rgb, metallic); // f0
envl.rgb += backgroundCol * F;
#endif
#endif
@ -281,7 +319,12 @@ void main() {
float transmittance = 1.0 - transF;
#ifdef _Rad
if (matp2.z > 0.0 && transmittance > 0.0) {
vec3 refrDir = transmissionIBLDirection(n, v, matp3.x);
vec3 refrDir;
if (matp3.y > 0.5) {
refrDir = reflect(-v, n);
} else {
refrDir = transmissionIBLDirection(n, v, matp3.x);
}
float transLod = getMipFromRoughness(matp2.w, envmapNumMipmaps);
vec3 transColor = textureLod(senvmapRadiance,
envMapEquirect(refrDir), transLod).rgb;
@ -337,7 +380,7 @@ void main() {
float sdotNH = max(0.0, dot(n, sh));
float sdotVH = max(0.0, dot(v, sh));
float sdotNL = max(0.0, dot(n, sunDir));
float svisibility = 1.0;
vec3 svisibility = vec3(1.0);
#ifdef _Anisotropy
vec3 sdirect;
if (abs(matp0.x) > 0.001 && dot(wTangent, wTangent) > 0.001) {
@ -385,29 +428,59 @@ void main() {
#ifdef _CSM
svisibility = shadowTestCascade(
#ifdef _ShadowMapAtlas
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasSun, shadowMapAtlasSunTransparent
#else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas
shadowMapAtlasSun
#else
shadowMapAtlas
#endif
#endif
#else
shadowMap
#ifdef _ShadowMapTransparent
shadowMap, shadowMapTransparent
#else
shadowMap
#endif
#endif
, eye, p, n, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif
, eye, p + n * shadowsBias * 10, shadowsBias
);
#else
vec4 lPos = LWVP * vec4(p + n * shadowsBias * 100, 1.0);
vec4 lPos = LWVP * vec4(p + n * shadowsBias * 2, 1.0);
if (lPos.w > 0.0) svisibility = shadowTest(
#ifdef _ShadowMapAtlas
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasSun, shadowMapAtlasSunTransparent
#else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas
shadowMapAtlasSun
#else
shadowMapAtlas
#endif
#endif
#else
shadowMap
#ifdef _ShadowMapTransparent
shadowMap, shadowMapTransparent
#else
shadowMap
#endif
#endif
, lPos.xyz / lPos.w, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif
);
#endif
#endif
@ -415,11 +488,18 @@ void main() {
fragColor.rgb += sdirect * svisibility * sunCol;
#endif
#ifdef _ShadowMapAtlas
tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
#endif
#ifdef _SinglePoint
fragColor.rgb += sampleLight(
p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0
#ifdef _ShadowMap
, 0, pointBias, true
#ifdef _ShadowMapTransparent
, false
#endif
#endif
#ifdef _Spot
, true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight // TODO: Test!
@ -472,12 +552,15 @@ void main() {
#ifdef _ShadowMap
// light index, shadow bias, cast_shadows
, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0
#ifdef _ShadowMapTransparent
, false
#endif
#endif
#ifdef _Spot
, lightsArray[li * 3 + 2].y != 0.0
, lightsArray[li * 3 + 2].y // spot size (cutoff)
, lightsArraySpot[li].w // spot blend (exponent)
, lightsArraySpot[li].xyz // spotDir
, lightsArraySpot[li * 2].w // spot blend (exponent)
, lightsArraySpot[li * 2].xyz // spotDir
, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale
, lightsArraySpot[li * 2 + 1].xyz // right
#endif

View File

@ -122,11 +122,6 @@
"link": "_inverseViewProjectionMatrixRight",
"ifdef": ["_VRStereo"]
},
{
"name": "invVP",
"link": "_viewProjectionMatrix",
"ifdef": ["_SSRS"]
},
{
"name": "smSizeUniform",
"link": "_shadowMapSize",
@ -137,14 +132,6 @@
"link": "_lightPlane",
"ifdef": ["_SSS"]
},
{
"name": "VP",
"link": "_viewProjectionMatrix",
"ifdef": ["_SSRS"]
},
{
"ifdef": ["_SMSizeUniform"]
},
{
"name": "lightProj",
"link": "_lightPlaneProj",
@ -196,6 +183,11 @@
"link": "_biasLightWorldViewProjectionMatrixSpotArray",
"ifdef": ["_Clusters", "_ShadowMap", "_Spot"]
},
{
"name": "LWVPSpotArray",
"link": "_biasLightViewProjectionMatrixSpotArray",
"ifdef": ["_SinglePoint", "_ShadowMap", "_Spot"]
},
{
"name": "pointLightDataArray",
"link": "_pointLightsAtlasArray",

View File

@ -3,8 +3,8 @@
{
"name": "probe_cubemap",
"depth_write": false,
"compare_mode": "less",
"cull_mode": "clockwise",
"compare_mode": "greater",
"cull_mode": "counter_clockwise",
"blend_source": "blend_one",
"blend_destination": "blend_one",
"blend_operation": "add",

View File

@ -3,8 +3,8 @@
{
"name": "probe_planar",
"depth_write": false,
"compare_mode": "less",
"cull_mode": "clockwise",
"compare_mode": "greater",
"cull_mode": "counter_clockwise",
"blend_source": "blend_one",
"blend_destination": "blend_one",
"blend_operation": "add",

View File

@ -81,11 +81,11 @@ void main() {
vec4 gr = textureLod(gbuffer_refraction, texCoord, 0.0);
float ior = unpackIOR(gr.x);
float transmittance = gr.y;
float surfaceDepth = gr.z;
float surfaceDepth = textureLod(gbufferD, texCoord, 0.0).r;
float d = surfaceDepth * 2.0 - 1.0;
vec4 sceneSample = textureLod(tex, texCoord, 0.0);
if (surfaceDepth == 0.0 || surfaceDepth == 1.0) {
if (gr.z == 0.0 || surfaceDepth == 1.0) {
fragColor = sceneSample;
return;
}

View File

@ -595,3 +595,4 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
#endif
#endif

View File

@ -14,15 +14,24 @@
#ifdef _SinglePoint
#ifdef _Spot
uniform sampler2DShadow shadowMapSpot[1];
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapSpotTransparent[1];
#endif
uniform mat4 LWVPSpotArray[1];
#else
uniform samplerCubeShadow shadowMapPoint[1];
#ifdef _ShadowMapTransparent
uniform samplerCube shadowMapPointTransparent[1];
#endif
uniform vec2 lightProj;
#endif
#endif
#ifdef _Clusters
#ifdef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlas;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasTransparent;
#endif
#endif
#ifndef _SinglePoint
uniform vec2 lightProj;
@ -30,17 +39,29 @@
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasPoint;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasPointTransparent;
#endif
#endif
#else
uniform samplerCubeShadow shadowMapPoint[4];
#ifdef _ShadowMapTransparent
uniform samplerCube shadowMapPointTransparent[4];
#endif
#endif
#ifdef _Spot
#ifdef _ShadowMapAtlas
#ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSpot;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasSpotTransparent;
#endif
#endif
#else
uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
#endif
#endif
uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif
@ -51,6 +72,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
const vec3 albedo, const float rough, const float spec, const vec3 f0
#ifdef _ShadowMap
, int index, float bias, bool receiveShadow
#ifdef _ShadowMapTransparent
, bool transparent
#endif
#endif
#ifdef _Spot
, bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right
@ -121,26 +145,77 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _ShadowMap
if (receiveShadow) {
#ifdef _SinglePoint
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias);
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif
#ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas
tileBounds = tileBoundsSpotArray[index];
direct *= shadowTest(
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
shadowMapAtlas
tileBounds = tileBoundsSpotArray[index];
direct *= shadowTest(
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasSpot, shadowMapAtlasSpotTransparent
#else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
shadowMapAtlas
#endif
#endif
, lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#else
if (index == 0) direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0],
#endif
, lPos.xyz / lPos.w, bias
);
#else
if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias);
else if (index == 1) direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias);
else if (index == 2) direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias);
else if (index == 3) direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias);
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 1) direct *= shadowTest(shadowMapSpot[1],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[1],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 2) direct *= shadowTest(shadowMapSpot[2],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[2],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 3) direct *= shadowTest(shadowMapSpot[3],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[3],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif
#endif
}
@ -153,25 +228,76 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
if (receiveShadow) {
#ifdef _SinglePoint
#ifndef _Spot
direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n);
#ifndef _Spot
direct *= PCFCube(shadowMapPoint[0],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[0],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif
#endif
#ifdef _Clusters
#ifdef _ShadowMapAtlas
direct *= PCFFakeCube(
#ifndef _SingleAtlas
shadowMapAtlasPoint
#else
shadowMapAtlas
#endif
, ld, -l, bias, lightProj, n, index
);
direct *= PCFFakeCube(
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasPoint, shadowMapAtlasPointTransparent
#else
if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n);
else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n);
else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n);
else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n);
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas
shadowMapAtlasPoint
#else
shadowMapAtlas
#endif
#endif
, ld, -l, bias, lightProj, n, index
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#else
if (index == 0) direct *= PCFCube(shadowMapPoint[0],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[0],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 1) direct *= PCFCube(shadowMapPoint[1],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[1],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 2) direct *= PCFCube(shadowMapPoint[2],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[2],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 3) direct *= PCFCube(shadowMapPoint[3],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[3],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif
#endif
}
@ -186,6 +312,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
const vec3 albedo, const float rough, const float spec, const vec3 f0
#ifdef _ShadowMap
, int index, float bias, bool receiveShadow
#ifdef _ShadowMapTransparent
, bool transparent
#endif
#endif
#ifdef _Spot
, bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right
@ -194,6 +323,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
return sampleLight(p, n, v, dotNV, lp, lightCol, albedo, rough, spec, f0
#ifdef _ShadowMap
, index, bias, receiveShadow
#ifdef _ShadowMapTransparent
, transparent
#endif
#endif
#ifdef _Spot
, isSpot, spotSize, spotBlend, spotDir, scale, right

View File

@ -24,7 +24,7 @@ uniform vec2 smSizeUniform;
#ifdef _ShadowMapAtlas
uniform vec4 tileBoundsSunArray[maxLights * shadowmapCascades];
#if defined(_Clusters) && defined(_Spot) && defined(_ShadowMap)
#if defined(_Clusters) && (defined(_Spot) || defined(_LTC)) && defined(_ShadowMap)
uniform vec4 tileBoundsSpotArray[maxLightsCluster];
#endif
vec4 tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
@ -180,7 +180,7 @@ vec3 PCFCube(samplerCubeShadow shadowMapCube,
#endif
) {
const float s = shadowmapCubePcfSize; // TODO: incorrect...
float compare = lpToDepth(lp, lightProj) - bias * 1.5;
float compare = lpToDepth(lp, lightProj) - bias * 1.5 - 0.0001;
ml = ml + n * bias * 20;
#ifdef _InvY
ml.y = -ml.y;
@ -321,7 +321,7 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap,
, const bool transparent
#endif
) {
const float compare = lpToDepth(lp, lightProj) - bias * 1.5;
const float compare = lpToDepth(lp, lightProj) - bias * 1.5 - 0.0001;
ml = ml + n * bias * 20;
int faceIndex = 0;
const int lightIndex = index * 6;
@ -408,10 +408,16 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
if (transparent == false) {
vec3 transResult = vec3(0.0);
vec2 tileMin = pointLightTile.xy;
vec2 tileMax = pointLightTile.xy + pointLightTile.z;
#ifdef _FlipY
tileMin.y = 1.0 - tileMax.y;
tileMax.y = 1.0 - pointLightTile.y;
#endif
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec4 smt = texture(shadowMapTransparent,
clamp(uvtiled + vec2(x, y) / smSize, 0.0, 1.0));
clamp(uvtiled + vec2(x, y) / smSize, tileMin, tileMax));
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
}
}
@ -437,15 +443,15 @@ vec3 shadowTest(sampler2DShadow shadowMap,
#ifdef _ShadowMapAtlas
// use tile PCF
#ifdef _SMSizeUniform
vec2 smSizeAtlas = smSizeUniform * (tileBounds.zw - tileBounds.xy);
vec2 smSizeAtlas = smSizeUniform;
#else
vec2 smSizeAtlas = shadowmapSize * (tileBounds.zw - tileBounds.xy);
vec2 smSizeAtlas = shadowmapSize;
#endif
return PCFTileAware(shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos.xy, lPos.z - shadowsBias, smSizeAtlas,
lPos.xy, lPos.z - shadowsBias - 0.0001, smSizeAtlas,
tileBounds.xy, tileBounds.zw
#ifdef _ShadowMapTransparent
, transparent
@ -462,7 +468,7 @@ vec3 shadowTest(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos.xy, lPos.z - shadowsBias, smSize
lPos.xy, lPos.z - shadowsBias - 0.0001, smSize
#ifdef _ShadowMapTransparent
, transparent
#endif
@ -499,11 +505,22 @@ mat4 getCascadeMat(const float d, out int casi, out int casIndex) {
// ..
}
float worldPerTexel(const mat4 LWVP, const vec2 smSize) {
return max(1.0 / abs(LWVP[0][0]), 1.0 / abs(LWVP[1][1])) / smSize.x;
}
float nlSlopeBias(const vec3 n, const mat4 LWVP, const float wpt) {
vec3 ldir = normalize(vec3(LWVP[0][2], LWVP[1][2], LWVP[2][2]));
float ndotl = max(abs(dot(n, ldir)), 0.05);
float tanTheta = min(sqrt(1.0 - ndotl * ndotl) / ndotl, 1.5);
return wpt * tanTheta * abs(LWVP[2][2]);
}
vec3 shadowTestCascade(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
sampler2D shadowMapTransparent,
#endif
const vec3 eye, const vec3 p, const float shadowsBias
const vec3 eye, const vec3 p, const vec3 n, const float shadowsBias
#ifdef _ShadowMapTransparent
, const bool transparent
#endif
@ -522,8 +539,10 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
int casi;
int casIndex;
mat4 LWVP = getCascadeMat(d, casi, casIndex);
vec4 lPos = LWVP * vec4(p, 1.0);
float wpt = worldPerTexel(LWVP, smSize);
vec4 lPos = LWVP * vec4(p + n * 0.5 * wpt, 1.0);
lPos.xyz /= lPos.w;
float scaledBias = shadowsBias + nlSlopeBias(n, LWVP, wpt);
#ifdef _ShadowMapAtlas
tileBounds = tileBoundsSunArray[casi];
@ -536,7 +555,7 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos.xy, lPos.z - shadowsBias, smSize,
lPos.xy, lPos.z - scaledBias, smSize,
tileBounds.xy, tileBounds.zw
#ifdef _ShadowMapTransparent
, transparent
@ -547,7 +566,7 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos.xy, lPos.z - shadowsBias, smSize
lPos.xy, lPos.z - scaledBias, smSize
#ifdef _ShadowMapTransparent
, transparent
#endif
@ -569,8 +588,10 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
casData[casIndex2 + 2],
casData[casIndex2 + 3]);
vec4 lPos2 = LWVP2 * vec4(p, 1.0);
float wpt2 = worldPerTexel(LWVP2, smSize);
vec4 lPos2 = LWVP2 * vec4(p + n * 0.5 * wpt2, 1.0);
lPos2.xyz /= lPos2.w;
float scaledBias2 = shadowsBias + nlSlopeBias(n, LWVP2, wpt2);
vec3 visibility2 = vec3(1.0);
// use lPos2 coordinates for second cascade, not lPos
#ifdef _ShadowMapAtlas
@ -582,7 +603,7 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos2.xy, lPos2.z - shadowsBias, smSize,
lPos2.xy, lPos2.z - scaledBias2, smSize,
tileBounds.xy, tileBounds.zw
#ifdef _ShadowMapTransparent
, transparent
@ -593,7 +614,7 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
shadowMapTransparent,
#endif
lPos2.xy, lPos2.z - shadowsBias, smSize
lPos2.xy, lPos2.z - scaledBias2, smSize
#ifdef _ShadowMapTransparent
, transparent
#endif
@ -612,4 +633,4 @@ vec3 shadowTestCascade(sampler2DShadow shadowMap,
// if (ci == 12) albedo.rgb = vec3(1.0, 1.0, 0.0);
}
#endif
#endif
#endif

View File

@ -96,41 +96,81 @@ vec3 sampleWaterNormals(vec2 hitXY, float speed, out vec2 tcnor0, out vec2 tcnor
return normalize(n0 + n1 - 1.0);
}
float waveHeight(vec2 xy, float speed) {
return (sin(xy.x * 10.0 / waterDisplace + speed) * cos(xy.y * 10.0 / waterDisplace + speed) + sin(xy.x * 20.0 / waterDisplace + speed * 1.3) * cos(xy.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) / 50.0 * waterDisplace;
}
void main() {
float gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0;
vec3 vray = normalize(viewRay);
float speed = time * 2.0 * waterSpeed;
bool isSky = (gdepth == 1.0);
float farPlane = cameraProj.y / (1.0 - cameraProj.x);
float cosAng = dot(eyeLook, vray);
float domeR = farPlane * 0.94;
float tDome = -3.5 * vray.z + sqrt(max(domeR * domeR - 12.25 * (1.0 - vray.z * vray.z), 0.0));
float domeDepth01 = cameraProj.x + cameraProj.y / (tDome * cosAng);
bool isSky = (gdepth * 0.5 + 0.5) >= domeDepth01;
// Ray-plane intersection with water surface (z = waterLevel)
float denom = dot(vray, vec3(0.0, 0.0, 1.0));
float denom = vray.z;
float tWater = (waterLevel - eye.z) / denom;
bool hasWaterHit = (abs(denom) > 0.0001) && (tWater > 0.0);
if (eye.z < waterLevel) {
vec2 tc = texCoord;
float fogFactor;
if (hasWaterHit && denom > 0.0) {
// Looking up at water surface - apply normal distortion
vec3 hit = eye + tWater * vray;
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(hit.xy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
fogFactor = clamp(tWater * waterDensity, 0.0, 0.95);
} else {
// Looking forward/down - distort via water surface above fragment
vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj);
vec2 wxy = isSky ? (eye.xy + vray.xy * 50.0) : p.xy;
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float waterDist = isSky ? 50.0 : length(p - eye);
fogFactor = clamp(waterDist * waterDensity, 0.0, 0.95);
float depthThick = 1.0 + (waterLevel - eye.z);
vec3 p = vec3(0.0);
float geomDist = farPlane;
if (!isSky) {
p = getPos(eye, eyeLook, vray, gdepth, cameraProj);
geomDist = length(p - eye);
}
bool upRay = hasWaterHit && denom > 0.0;
float waterDist = upRay ? min(tWater, geomDist) : geomDist;
vec2 wxy;
if (upRay && tWater < geomDist) {
wxy = (eye + tWater * vray).xy;
} else if (!isSky) {
wxy = p.xy;
} else {
wxy = eye.xy + vray.xy * 50.0;
}
vec2 tc0, tc1;
vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1);
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float ddepth2 = textureLod(gbufferD, tc, 0.0).r * 2.0 - 1.0;
float dist2 = cameraProj.y / ((ddepth2 * 0.5 + 0.5) - cameraProj.x) / cosAng;
if (dist2 > geomDist + 1.0) tc = texCoord;
vec3 refracted = textureLod(tex, tc, 0.0).rgb;
fragColor.rgb = mix(refracted, waterColor, fogFactor);
vec3 outCol = refracted;
if (upRay && tWater < geomDist) {
outCol *= max(1.0 - length(n2.xy) * 1.5, 0.0);
vec3 h2 = normalize(-vray + ld);
outCol += pow(max(dot(n2, h2), 0.0), 80.0) * 0.6;
}
if (!isSky) {
float fdw = abs(p.z - (waterLevel + waveHeight(p.xy, speed)));
float bandw = 0.1 + waterDist * 0.003;
if (fdw < bandw) {
float foamLod = clamp(waterDist / 15.0, 0.0, 5.0);
vec3 fm0 = textureLod(sfoam, tc0 * 3.0 + vec2(speed / 30.0, speed / 50.0), foamLod).rgb;
vec3 fm1 = textureLod(sfoam, tc1 * 4.0 + vec2(-speed / 35.0, speed / 45.0), foamLod).rgb;
float fstr = clamp(1.0 - fm0.r * 0.5 - fm1.b * 0.5, 0.0, 1.0);
float ffac = clamp(1.0 - fdw / bandw, 0.0, 1.0);
float foamFade = clamp(1.0 - (waterLevel - eye.z) * 0.4, 0.0, 1.0);
vec3 foamCol = mix(waterColor, vec3(1.0), 0.6);
outCol = mix(outCol, foamCol, ffac * fstr * 0.5 * foamFade);
}
}
float fogFactor = 1.0 - exp(-waterDist * waterDensity * 0.006 * depthThick);
outCol = mix(outCol, waterColor, clamp(fogFactor, 0.0, 1.0));
fragColor.rgb = outCol;
fragColor.a = 1.0;
return;
}
@ -144,20 +184,19 @@ void main() {
if (isSky) tWater = min(tWater, 100.0); // Clamp to prevent aliasing at horizon
vec3 p = isSky ? (eye + tWater * vray) : getPos(eye, eyeLook, vray, gdepth, cameraProj);
float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0);
if (!isSky && p.z > waterLevel) {
fragColor = vec4(0.0);
if (!isSky && p.z > waterLevel - 0.01) {
fragColor = textureLod(tex, texCoord, 0.0);
return;
}
float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0);
// Displace surface
p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed)
+ sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5)
/ 50.0 * waterDisplace;
float geomZ = p.z; // undisplaced depth for foam/fog tests
p.z += waveHeight(p.xy, speed);
// Hit plane to determine uvs
vec3 v = normalize(eye - p);
float t = (waterLevel - eye.z) / dot(v, vec3(0.0, 0.0, 1.0));
float t = (waterLevel - eye.z) / v.z;
vec3 hit = eye + t * v;
// Sample normal maps
@ -170,8 +209,8 @@ void main() {
tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
} else {
float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0;
vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj);
tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
float dist2 = cameraProj.y / ((ddepth * 0.5 + 0.5) - cameraProj.x) / cosAng;
tc = (eye.z + vray.z * dist2) > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract;
}
// Light
@ -221,22 +260,25 @@ void main() {
#else
fragColor.rgb = mix(refracted, reflectedEnv, waterReflect * fresnel);
#endif
// Water color tint - blend rather than multiply to preserve brightness
float colorMix = isSky ? 0.7 : 0.5;
fragColor.rgb = mix(fragColor.rgb, fragColor.rgb * waterColor, colorMix * horizonFactor);
fragColor.rgb = mix(fragColor.rgb, waterColor, colorMix);
// Blinn-Phong specular using half-vector, faded at horizon
vec3 h = normalize(v + ld);
float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0);
fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0) * horizonFactor;
// Depth fog - blend toward waterColor with depth, faded at horizon
float depthFog = clamp(-(p.z - waterLevel) * waterDensity, 0.0, 0.9);
fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor);
float waterColumn = isSky ? 1.0 : max(length(p - eye) - tWater, 0.0);
float depthFog = 1.0 - exp(-waterColumn * waterDensity * 0.006);
vec3 surfaceCol = mix(waterColor, reflectedEnv, waterReflect * fresnel);
fragColor.rgb = mix(fragColor.rgb, surfaceCol, min(depthFog, 0.9));
// Alpha fades smoothly at horizon instead of hard cut
fragColor.a = isSky ? horizonFactor : clamp(abs(p.z - waterLevel) * 5.0, 0.0, 1.0);
// Foam - based on actual geometry depth below water surface
float fd = isSky ? 1.0 : abs(p.z - waterLevel);
if (fd < 0.1) {
// Skydome sits 3.5 below the camera (_skydomeMatrix), so its horizon
// appears where the ray z reaches -3.5 / domeRadius
float horizonFade = clamp((-3.5 / (farPlane * 0.95) - vray.z) * 20.0, 0.0, 1.0);
fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0 + 0.1, 0.0, 1.0);
float waveH = p.z - geomZ; // displacement already applied above
float band = 0.1 + tWater * 0.003;
float fd = isSky ? 1.0 : abs(geomZ - (waterLevel + waveH));
if (fd < band) {
// Based on foam by Owen Deery
// http://fire-face.com/personal/water
// Distance-based LOD blurs foam at range to reduce noise
@ -245,8 +287,10 @@ void main() {
vec2 foamUV1 = tcnor1 * 4.0 + vec2(-speed / 35.0, speed / 45.0);
vec3 foamMask0 = textureLod(sfoam, foamUV0, foamLod).rgb;
vec3 foamMask1 = textureLod(sfoam, foamUV1, foamLod).rgb;
float foamStrength = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0);
float fac = (1.0 - (fd * (1.0 / 0.1))) * horizonFactor;
fragColor.rgb = mix(fragColor.rgb, mix(fragColor.rgb, waterColor + 0.2, foamStrength), clamp(fac, 0.0, 1.0) * 0.5);
float foamMask = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0);
float fac = clamp(1.0 - fd / band, 0.0, 1.0);
vec3 foamCol = mix(waterColor, vec3(1.0), 0.6);
fragColor.rgb = mix(fragColor.rgb, foamCol, fac * foamMask * 0.8);
fragColor.a = max(fragColor.a, fac * foamMask);
}
}

View File

@ -34,7 +34,7 @@
},
{
"name": "ld",
"link": "_lightDirection"
"link": "_sunDirection"
},
{
"name": "invVP",

View File

@ -18,6 +18,13 @@ import iron.object.LightObject;
import iron.object.MeshObject;
import iron.object.Uniforms;
import iron.object.Clipmap;
#if lnx_radix
import haxe.io.FPHelper;
import radix.Radix;
#end
#if lnx_occlusion_culling
import iron.culling.CullSystem;
#end
#if lnx_vr
import iron.math.Vec4;
import iron.math.Mat4;
@ -27,17 +34,19 @@ import iron.math.Quat;
class RenderPath {
public static var active: RenderPath;
#if lnx_vr
static var vrSimulateMode: Bool = false;
static var vrCameraOffsetSet:Bool = false;
static var vrCameraOffset:Vec4 = new Vec4();
static var wasVRPresenting:Bool = false;
public static var vrCalibrationPosition:Vec4 = null;
public static var vrCalibrationRotation:iron.math.Quat = null;
public static var vrCalibrationSaved:Bool = false;
public static var vrCenterCameraWorld:Mat4 = null;
static var vrOriginalSuperSample:Float = -1.0;
#end
static var vrSimulateMode: Bool = false;
public static inline function isVRPresenting(): Bool {
#if (kha_webgl && lnx_vr)
return kha.vr.VrInterface.instance != null && kha.vr.VrInterface.instance.IsPresenting();
@ -45,17 +54,11 @@ class RenderPath {
return false;
#end
}
public static inline function isVRSimulateMode(): Bool {
return vrSimulateMode;
}
// TODO: done remove safely
public static inline function debugLog(msg: String, once: Bool = true): Void {
return;
}
#end
public var frameScissor = false;
public var frameScissorX = 0;
public var frameScissorY = 0;
@ -81,6 +84,18 @@ class RenderPath {
var renderToXRFramebuffer = false;
#end
public var drawOrder = DrawOrder.Distance;
#if lnx_frustum_culling
public var frustumCulling = true;
#else
public var frustumCulling = false;
#end
#if lnx_occlusion_culling
public var occlusionCulling = true;
#else
public var occlusionCulling = false;
#end
public var paused = false;
public var ready(get, never): Bool;
function get_ready(): Bool { return loading == 0; }
@ -95,6 +110,8 @@ class RenderPath {
var lastH = 0;
var bindParams: Array<String>;
var meshesSorted: Bool;
var cullComputed: Bool;
var cullFlagsReset: Bool;
var scissorSet = false;
var viewportScaled = false;
var lastFrameTime = 0.0;
@ -162,24 +179,24 @@ class RenderPath {
var appW = iron.App.w();
var appH = iron.App.h();
// use native XR framebuffer dimensions
#if (kha_webgl && lnx_vr)
if (kha.vr.VrInterface.instance != null) {
var vr = kha.vr.VrInterface.instance;
var isPresenting = vr != null && vr.IsPresenting();
// save/restore camera position between modes
if (!wasVRPresenting && isPresenting) {
if (Scene.active != null && Scene.active.camera != null) {
if (vrCalibrationPosition == null) vrCalibrationPosition = new Vec4();
if (vrCalibrationRotation == null) vrCalibrationRotation = new Quat();
vrCalibrationPosition.setFrom(Scene.active.camera.transform.loc);
vrCalibrationRotation.setFrom(Scene.active.camera.transform.rot);
vrCalibrationSaved = true;
}
// save original super sampling for later
vrOriginalSuperSample = leenkx.renderpath.Inc.superSample;
@ -196,18 +213,18 @@ class RenderPath {
#if (kha_webgl && lnx_vr)
iron.system.Time.vrFrameTime = -1.0;
#end
if (vrCalibrationSaved && Scene.active != null && Scene.active.camera != null) {
Scene.active.camera.transform.loc.setFrom(vrCalibrationPosition);
Scene.active.camera.transform.rot.setFrom(vrCalibrationRotation);
Scene.active.camera.buildMatrix();
Scene.active.camera.buildProjection();
}
// restore original super sampling from simulate mode
if (vrOriginalSuperSample >= 0.0) {
leenkx.renderpath.Inc.superSample = vrOriginalSuperSample;
for (rt in renderTargets) {
if (rt.raw.width == 0 && rt.raw.scale != null) {
rt.raw.scale = vrOriginalSuperSample;
@ -216,13 +233,13 @@ class RenderPath {
resize();
vrOriginalSuperSample = -1.0;
}
// reset offset for next session
vrCameraOffsetSet = false;
vrCameraOffset = null;
}
wasVRPresenting = isPresenting;
if (isPresenting) {
// TODO: re-investigate using super sampling to avoid pixelation in simulate mode while giving max quality in headset
if (vrOriginalSuperSample >= 0.0 && leenkx.renderpath.Inc.superSample != 4.0) {
@ -234,7 +251,7 @@ class RenderPath {
}
resize();
}
var xrVr: kha.js.vr.VrInterface = cast vr;
if (xrVr.xrGLLayer != null) {
appW = xrVr.xrGLLayer.framebufferWidth;
@ -304,6 +321,8 @@ class RenderPath {
currentD = 1;
currentFace = -1;
meshesSorted = false;
cullComputed = false;
cullFlagsReset = false;
sun = null;
for (l in Scene.active.lights) {
@ -384,7 +403,7 @@ class RenderPath {
if (currentG != null) end();
currentG = g;
additionalTargets = additionalRenderTargets;
// we still bind but skip begin() when explicitly rendering to XR framebuffer (renderToXRFramebuffer flag)
#if lnx_vr
if (!renderToXRFramebuffer) {
@ -405,7 +424,7 @@ class RenderPath {
currentG.disableScissor();
scissorSet = false;
}
#if lnx_vr
if (beginCalled) {
currentG.end();
@ -470,6 +489,60 @@ class RenderPath {
rt.image.generateMipmaps(1000);
}
#if lnx_radix
static var sortDistanceKeys = new Array<Int>();
static var sortDistanceMeshesBuf = new Array<MeshObject>();
static var sortDistanceKeysBuf = new Array<Int>();
static var sortIndexKeys = new Array<Int>();
static var sortIndexMeshesBuf = new Array<MeshObject>();
static var sortIndexKeysBuf = new Array<Int>();
public static function sortMeshesDistance(meshes: Array<MeshObject>) {
var n = meshes.length;
var keys = sortDistanceKeys;
var meshesBuf = sortDistanceMeshesBuf;
var keysBuf = sortDistanceKeysBuf;
if (keys.length < n) {
keys.resize(n);
meshesBuf.resize(n);
keysBuf.resize(n);
}
for (i in 0...n) {
var m = meshes[i];
var key = FPHelper.floatToI32(m.cameraDistance) >>> 1;
#if rp_depth_texture
if (m.depthRead) key |= 0x80000000;
#end
keys[i] = key;
}
Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf);
if (meshesBuf.length > n) meshesBuf.resize(n);
}
public static function sortMeshesIndex(meshes: Array<MeshObject>) {
var n = meshes.length;
var keys = sortIndexKeys;
var meshesBuf = sortIndexMeshesBuf;
var keysBuf = sortIndexKeysBuf;
if (keys.length < n) {
keys.resize(n);
meshesBuf.resize(n);
keysBuf.resize(n);
}
for (i in 0...n) {
var m = meshes[i];
var si = m.data.sortingIndex;
var key = si < -0x40000000 ? 0 : si > 0x3fffffff ? 0x7fffffff : si + 0x40000000;
#if rp_depth_texture
if (m.depthRead) key |= 0x80000000;
#end
keys[i] = key;
}
Radix.sortCopyKeyed(meshes, keys, meshesBuf, keysBuf);
if (meshesBuf.length > n) meshesBuf.resize(n);
}
#else
static inline function boolToInt(b: Bool): Int {
return b ? 1 : 0;
}
@ -501,6 +574,7 @@ class RenderPath {
return a.data.name > b.data.name ? 1 : -1;
});
}
#end
public function drawMeshes(context: String) {
var isShadows = context == "shadowmap" || context == "shadowmap_transparent";
@ -519,6 +593,11 @@ class RenderPath {
for (i in 0...LightObject.cascadeCount) {
light.setCascade(Scene.active.camera, i);
currentG.viewport(i * step, 0, step, step);
#if lnx_occlusion_culling
if (occlusionCulling && CullSystem.active != null) {
CullSystem.active.cullShadow(light);
}
#end
submitDraw(context);
}
drawn = true;
@ -529,7 +608,14 @@ class RenderPath {
if (context == "mesh") LightObject.updateClusters(Scene.active.camera);
#end
if (!drawn) submitDraw(context);
if (!drawn) {
#if lnx_occlusion_culling
if (isShadows && occlusionCulling && CullSystem.active != null) {
CullSystem.active.cullShadow(light);
}
#end
submitDraw(context);
}
// Callbacks to specific context
if (contextEvents != null) {
@ -546,7 +632,34 @@ class RenderPath {
var meshes = Scene.active.meshes;
MeshObject.lastPipeline = null;
if (!meshesSorted && camera != null) { // Order max once per frame for now
#if lnx_occlusion_culling
if (!cullFlagsReset && !occlusionCulling) {
cullFlagsReset = true;
for (m in meshes) {
m.occlusionVisible = true;
m.shadowOcclusionVisible = true;
}
}
#end
if (!cullComputed && (context == "mesh" || context == "depth")) {
#if lnx_occlusion_culling
if (occlusionCulling) {
if (CullSystem.active == null) CullSystem.init();
if (CullSystem.active != null && camera != null) {
if (CullSystem.active.proxiesDirty) {
CullSystem.active.loadOccluderProxies();
CullSystem.active.proxiesDirty = false;
}
CullSystem.active.cull(camera);
}
}
#end
cullComputed = true;
}
var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent";
if (!meshesSorted && camera != null && !isShadowCtx) {
var camX = camera.transform.worldx();
var camY = camera.transform.worldy();
var camZ = camera.transform.worldz();
@ -571,8 +684,20 @@ class RenderPath {
static inline function meshRenderLoop(g: Graphics, context: String, _bindParams: Array<String>, _meshes: Array<MeshObject>) {
var isReadingDepth = false;
#if lnx_occlusion_culling
var cullSys = CullSystem.active;
#end
for (m in _meshes) {
#if lnx_occlusion_culling
if (cullSys != null && !m.occlusionVisible && (context == "mesh" || context == "depth" || context == "translucent")) {
continue;
}
if (cullSys != null && !m.shadowOcclusionVisible && (context == "shadowmap" || context == "shadowmap_transparent")) {
continue;
}
#end
#if rp_depth_texture
// First mesh that reads depth
if (!isReadingDepth && m.depthRead) {
@ -692,7 +817,7 @@ class RenderPath {
if (vr == null || vr._glContext == null || vr.xrGLLayer == null) {
return;
}
var gl: js.html.webgl.WebGL2RenderingContext = cast vr._glContext;
var source = renderTargets.get(sourceTarget);
if (source == null) {
@ -703,21 +828,21 @@ class RenderPath {
return;
}
// trace('Framebuffer OK');
renderToXRFramebuffer = true;
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER, vr.xrGLLayer.framebuffer);
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER, sourceFB);
var readStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER);
var drawStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER);
if (readStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE ||
if (readStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE ||
drawStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE) {
return;
}
var halfWidth = Std.int(source.image.width / 2);
var fullHeight = source.image.height;
if (vr._leftViewport != null) {
var vp = vr._leftViewport;
gl.blitFramebuffer(
@ -736,7 +861,7 @@ class RenderPath {
js.html.webgl.WebGL2RenderingContext.LINEAR
);
}
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER, null);
renderToXRFramebuffer = false;
#end
@ -749,24 +874,24 @@ class RenderPath {
if (currentG == null && frameG != null) {
currentG = frameG;
}
var appw = iron.App.w();
var apph = iron.App.h();
var g = currentG;
// get render target dimensions not App.w/h gbuffer is scaled in simulate mode with supersampling
var gbuffer0 = renderTargets.get("gbuffer0");
var actualWidth = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.width : appw;
var actualHeight = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.height : apph;
var actualHalfWidth = Std.int(actualWidth / 2);
var vrFBWidth = actualWidth;
var vrFBHeight = actualHeight;
var vrHalfWidth = actualHalfWidth;
var isVRPresenting = false;
vrSimulateMode = false;
var vr:Dynamic = null;
var vrExists = false;
#if (kha_webgl && lnx_vr)
@ -779,7 +904,7 @@ class RenderPath {
if (vrExists && vr != null && vr.IsPresenting()) {
vrSimulateMode = false;
isVRPresenting = true;
// get framebuffer dimensions from XR layer
#if (kha_webgl && lnx_vr)
var xrVr: kha.js.vr.VrInterface = cast vr;
@ -789,16 +914,16 @@ class RenderPath {
vrHalfWidth = Std.int(vrFBWidth / 2);
}
#end
if (Scene.active == null || Scene.active.camera == null) {
return;
}
#if (kha_webgl && lnx_vr)
if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity();
vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world);
#end
// LEFT EYE
// HMD center for room scale position tracking
#if (kha_webgl && lnx_vr)
@ -808,10 +933,10 @@ class RenderPath {
if (viewerTransform != null && viewerTransform.position != null) {
// VR present calibration is used to position objects in world space not the camera
var pos = viewerTransform.position;
// camera follows headset directly in local floor space
Scene.active.camera.transform.loc.set(pos.x, pos.y, pos.z);
if (viewerTransform.orientation != null) {
Scene.active.camera.transform.rot.set(
viewerTransform.orientation.x,
@ -825,7 +950,7 @@ class RenderPath {
}
iron.system.VRController.updatePoses();
#end
Scene.active.camera.V.self = vr.GetViewMatrix(0);
Scene.active.camera.P.self = vr.GetProjectionMatrix(0);
Scene.active.camera.VP.setFrom(Scene.active.camera.P);
@ -835,7 +960,7 @@ class RenderPath {
var renderWidth = actualWidth;
var renderHeight = actualHeight;
var renderHalfWidth = actualHalfWidth;
// left half of render target
g.viewport(0, 0, renderHalfWidth, renderHeight);
g.scissor(0, 0, renderHalfWidth, renderHeight);
@ -847,12 +972,12 @@ class RenderPath {
Scene.active.camera.VP.setFrom(Scene.active.camera.P);
Scene.active.camera.VP.multmat(Scene.active.camera.V);
Scene.active.camera.buildMatrix();
// right half of render target
g.viewport(renderHalfWidth, 0, renderHalfWidth, renderHeight);
g.scissor(renderHalfWidth, 0, renderHalfWidth, renderHeight);
drawMeshes();
// restore for post-processing
g.disableScissor();
g.viewport(0, 0, renderWidth, renderHeight);
@ -865,7 +990,7 @@ class RenderPath {
if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity();
vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world);
#end
Scene.active.camera.buildProjection(actualHalfWidth / actualHeight);
Scene.active.camera.transform.move(Scene.active.camera.right(), -ipd_offset);
@ -908,7 +1033,9 @@ class RenderPath {
#end
Data.getShader(shaderPath[0], shaderPath[1], function(res: ShaderData) {
cc.context = res.getContext(shaderPath[2]);
if (res != null) {
cc.context = res.getContext(shaderPath[2]);
}
loading--;
});
}

View File

@ -1003,7 +1003,7 @@ class Scene {
}
if (o.vertex_groups != null) {
cast(object, MeshObject).vertexGroups = new Map();
object.vertexGroups = new Map();
for (p in o.vertex_groups) {
var verts:Array<iron.math.Vec4> = [];
@ -1014,7 +1014,7 @@ class Scene {
verts.push(new iron.math.Vec4(data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 1.0));
}
}
cast(object, MeshObject).vertexGroups.set(p.name, verts);
object.vertexGroups.set(p.name, verts);
}
}

View File

@ -0,0 +1,20 @@
package iron.culling;
class BVHNode {
public var minX: Float = 0;
public var minY: Float = 0;
public var minZ: Float = 0;
public var maxX: Float = 0;
public var maxY: Float = 0;
public var maxZ: Float = 0;
public var centerX: Float = 0;
public var centerY: Float = 0;
public var centerZ: Float = 0;
public var isLeaf: Bool = false;
public var leftChild: Int = -1;
public var rightChild: Int = -1;
public var firstObject: Int = 0;
public var objectCount: Int = 0;
public function new() {}
}

View File

@ -0,0 +1,62 @@
package iron.culling;
class ContributionCull {
public static var minPixelRadius: Float = 2.0;
public static var enabled: Bool = false;
public static function passes(
centerX: Float, centerY: Float, centerZ: Float,
radius: Float,
camX: Float, camY: Float, camZ: Float,
focalLengthPx: Float
): Bool {
if (!enabled || minPixelRadius <= 0.0 || focalLengthPx <= 0.0) return true;
var dx = centerX - camX;
var dy = centerY - camY;
var dz = centerZ - camZ;
var dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= radius) return true;
var projectedRadiusPx = focalLengthPx * radius / dist;
return projectedRadiusPx >= minPixelRadius;
}
public static function passesNode(
node: BVHNode,
camX: Float, camY: Float, camZ: Float,
focalLengthPx: Float
): Bool {
if (!enabled || minPixelRadius <= 0.0 || focalLengthPx <= 0.0) return true;
var cx = (node.minX + node.maxX) * 0.5;
var cy = (node.minY + node.maxY) * 0.5;
var cz = (node.minZ + node.maxZ) * 0.5;
var ex = (node.maxX - node.minX) * 0.5;
var ey = (node.maxY - node.minY) * 0.5;
var ez = (node.maxZ - node.minZ) * 0.5;
var radius = Math.sqrt(ex * ex + ey * ey + ez * ez);
if (camX >= node.minX && camX <= node.maxX &&
camY >= node.minY && camY <= node.maxY &&
camZ >= node.minZ && camZ <= node.maxZ) {
return true;
}
var dx = cx - camX;
var dy = cy - camY;
var dz = cz - camZ;
var dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= radius) return true;
var projectedRadiusPx = focalLengthPx * radius / dist;
return projectedRadiusPx >= minPixelRadius;
}
public static function computeFocalLengthPx(fov: Float, screenHeight: Int): Float {
return screenHeight / (2.0 * Math.tan(fov * 0.5));
}
}

View File

@ -0,0 +1,373 @@
package iron.culling;
import iron.object.MeshObject;
import iron.Scene;
import kha.arrays.Float32Array;
class CullData {
static inline var STRIDE = 16;
static inline var BASIS_STRIDE = 12;
public var data: Float32Array;
public var prevData: Float32Array;
public var uidData: kha.arrays.Int32Array;
public var prevUidData: kha.arrays.Int32Array;
var basisData: Float32Array;
var prevBasisData: Float32Array;
public var count: Int = 0;
public var prevCount: Int = 0;
public var anyMoved: Bool = false;
public var anyFlagsChanged: Bool = false;
public var orderChanged: Bool = false;
public var meshes: Array<MeshObject> = [];
public var slotsDirty: Bool = true;
public var slotsRebuilt: Bool = false;
public var visibleList: Array<Int>;
public var visibleCount: Int = 0;
var staticFlagsCache: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
var matTranspCache: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
var matSnapshots: haxe.ds.IntMap<haxe.ds.Vector<iron.data.MaterialData>> = new haxe.ds.IntMap();
var prevUidMap: haxe.ds.IntMap<Int> = new haxe.ds.IntMap<Int>();
public function new() {
visibleList = [];
}
public inline function stride(): Int {
return STRIDE;
}
public function ensureCapacity(cap: Int) {
if (data == null || data.length < cap * STRIDE) {
var newCap = cap;
if (data != null) newCap = Std.int(Math.max(cap, data.length / STRIDE * 2));
var oldData = data;
var oldPrev = prevData;
var oldUid = uidData;
var oldPrevUid = prevUidData;
var oldBasis = basisData;
var oldPrevBasis = prevBasisData;
data = new Float32Array(newCap * STRIDE);
prevData = new Float32Array(newCap * STRIDE);
uidData = new kha.arrays.Int32Array(newCap);
prevUidData = new kha.arrays.Int32Array(newCap);
basisData = new Float32Array(newCap * BASIS_STRIDE);
prevBasisData = new Float32Array(newCap * BASIS_STRIDE);
if (oldData != null) {
var copyLen = oldData.length;
for (i in 0...copyLen) data[i] = oldData[i];
}
if (oldPrev != null) {
var copyLen = oldPrev.length;
for (i in 0...copyLen) prevData[i] = oldPrev[i];
}
if (oldUid != null) {
var copyLen = oldUid.length;
for (i in 0...copyLen) uidData[i] = oldUid[i];
}
if (oldPrevUid != null) {
var copyLen = oldPrevUid.length;
for (i in 0...copyLen) prevUidData[i] = oldPrevUid[i];
}
if (oldBasis != null) {
var copyLen = oldBasis.length;
for (i in 0...copyLen) basisData[i] = oldBasis[i];
}
if (oldPrevBasis != null) {
var copyLen = oldPrevBasis.length;
for (i in 0...copyLen) prevBasisData[i] = oldPrevBasis[i];
}
}
}
public function syncFromScene() {
var sceneMeshes = Scene.active.meshes;
slotsRebuilt = false;
if (slotsDirty || meshes.length != sceneMeshes.length) {
meshes = sceneMeshes.copy();
slotsDirty = false;
slotsRebuilt = true;
staticFlagsCache.clear();
matTranspCache.clear();
matSnapshots.clear();
}
count = meshes.length;
ensureCapacity(count);
var tmpD = prevData; prevData = data; data = tmpD;
var tmpU = prevUidData; prevUidData = uidData; uidData = tmpU;
var tmpB = prevBasisData; prevBasisData = basisData; basisData = tmpB;
prevUidMap.clear();
var prevUidMapBuilt = false;
anyMoved = false;
anyFlagsChanged = false;
orderChanged = false;
for (i in 0...count) {
var mesh = meshes[i];
var t = mesh.transform;
var uid = mesh.uid;
var p = mesh.parent;
while (p != null) {
p.transform.update();
p = p.parent;
}
t.update();
var off = i * STRIDE;
var r = t.radius;
var wx = t.worldx();
var wy = t.worldy();
var wz = t.worldz();
var radiusScale = mesh.data.isSkinned ? 2.0 : 1.0;
#if lnx_gpu_particles
if (mesh.particleSystems != null || mesh.particleOwner != null) radiusScale *= 1000;
#end
if (mesh.data.geom.instanced) radiusScale *= 100;
r *= radiusScale;
data[off + 3] = r;
data[off + 4] = wx;
data[off + 5] = wy;
data[off + 6] = wz;
var w = t.world;
var prevIdx: Null<Int> = null;
if (prevUidData != null && i < prevCount && prevUidData[i] == uid) {
prevIdx = i;
} else {
if (!prevUidMapBuilt) {
prevUidMapBuilt = true;
if (prevUidData != null) {
for (pi in 0...prevCount) prevUidMap.set(prevUidData[pi], pi);
}
}
prevIdx = prevUidMap.get(uid);
}
if (prevIdx != null && prevIdx != i) orderChanged = true;
var aabbCached = false;
if (prevIdx != null) {
var poff = prevIdx * STRIDE;
var pb = prevIdx * BASIS_STRIDE;
if (prevData[poff + 4] == wx && prevData[poff + 5] == wy && prevData[poff + 6] == wz &&
prevBasisData[pb + 0] == w._00 && prevBasisData[pb + 1] == w._10 && prevBasisData[pb + 2] == w._20 &&
prevBasisData[pb + 3] == w._01 && prevBasisData[pb + 4] == w._11 && prevBasisData[pb + 5] == w._21 &&
prevBasisData[pb + 6] == w._02 && prevBasisData[pb + 7] == w._12 && prevBasisData[pb + 8] == w._22 &&
prevBasisData[pb + 9] == t.dim.x && prevBasisData[pb + 10] == t.dim.y && prevBasisData[pb + 11] == t.dim.z) {
data[off + 0] = prevData[poff + 0];
data[off + 1] = prevData[poff + 1];
data[off + 2] = prevData[poff + 2];
data[off + 8] = prevData[poff + 8];
data[off + 9] = prevData[poff + 9];
data[off + 10] = prevData[poff + 10];
aabbCached = true;
}
}
if (!aabbCached) {
var r00 = w._00, r10 = w._10, r20 = w._20;
var r01 = w._01, r11 = w._11, r21 = w._21;
var r02 = w._02, r12 = w._12, r22 = w._22;
var lenX = Math.sqrt(r00 * r00 + r01 * r01 + r02 * r02);
var lenY = Math.sqrt(r10 * r10 + r11 * r11 + r12 * r12);
var lenZ = Math.sqrt(r20 * r20 + r21 * r21 + r22 * r22);
if (lenX > 1e-8) { r00 /= lenX; r01 /= lenX; r02 /= lenX; }
if (lenY > 1e-8) { r10 /= lenY; r11 /= lenY; r12 /= lenY; }
if (lenZ > 1e-8) { r20 /= lenZ; r21 /= lenZ; r22 /= lenZ; }
var hx = t.dim.x * 0.5 * radiusScale;
var hy = t.dim.y * 0.5 * radiusScale;
var hz = t.dim.z * 0.5 * radiusScale;
var aabbMinX = 1e20, aabbMinY = 1e20, aabbMinZ = 1e20;
var aabbMaxX = -1e20, aabbMaxY = -1e20, aabbMaxZ = -1e20;
for (c in 0...8) {
var cx = (c & 1) != 0 ? hx : -hx;
var cy = (c & 2) != 0 ? hy : -hy;
var cz = (c & 4) != 0 ? hz : -hz;
var px = cx * r00 + cy * r10 + cz * r20 + wx;
var py = cx * r01 + cy * r11 + cz * r21 + wy;
var pz = cx * r02 + cy * r12 + cz * r22 + wz;
if (px < aabbMinX) aabbMinX = px;
if (py < aabbMinY) aabbMinY = py;
if (pz < aabbMinZ) aabbMinZ = pz;
if (px > aabbMaxX) aabbMaxX = px;
if (py > aabbMaxY) aabbMaxY = py;
if (pz > aabbMaxZ) aabbMaxZ = pz;
}
data[off + 0] = aabbMinX;
data[off + 1] = aabbMinY;
data[off + 2] = aabbMinZ;
data[off + 8] = aabbMaxX;
data[off + 9] = aabbMaxY;
data[off + 10] = aabbMaxZ;
}
var flags: Int;
var cachedFlags = staticFlagsCache.get(uid);
if (cachedFlags != null) {
flags = cachedFlags;
} else {
flags = 0;
if (mesh.raw != null && mesh.raw.properties != null) {
for (prop in mesh.raw.properties) {
if (prop.name == "occluder" && prop.value == true) {
flags |= 1;
break;
}
}
}
if ((flags & 1) == 0 && mesh.data != null &&
mesh.data.raw != null && mesh.data.raw.occluder_proxy != null) {
flags |= 1;
}
if (mesh.raw != null && mesh.raw.visible_shadow != false) flags |= 4;
staticFlagsCache.set(uid, flags);
}
var mats = mesh.materials;
var snap = matSnapshots.get(uid);
var matsChanged = snap == null || mats == null || snap.length != mats.length;
if (!matsChanged) {
for (mi in 0...mats.length) {
if (snap[mi] != mats[mi]) { matsChanged = true; break; }
}
}
if (matsChanged) {
var tbit = 0;
if (mats != null) {
for (mi in 0...mats.length) {
var mat = mats[mi];
if (mat == null || mat.shader == null) continue;
for (sc in mat.shader.contexts) {
var sr = sc.raw;
if (sr != null && sr.name != null && sr.name.substr(0, 4) == "mesh" &&
(sr.depth_write != true || sr.blend_source != null)) {
tbit = 32;
break;
}
}
if (tbit != 0) break;
}
}
matTranspCache.set(uid, tbit);
var nsnap = new haxe.ds.Vector<iron.data.MaterialData>(mats != null ? mats.length : 0);
for (mi in 0...nsnap.length) nsnap[mi] = mats[mi];
matSnapshots.set(uid, nsnap);
}
var tbitCached = matTranspCache.get(uid);
if (tbitCached != null) flags |= tbitCached;
if (mesh.visible) flags |= 8;
if (radiusScale != 1.0) flags |= 16;
if (prevIdx != null) {
var poff = prevIdx * STRIDE;
if (prevData[poff + 0] != data[off + 0] ||
prevData[poff + 1] != data[off + 1] ||
prevData[poff + 2] != data[off + 2] ||
prevData[poff + 8] != data[off + 8] ||
prevData[poff + 9] != data[off + 9] ||
prevData[poff + 10] != data[off + 10]) {
flags |= 2;
anyMoved = true;
}
} else {
flags |= 2;
anyMoved = true;
anyFlagsChanged = true;
}
if (prevIdx != null) {
var poff = prevIdx * STRIDE;
if (((flags ^ Std.int(prevData[poff + 7])) & ~2) != 0) anyFlagsChanged = true;
}
data[off + 7] = flags;
var boff = i * BASIS_STRIDE;
basisData[boff + 0] = w._00;
basisData[boff + 1] = w._10;
basisData[boff + 2] = w._20;
basisData[boff + 3] = w._01;
basisData[boff + 4] = w._11;
basisData[boff + 5] = w._21;
basisData[boff + 6] = w._02;
basisData[boff + 7] = w._12;
basisData[boff + 8] = w._22;
basisData[boff + 9] = t.dim.x;
basisData[boff + 10] = t.dim.y;
basisData[boff + 11] = t.dim.z;
uidData[i] = uid;
}
prevCount = count;
}
public inline function getMinX(i: Int): Float {
return data[i * STRIDE + 0];
}
public inline function getMinY(i: Int): Float {
return data[i * STRIDE + 1];
}
public inline function getMinZ(i: Int): Float {
return data[i * STRIDE + 2];
}
public inline function getRadius(i: Int): Float {
return data[i * STRIDE + 3];
}
public inline function getCenterX(i: Int): Float {
return data[i * STRIDE + 4];
}
public inline function getCenterY(i: Int): Float {
return data[i * STRIDE + 5];
}
public inline function getCenterZ(i: Int): Float {
return data[i * STRIDE + 6];
}
public inline function getFlags(i: Int): Int {
return Std.int(data[i * STRIDE + 7]);
}
public inline function isOccluder(i: Int): Bool {
return (getFlags(i) & 1) != 0;
}
public inline function isDynamic(i: Int): Bool {
return (getFlags(i) & 2) != 0;
}
public inline function isShadowCaster(i: Int): Bool {
return (getFlags(i) & 4) != 0;
}
public inline function isMeshVisible(i: Int): Bool {
return (getFlags(i) & 8) != 0;
}
public inline function getObjectID(i: Int): Int {
return uidData[i];
}
public inline function getMaxX(i: Int): Float {
return data[i * STRIDE + 8];
}
public inline function getMaxY(i: Int): Float {
return data[i * STRIDE + 9];
}
public inline function getMaxZ(i: Int): Float {
return data[i * STRIDE + 10];
}
public function clearVisible() {
visibleCount = 0;
if (visibleList.length > 0) visibleList.resize(0);
}
public function addVisible(objectIndex: Int) {
visibleList.push(objectIndex);
visibleCount++;
}
}

View File

@ -0,0 +1,773 @@
package iron.culling;
import iron.object.CameraObject;
import iron.object.FrustumPlane;
import iron.Scene;
import iron.RenderPath;
import iron.math.Mat4;
class CullSystem {
public static var active: CullSystem = null;
public var tier: CullTier;
public var cullData: CullData;
public var rasterizer: SoftwareOcclusionRasterizer;
public var shadowRasterizer: SoftwareOcclusionRasterizer;
public var occluderProxies: Array<OccluderProxy>;
public var bvh: ObjectBVH;
public var temporal: TemporalState;
public var enabled: Bool = true;
public var occlusionEnabled: Bool = true;
public var bvhDirty: Bool = true;
public var proxiesDirty: Bool = true;
var dataSyncedFrame: Int = -1;
var focalLengthPx: Float = 500.0;
var orthoPxPerUnit: Float = 0.0;
var screenW: Int = 1920;
var screenH: Int = 1080;
public static var maxOccluders: Int = 48;
public static var maxOccluderTris: Int = 4000;
public static var maxTrisPerProxy: Int = 768;
public static var maxShadowOccluders: Int = 24;
public static var maxShadowOccluderTris: Int = 1536;
public static var boxOccluders: Bool = false;
public static var shadowBoxOccluders: Bool = false;
public static var shadowBoxShrink: Float = 0.7;
public static var shadowCullingEnabled: Bool = true;
public static var minOccluderPx: Float = 24.0;
public static var boxShrink: Float = 0.5;
public static var depthEps: Float = 1e-6;
public static var shadowCullInterval: Int = 2;
static inline var MAX_SHADOW_SLOTS = 8;
var shadowPasses: haxe.ds.ObjectMap<iron.object.LightObject, Array<ShadowPassSlot>> = new haxe.ds.ObjectMap();
var shadowCallIdx: Map<iron.object.LightObject, Int> = new Map();
var shadowCallFrame: Map<iron.object.LightObject, Int> = new Map();
var lastShadowSlot: ShadowPassSlot = null;
var lastShadowFrame: Int = -1;
var lastShadowCallIdx: Int = -1;
#if lnx_csm
var shadowVps: haxe.ds.ObjectMap<iron.object.LightObject, Mat4> = new haxe.ds.ObjectMap();
#end
var sceneRev: Int = 0;
var sceneRevFrame: Int = -1;
var frameCounter: Int = 0;
var bvhSyncedFrame: Int = -1; // Frame when BVH was built/refit
var occlusionFrustumPlanes: Array<FrustumPlane> = null;
var frustumPlanes: Array<FrustumPlane> = null;
var shadowFrustumPlanes: Array<FrustumPlane> = null;
var uidToProxy: haxe.ds.IntMap<OccluderProxy> = new haxe.ds.IntMap<OccluderProxy>();
var uidScan: haxe.ds.IntMap<Bool> = new haxe.ds.IntMap<Bool>();
var prevVP: Mat4 = null;
#if lnx_taa
var unjitteredVP: Mat4 = null;
#end
public var shadowFlagsValid: Bool = false;
var inFrustumList: Array<Int> = [];
var selIdx: Array<Int> = [];
var selScore: Array<Float> = [];
var selCount: Int = 0;
public function new() {
cullData = new CullData();
rasterizer = new SoftwareOcclusionRasterizer(128, 64);
shadowRasterizer = new SoftwareOcclusionRasterizer(128, 128);
occluderProxies = [];
temporal = new TemporalState();
tier = detectTier();
}
static inline var MAT_EPS = 1e-7;
static inline function matEquals(a: Mat4, b: Mat4): Bool {
return
Math.abs(a._00 - b._00) < MAT_EPS && Math.abs(a._10 - b._10) < MAT_EPS &&
Math.abs(a._20 - b._20) < MAT_EPS && Math.abs(a._30 - b._30) < MAT_EPS &&
Math.abs(a._01 - b._01) < MAT_EPS && Math.abs(a._11 - b._11) < MAT_EPS &&
Math.abs(a._21 - b._21) < MAT_EPS && Math.abs(a._31 - b._31) < MAT_EPS &&
Math.abs(a._02 - b._02) < MAT_EPS && Math.abs(a._12 - b._12) < MAT_EPS &&
Math.abs(a._22 - b._22) < MAT_EPS && Math.abs(a._32 - b._32) < MAT_EPS &&
Math.abs(a._03 - b._03) < MAT_EPS && Math.abs(a._13 - b._13) < MAT_EPS &&
Math.abs(a._23 - b._23) < MAT_EPS && Math.abs(a._33 - b._33) < MAT_EPS;
}
public static function detectTier(): CullTier {
#if (target.threaded)
return CPUMultiThread;
#else
return CPUSingleThread;
#end
}
public function cull(camera: CameraObject) {
frameCounter = RenderPath.active != null ? RenderPath.active.frame : frameCounter + 1;
if (!enabled || Scene.active == null || Scene.active.meshes.length == 0) {
if (Scene.active != null) {
var meshes = Scene.active.meshes;
for (i in 0...meshes.length) {
meshes[i].occlusionVisible = true;
meshes[i].shadowOcclusionVisible = true;
}
}
return;
}
var rp = RenderPath.active;
if (rp != null) {
screenW = rp.currentW;
screenH = rp.currentH;
}
var targetW = Std.int(Math.max(128, Math.min(512, screenW >> 4)));
var targetH = Std.int(Math.max(64, Math.min(256, screenH >> 4)));
if (rasterizer.width < (targetW >> 1) || rasterizer.width > (targetW << 1) ||
rasterizer.height < (targetH >> 1) || rasterizer.height > (targetH << 1)) {
rasterizer = new SoftwareOcclusionRasterizer(targetW, targetH);
}
if (camera.data.raw.ortho == null) {
focalLengthPx = ContributionCull.computeFocalLengthPx(
camera.data.raw.fov, screenH
);
orthoPxPerUnit = 0.0;
} else {
focalLengthPx = 0;
var o = camera.data.raw.ortho;
var halfH = (o[3] - o[2]) * 0.5;
orthoPxPerUnit = halfH > 1e-9 ? screenH * 0.5 / halfH : 0.0;
}
rasterizer.nearClip = camera.data.raw.ortho == null ?
camera.data.raw.near_plane : 0.0001;
if (dataSyncedFrame != frameCounter) {
cullData.syncFromScene();
dataSyncedFrame = frameCounter;
}
if (cullData.slotsRebuilt) temporal.clear();
if (sceneRevFrame != frameCounter) {
sceneRevFrame = frameCounter;
if (cullData.anyMoved || cullData.anyFlagsChanged || cullData.orderChanged) sceneRev++;
}
var cd = cullData;
var meshes = cd.meshes;
#if lnx_taa
if (unjitteredVP == null) unjitteredVP = Mat4.identity();
unjitteredVP.multmats(camera.noJitterP, camera.V);
var cmpVP = unjitteredVP;
#else
var cmpVP = camera.VP;
#end
var vpSame = prevVP != null && matEquals(cmpVP, prevVP);
if (vpSame && !cd.anyMoved && !cd.anyFlagsChanged && !cd.orderChanged && !bvhDirty) {
return;
}
var needBvhRebuild = bvhDirty;
bvhDirty = false;
if (prevVP == null) prevVP = Mat4.identity();
prevVP.setFrom(cmpVP);
if (occlusionFrustumPlanes == null) {
occlusionFrustumPlanes = [];
for (i in 0...6) occlusionFrustumPlanes.push(new FrustumPlane());
}
{
var vp = camera.VP;
var p = occlusionFrustumPlanes;
p[0].setComponents(vp._03 + vp._00, vp._13 + vp._10, vp._23 + vp._20, vp._33 + vp._30);
p[1].setComponents(vp._03 - vp._00, vp._13 - vp._10, vp._23 - vp._20, vp._33 - vp._30);
p[2].setComponents(vp._03 - vp._01, vp._13 - vp._11, vp._23 - vp._21, vp._33 - vp._31);
p[3].setComponents(vp._03 + vp._01, vp._13 + vp._11, vp._23 + vp._21, vp._33 + vp._31);
p[4].setComponents(vp._02, vp._12, vp._22, vp._32);
p[5].setComponents(vp._03 - vp._02, vp._13 - vp._12, vp._23 - vp._22, vp._33 - vp._32);
}
frustumPlanes = occlusionFrustumPlanes;
var camX = camera.transform.worldx();
var camY = camera.transform.worldy();
var camZ = camera.transform.worldz();
if (bvh == null) bvh = new ObjectBVH();
if (bvhSyncedFrame != frameCounter) {
if (needBvhRebuild || bvh.root < 0) {
bvh.build(cd);
} else if (cd.anyMoved || cd.orderChanged) {
bvh.refit(cd);
}
bvhSyncedFrame = frameCounter;
}
inFrustumList.resize(0);
var planes = frustumPlanes;
var fpx = focalLengthPx;
bvh.traverseFrontToBack(camX, camY, camZ,
function(node: BVHNode): Bool {
if (!ObjectBVH.aabbInFrustum(node, planes)) return true;
if (!ContributionCull.passesNode(node, camX, camY, camZ, fpx)) return true;
return false;
},
function(objIdx: Int, _: Int) {
if ((cd.getFlags(objIdx) & 8) == 0) return;
if (!ContributionCull.passes(
cd.getCenterX(objIdx), cd.getCenterY(objIdx), cd.getCenterZ(objIdx),
cd.getRadius(objIdx), camX, camY, camZ, fpx
)) return;
inFrustumList.push(objIdx);
}
);
cullData.clearVisible();
if (occlusionEnabled) {
selectOccluders(meshes, camX, camY, camZ);
for (i in 0...meshes.length) {
meshes[i].occlusionVisible = true;
}
rasterizer.clear();
if (selCount > 0) rasterizeSelected(meshes, camera.VP);
var trisDone = rasterizer.trianglesRasterized;
if (trisDone > 0) rasterizer.buildHiZ();
if (trisDone > 0) {
testVisibilityBVH(camera.VP, camX, camY, camZ, planes);
} else {
for (k in 0...inFrustumList.length) {
var objIdx = inFrustumList[k];
cd.addVisible(objIdx);
}
}
} else {
for (i in 0...meshes.length) {
meshes[i].occlusionVisible = true;
}
for (k in 0...inFrustumList.length) {
var objIdx = inFrustumList[k];
cd.addVisible(objIdx);
}
}
temporal.advanceFrame();
}
public function cullShadow(light: iron.object.LightObject) {
if (!occlusionEnabled || light == null || Scene.active == null) return;
if (!shadowCullingEnabled) {
var ms = Scene.active.meshes;
for (m in ms) m.shadowOcclusionVisible = true;
return;
}
var frame = RenderPath.active != null ? RenderPath.active.frame : frameCounter;
var vp = light.VP;
if (vp == null) return;
var cd = cullData;
if (dataSyncedFrame != frame) {
cd.syncFromScene();
dataSyncedFrame = frame;
}
if (sceneRevFrame != frame) {
sceneRevFrame = frame;
if (cd.anyMoved || cd.anyFlagsChanged || cd.orderChanged) sceneRev++;
}
var meshes = cd.meshes;
var slots = shadowPasses.get(light);
if (slots == null) {
slots = [];
shadowPasses.set(light, slots);
}
var callIdx = 0;
{
var f = shadowCallFrame.get(light);
callIdx = (f == frame) ? shadowCallIdx.get(light) + 1 : 0;
shadowCallIdx.set(light, callIdx);
shadowCallFrame.set(light, frame);
}
var slotIdx = callIdx < MAX_SHADOW_SLOTS ? callIdx : MAX_SHADOW_SLOTS - 1;
while (slots.length <= slotIdx) slots.push(new ShadowPassSlot());
var slot = slots[slotIdx];
if (slot != null && slot == lastShadowSlot && lastShadowFrame == frame &&
lastShadowCallIdx == callIdx && slot.appliedRev == sceneRev) return;
for (i in 0...meshes.length) {
meshes[i].shadowOcclusionVisible = true;
}
#if lnx_csm
var isCsmLight = light.data.raw.type == "sun" &&
iron.object.LightObject.cascadeCount > 1;
if (isCsmLight && callIdx == 0) {
var vp0 = shadowVps.get(light);
if (vp0 == null) { vp0 = Mat4.identity(); shadowVps.set(light, vp0); }
vp0.setFrom(vp);
}
#end
if (shadowCullInterval > 1) {
if ((frame + slotIdx) % shadowCullInterval != 0) {
var applied = false;
#if lnx_csm
if (isCsmLight && callIdx > 0) {
var head = slots[0];
var vp0 = shadowVps.get(light);
if (head.appliedRev == sceneRev && head.flagsLen > 0 &&
head.vp != null && vp0 != null && matEquals(vp0, head.vp)) {
var hn = head.flagsLen < meshes.length ? head.flagsLen : meshes.length;
for (i in 0...hn) meshes[i].shadowOcclusionVisible = head.flags[i];
}
applied = true;
}
#end
if (!applied && slot.appliedRev == sceneRev && slot.flagsLen > 0 &&
slot.vp != null && matEquals(vp, slot.vp)) {
var n = slot.flagsLen < meshes.length ? slot.flagsLen : meshes.length;
for (i in 0...n) meshes[i].shadowOcclusionVisible = slot.flags[i];
}
lastShadowSlot = slot;
lastShadowFrame = frame;
lastShadowCallIdx = callIdx;
return;
}
}
if (bvh == null) bvh = new ObjectBVH();
if (bvhSyncedFrame != frame) {
if (bvhDirty || bvh.root < 0) {
bvh.build(cd);
} else if (cd.anyMoved || cd.orderChanged) {
bvh.refit(cd);
}
bvhSyncedFrame = frame;
}
if (shadowFrustumPlanes == null) {
shadowFrustumPlanes = [];
for (i in 0...6) shadowFrustumPlanes.push(new FrustumPlane());
}
{
var p = shadowFrustumPlanes;
p[0].setComponents(vp._03 + vp._00, vp._13 + vp._10, vp._23 + vp._20, vp._33 + vp._30);
p[1].setComponents(vp._03 - vp._00, vp._13 - vp._10, vp._23 - vp._20, vp._33 - vp._30);
p[2].setComponents(vp._03 - vp._01, vp._13 - vp._11, vp._23 - vp._21, vp._33 - vp._31);
p[3].setComponents(vp._03 + vp._01, vp._13 + vp._11, vp._23 + vp._21, vp._33 + vp._31);
p[4].setComponents(vp._02, vp._12, vp._22, vp._32);
p[5].setComponents(vp._03 - vp._02, vp._13 - vp._12, vp._23 - vp._22, vp._33 - vp._32);
}
var lightPlanes = shadowFrustumPlanes;
var lx = light.transform.worldx();
var ly = light.transform.worldy();
var lz = light.transform.worldz();
var raster = slot.rasterizer;
var vpSame = slot.vp != null && matEquals(vp, slot.vp);
if (slot.sceneRev == sceneRev && vpSame && raster != null) {
// skip
} else {
if (raster == null) {
raster = new SoftwareOcclusionRasterizer(
shadowRasterizer.width, shadowRasterizer.height);
slot.rasterizer = raster;
}
if (slot.vp == null) slot.vp = Mat4.identity();
slot.vp.setFrom(vp);
var perspective = light.data.raw.type != "sun";
selCount = 0;
bvh.traverseFrontToBack(lx, ly, lz,
function(node: BVHNode): Bool {
if (!ObjectBVH.aabbInFrustum(node, lightPlanes)) return true;
return false;
},
function(i: Int, _: Int) {
if ((cd.getFlags(i) & 8) == 0) return;
if ((cd.getFlags(i) & 16) != 0) return;
if ((cd.getFlags(i) & 4) == 0) return;
if ((cd.getFlags(i) & 32) != 0) return;
if (i >= meshes.length) return;
var r = cd.getRadius(i);
var score = r;
if (perspective) {
var dx = cd.getCenterX(i) - lx;
var dy = cd.getCenterY(i) - ly;
var dz = cd.getCenterZ(i) - lz;
var dist2 = dx * dx + dy * dy + dz * dz;
if (dist2 <= r * r) return;
score = r / Math.sqrt(dist2);
}
insertTopK(i, score, maxShadowOccluders);
}
);
raster = slot.rasterizer;
raster.clear();
var triBudget = maxShadowOccluderTris;
for (s in 0...selCount) {
if (triBudget < 12) break;
var i = selIdx[s];
var proxy = uidToProxy.get(cd.getObjectID(i));
if (proxy != null && proxy.triangleCount > 0 &&
proxy.triangleCount <= maxTrisPerProxy && proxy.triangleCount <= triBudget) {
proxy.rasterize(raster, meshes[i].transform.world, vp,
raster.width, raster.height);
triBudget -= proxy.triangleCount;
} else if (shadowBoxOccluders) {
rasterizeBoxOccluder(raster, i, vp, shadowBoxShrink);
triBudget -= 12;
}
}
if (raster.trianglesRasterized > 0) raster.buildHiZ();
}
if (raster.trianglesRasterized > 0) {
var sw = raster.width;
var sh = raster.height;
bvh.traverseFrontToBack(lx, ly, lz,
function(node: BVHNode): Bool {
if (!ObjectBVH.aabbInFrustum(node, lightPlanes)) return true;
var anyVisible = false;
if (node.isLeaf) {
for (j in 0...node.objectCount) {
var idx = bvh.objectIndices[node.firstObject + j];
if ((cd.getFlags(idx) & 8) != 0 && idx < meshes.length) {
anyVisible = true; break;
}
}
if (!anyVisible) return true;
}
if (raster.isOccluded(
node.minX, node.minY, node.minZ,
node.maxX, node.maxY, node.maxZ,
vp, sw, sh
)) {
bvh.forEachLeafInSubtree(node, function(idx: Int) {
if (idx < meshes.length && (cd.getFlags(idx) & 8) != 0) {
meshes[idx].shadowOcclusionVisible = false;
}
});
return true;
}
return false;
},
function(objIdx: Int, _: Int) {
if ((cd.getFlags(objIdx) & 8) == 0) return;
if (objIdx >= meshes.length) return;
meshes[objIdx].shadowOcclusionVisible = true;
if (raster.isOccluded(
cd.getMinX(objIdx), cd.getMinY(objIdx), cd.getMinZ(objIdx),
cd.getMaxX(objIdx), cd.getMaxY(objIdx), cd.getMaxZ(objIdx),
vp, sw, sh
)) {
meshes[objIdx].shadowOcclusionVisible = false;
}
}
);
}
#if lnx_csm
if (isCsmLight) {
for (c in 1...iron.object.LightObject.cascadeCount) {
if (c >= slots.length) break;
var s = slots[c];
if (s == null || s == slot || s.vp == null || s.rasterizer == null) continue;
for (i in 0...meshes.length) {
if (meshes[i].shadowOcclusionVisible) continue;
if (!s.rasterizer.isOccluded(
cd.getMinX(i), cd.getMinY(i), cd.getMinZ(i),
cd.getMaxX(i), cd.getMaxY(i), cd.getMaxZ(i),
s.vp, s.rasterizer.width, s.rasterizer.height
)) {
meshes[i].shadowOcclusionVisible = true;
}
}
}
}
#end
if (slot.flags.length < meshes.length) slot.flags.resize(meshes.length);
slot.flagsLen = meshes.length;
for (i in 0...meshes.length) slot.flags[i] = meshes[i].shadowOcclusionVisible;
#if lnx_csm
if (isCsmLight && callIdx > 0) {
var head = slots[0];
var vp0 = shadowVps.get(light);
if (head.appliedRev == sceneRev && head.flagsLen > 0 &&
head.vp != null && vp0 != null && matEquals(vp0, head.vp)) {
var hn = head.flagsLen < meshes.length ? head.flagsLen : meshes.length;
for (i in 0...hn) meshes[i].shadowOcclusionVisible = head.flags[i];
} else {
for (i in 0...meshes.length) meshes[i].shadowOcclusionVisible = true;
}
}
#end
slot.sceneRev = sceneRev;
slot.appliedRev = sceneRev;
slot.lastUsed = frame;
lastShadowSlot = slot;
lastShadowFrame = frame;
lastShadowCallIdx = callIdx;
shadowFlagsValid = true;
}
function selectOccluders(
meshes: Array<iron.object.MeshObject>,
camX: Float, camY: Float, camZ: Float
) {
var cd = cullData;
selCount = 0;
var minPx = minOccluderPx;
var autoPx = autoOccluderMinScreenRatio * screenH;
for (k in 0...inFrustumList.length) {
var i = inFrustumList[k];
if (i >= meshes.length) continue;
if (!meshes[i].occlusionVisible) continue;
var flags = cd.getFlags(i);
if ((flags & 16) != 0) continue;
if ((flags & 32) != 0) continue;
var r = cd.getRadius(i);
var px: Float;
var camInside = false;
if (focalLengthPx > 0) {
var dx = cd.getCenterX(i) - camX;
var dy = cd.getCenterY(i) - camY;
var dz = cd.getCenterZ(i) - camZ;
var dist2 = dx * dx + dy * dy + dz * dz;
if (dist2 <= r * r) camInside = true;
px = camInside ? 1e6 : focalLengthPx * r / Math.sqrt(dist2);
} else {
px = r * orthoPxPerUnit;
}
if (px < minPx) continue;
if ((flags & 1) == 0 && px < autoPx) continue;
if (camInside || !boxOccluders) {
var p = uidToProxy.get(cd.getObjectID(i));
if (p == null || p.triangleCount <= 0 || p.triangleCount > maxTrisPerProxy) continue;
}
insertTopK(i, px, maxOccluders);
}
}
function insertTopK(idx: Int, score: Float, k: Int) {
if (selIdx.length < k + 1) {
selIdx.resize(k + 1);
selScore.resize(k + 1);
}
var pos = selCount;
while (pos > 0 && selScore[pos - 1] < score) pos--;
if (pos >= k) return;
var last = selCount < k ? selCount : k - 1;
var j = last;
while (j > pos) {
selIdx[j] = selIdx[j - 1];
selScore[j] = selScore[j - 1];
j--;
}
selIdx[pos] = idx;
selScore[pos] = score;
if (selCount < k) selCount++;
}
function rasterizeSelected(meshes: Array<iron.object.MeshObject>, vp: Mat4) {
var cd = cullData;
var triBudget = maxOccluderTris;
#if (target.threaded)
rasterizer.beginCollect();
#end
for (s in 0...selCount) {
if (triBudget < 12) break;
var i = selIdx[s];
var proxy = uidToProxy.get(cd.getObjectID(i));
if (proxy != null && proxy.triangleCount > 0 &&
proxy.triangleCount <= maxTrisPerProxy && proxy.triangleCount <= triBudget) {
var mesh = i < meshes.length ? meshes[i] : null;
if (mesh == null) continue;
proxy.rasterize(rasterizer, mesh.transform.world, vp, screenW, screenH);
triBudget -= proxy.triangleCount;
} else {
if (!boxOccluders) continue;
rasterizeBoxOccluder(rasterizer, i, vp);
triBudget -= 12;
}
}
#if (target.threaded)
rasterizer.flushCollected();
#end
}
function rasterizeBoxOccluder(raster: SoftwareOcclusionRasterizer, objectIndex: Int, vp: Mat4, shrinkOverride = -1.0) {
var cd = cullData;
var bx0 = cd.getMinX(objectIndex), by0 = cd.getMinY(objectIndex), bz0 = cd.getMinZ(objectIndex);
var bx1 = cd.getMaxX(objectIndex), by1 = cd.getMaxY(objectIndex), bz1 = cd.getMaxZ(objectIndex);
var cx = (bx0 + bx1) * 0.5;
var cy = (by0 + by1) * 0.5;
var cz = (bz0 + bz1) * 0.5;
var boxShrink = shrinkOverride >= 0.0 ? shrinkOverride : CullSystem.boxShrink;
var hx = (bx1 - bx0) * 0.5 * boxShrink;
var hy = (by1 - by0) * 0.5 * boxShrink;
var hz = (bz1 - bz0) * 0.5 * boxShrink;
raster.rasterizeBox(
cx - hx, cy - hy, cz - hz,
cx + hx, cy + hy, cz + hz,
vp, raster.width, raster.height
);
}
function testVisibilityBVH(
vp: Mat4, camX: Float, camY: Float, camZ: Float,
planes: Array<FrustumPlane>
) {
var cd = cullData;
var raster = rasterizer;
var sw = screenW;
var sh = screenH;
var meshes = cd.meshes;
var bvh = this.bvh;
var temp = temporal;
var retestInterval = 4;
bvh.traverseFrontToBack(camX, camY, camZ,
function(node: BVHNode): Bool {
if (!ObjectBVH.aabbInFrustum(node, planes)) return true;
var anyVisible = false;
if (node.isLeaf) {
for (j in 0...node.objectCount) {
var idx = bvh.objectIndices[node.firstObject + j];
if ((cd.getFlags(idx) & 8) != 0) { anyVisible = true; break; }
}
if (!anyVisible) return true;
}
if (raster.isOccluded(
node.minX, node.minY, node.minZ,
node.maxX, node.maxY, node.maxZ,
vp, sw, sh
)) {
bvh.forEachLeafInSubtree(node, function(idx: Int) {
if (idx >= meshes.length || (cd.getFlags(idx) & 8) == 0) return;
meshes[idx].occlusionVisible = false;
var uid = cd.getObjectID(idx);
temp.setVisible(uid, false);
temp.markTested(uid);
});
return true;
}
return false;
},
function(objIdx: Int, _: Int) {
if ((cd.getFlags(objIdx) & 8) == 0) return;
if (objIdx >= meshes.length) return;
var uid = cd.getObjectID(objIdx);
if (temp.wasVisibleLastFrame(uid) && !temp.needsVisibleReTest(uid, retestInterval)) {
meshes[objIdx].occlusionVisible = true;
cd.addVisible(objIdx);
return;
}
if (raster.isOccluded(
cd.getMinX(objIdx), cd.getMinY(objIdx), cd.getMinZ(objIdx),
cd.getMaxX(objIdx), cd.getMaxY(objIdx), cd.getMaxZ(objIdx),
vp, sw, sh
)) {
meshes[objIdx].occlusionVisible = false;
temp.setVisible(uid, false);
temp.markTested(uid);
} else {
meshes[objIdx].occlusionVisible = true;
cd.addVisible(objIdx);
temp.setVisible(uid, true);
temp.markTested(uid);
}
}
);
}
public static var autoOccluderMinScreenRatio: Float = 0.10;
public inline function isVisible(objectIndex: Int): Bool {
var ms = cullData.meshes;
return objectIndex >= 0 && objectIndex < ms.length && ms[objectIndex].occlusionVisible;
}
public inline function markDirty() {
bvhDirty = true;
proxiesDirty = true;
cullData.slotsDirty = true;
shadowFlagsValid = false;
}
public function removeLight(light: iron.object.LightObject) {
shadowPasses.remove(light);
shadowCallIdx.remove(light);
shadowCallFrame.remove(light);
#if lnx_csm
shadowVps.remove(light);
#end
}
public static function init() {
if (active == null) {
active = new CullSystem();
}
}
public function loadOccluderProxies() {
occluderProxies.resize(0);
if (Scene.active == null) {
uidToProxy.clear();
return;
}
var meshes = Scene.active.meshes;
uidScan.clear();
for (i in 0...meshes.length) {
var mesh = meshes[i];
uidScan.set(mesh.uid, true);
if (uidToProxy.exists(mesh.uid)) continue;
if (mesh.data == null || mesh.data.raw == null) continue;
var raw = mesh.data.raw;
if (raw.occluder_proxy == null) continue;
var proxyRaw = raw.occluder_proxy;
if (proxyRaw.positions == null || proxyRaw.indices == null) continue;
var proxy = new OccluderProxy();
proxy.setData(proxyRaw.positions, proxyRaw.indices, mesh.uid);
uidToProxy.set(mesh.uid, proxy);
}
var dead: Array<Int> = null;
for (uid in uidToProxy.keys()) {
if (!uidScan.exists(uid)) {
if (dead == null) dead = [];
dead.push(uid);
}
}
if (dead != null) for (uid in dead) uidToProxy.remove(uid);
for (proxy in uidToProxy.iterator()) occluderProxies.push(proxy);
}
public static function shutdown() {
active = null;
}
}
class ShadowPassSlot {
public var vp: Mat4 = null;
public var rasterizer: SoftwareOcclusionRasterizer = null;
public var sceneRev: Int = -1;
public var appliedRev: Int = -1;
public var lastUsed: Int = -1;
public var flags: Array<Bool> = [];
public var flagsLen: Int = 0;
public function new() {}
}

View File

@ -0,0 +1,9 @@
package iron.culling;
enum CullTier {
CPUSingleThread;
CPUMultiThread;
GPUCompute;
GPUDriven;
MultiGPU;
}

View File

@ -0,0 +1,171 @@
package iron.culling;
class HiZPyramid {
public var width: Int;
public var height: Int;
public var mipLevels: Int;
public var mipBuffers: Array<kha.arrays.Float32Array>;
public var mipWidths: Array<Int>;
public var mipHeights: Array<Int>;
public var depthEps: Float;
var mipShifts: Array<Int>;
public function new(width: Int, height: Int) {
this.width = width;
this.height = height;
mipBuffers = [];
mipWidths = [];
mipHeights = [];
depthEps = CullSystem.depthEps;
var w = width;
var h = height;
mipLevels = 0;
while (w > 1 || h > 1) {
mipWidths.push(w);
mipHeights.push(h);
mipBuffers.push(new kha.arrays.Float32Array(w * h));
mipLevels++;
w = (w + 1) >> 1;
h = (h + 1) >> 1;
}
mipWidths.push(1);
mipHeights.push(1);
mipBuffers.push(new kha.arrays.Float32Array(1));
mipLevels++;
mipShifts = [0, 1, 2, 3, 4, 5, 6];
while (mipShifts.length < mipLevels) mipShifts.push(mipShifts.length);
}
public function clear(farDepth: Float = 1e20) {
var buf = mipBuffers[0];
for (i in 0...buf.length) buf[i] = farDepth;
}
public inline function writeDepth(x: Int, y: Int, depth: Float) {
var idx = y * width + x;
if (x >= 0 && x < width && y >= 0 && y < height) {
if (depth < mipBuffers[0][idx]) {
mipBuffers[0][idx] = depth;
}
}
}
public function buildPyramid() {
for (mip in 1...mipLevels) {
var prev = mipBuffers[mip - 1];
var cur = mipBuffers[mip];
var pw = mipWidths[mip - 1];
var ph = mipHeights[mip - 1];
var cw = mipWidths[mip];
var ch = mipHeights[mip];
for (y in 0...ch) {
for (x in 0...cw) {
var sx = x * 2;
var sy = y * 2;
var d00 = prev[sy * pw + sx];
var d10 = (sx + 1 < pw) ? prev[sy * pw + sx + 1] : d00;
var d01 = (sy + 1 < ph) ? prev[(sy + 1) * pw + sx] : d00;
var d11 = (sx + 1 < pw && sy + 1 < ph) ? prev[(sy + 1) * pw + sx + 1] : d00;
var maxD = d00;
if (d10 > maxD) maxD = d10;
if (d01 > maxD) maxD = d01;
if (d11 > maxD) maxD = d11;
cur[y * cw + x] = maxD;
}
}
}
}
public inline function sample(x: Int, y: Int, mip: Int): Float {
var mw = mipWidths[mip];
var mh = mipHeights[mip];
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x >= mw) x = mw - 1;
if (y >= mh) y = mh - 1;
return mipBuffers[mip][y * mw + x];
}
public static function mipForSize(rectW: Float, rectH: Float, maxMip: Int): Int {
var maxDim = Math.max(rectW, rectH);
if (maxDim <= 2.0) return 0;
var mip = Std.int(Math.floor(Math.log(maxDim) / Math.log(2))) - 1;
if (mip < 0) mip = 0;
if (mip > maxMip) mip = maxMip;
return mip;
}
public function isOccluded(
minX: Float, minY: Float, minZ: Float,
maxX: Float, maxY: Float, maxZ: Float,
vp: iron.math.Mat4,
_screenW: Int, _screenH: Int
): Bool {
var minSx = 1e20, minSy = 1e20;
var maxSx = -1e20, maxSy = -1e20;
var minDepth = 1e20;
var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30;
var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31;
var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32;
var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33;
for (c in 0...8) {
var x = (c & 1) != 0 ? maxX : minX;
var y = (c & 2) != 0 ? maxY : minY;
var z = (c & 4) != 0 ? maxZ : minZ;
var w = m03 * x + m13 * y + m23 * z + m33;
if (w <= 0) return false;
var invW = SimdOps.reciprocal(w);
var ndcZ = (m02 * x + m12 * y + m22 * z + m32) * invW;
if (ndcZ < -1.0) ndcZ = -1.0; else if (ndcZ > 1.0) ndcZ = 1.0;
var sx = ((m00 * x + m10 * y + m20 * z + m30) * invW * 0.5 + 0.5) * width;
var sy = ((m01 * x + m11 * y + m21 * z + m31) * invW * 0.5 + 0.5) * height;
if (sx < minSx) minSx = sx;
if (sy < minSy) minSy = sy;
if (sx > maxSx) maxSx = sx;
if (sy > maxSy) maxSy = sy;
if (ndcZ < minDepth) minDepth = ndcZ;
}
var ix0 = Std.int(Math.max(0, minSx));
var iy0 = Std.int(Math.max(0, minSy));
var ix1 = Std.int(Math.min(width - 1, maxSx));
var iy1 = Std.int(Math.min(height - 1, maxSy));
if (ix0 > ix1 || iy0 > iy1) return false;
if (ix0 > 0) ix0--;
if (iy0 > 0) iy0--;
if (ix1 < width - 1) ix1++;
if (iy1 < height - 1) iy1++;
var rectW = ix1 - ix0 + 1;
var rectH = iy1 - iy0 + 1;
var mip = mipForSize(rectW, rectH, mipLevels - 1);
var mw = mipWidths[mip];
var mh = mipHeights[mip];
var shift = mipShifts[mip];
var tx0 = Std.int(Math.min(mw - 1, ix0 >> shift));
var ty0 = Std.int(Math.min(mh - 1, iy0 >> shift));
var tx1 = Std.int(Math.min(mw - 1, ix1 >> shift));
var ty1 = Std.int(Math.min(mh - 1, iy1 >> shift));
var buf = mipBuffers[mip];
var limit = minDepth - depthEps;
for (ty in ty0...ty1 + 1) {
var rowOff = ty * mw;
for (tx in tx0...tx1 + 1) {
if (buf[rowOff + tx] >= limit) return false;
}
}
return true;
}
}

View File

@ -0,0 +1,60 @@
package iron.culling;
#if (target.threaded)
class JobSystem {
public static var numWorkers(default, null): Int = 0;
static var jobQueue: sys.thread.Deque<Void->Void> = null;
static var doneLock: sys.thread.Lock = null;
static var jobError: Dynamic = null;
public static function init(workers: Int = 3) {
if (jobQueue != null) return;
numWorkers = workers < 1 ? 1 : workers;
jobQueue = new sys.thread.Deque();
doneLock = new sys.thread.Lock();
for (_ in 0...numWorkers) {
sys.thread.Thread.create(workerLoop);
}
}
static function workerLoop() {
while (true) {
var job = jobQueue.pop(true);
if (job == null) return;
try {
job();
} catch (e: Dynamic) {
jobError = e;
}
doneLock.release();
}
}
public static function parallelFor(count: Int, fn: Int -> Int -> Void) {
init();
var chunks = numWorkers + 1;
var chunkSize = Std.int((count + chunks - 1) / chunks);
if (chunkSize < 1) chunkSize = 1;
var jobs = 0;
var start = chunkSize; // Chunk 0 runs on the calling thread
while (start < count) {
var s = start;
var e = start + chunkSize > count ? count : start + chunkSize;
jobQueue.add(function() fn(s, e));
jobs++;
start = e;
}
fn(0, chunkSize > count ? count : chunkSize);
for (_ in 0...jobs) doneLock.wait();
if (jobError != null) {
var e = jobError;
jobError = null;
throw e;
}
}
}
#end

View File

@ -0,0 +1,376 @@
package iron.culling;
class ObjectBVH {
static inline var LEAF_THRESHOLD = 4;
static inline var NUM_BINS = 12;
static inline var TRAVERSAL_COST = 1.0;
static inline var INTERSECT_COST = 1.0;
public var nodes: Array<BVHNode>;
public var objectIndices: Array<Int>;
public var root: Int = -1;
public var objectCount: Int = 0;
var stack: Array<Int> = [];
static var binCounts: Array<Int> = [for (i in 0...NUM_BINS) 0];
static var binMinX: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMinY: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMinZ: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxX: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxY: Array<Float> = [for (i in 0...NUM_BINS) 0];
static var binMaxZ: Array<Float> = [for (i in 0...NUM_BINS) 0];
public function new() {
nodes = [];
objectIndices = [];
}
public function build(cullData: CullData) {
objectCount = cullData.count;
nodes = [];
objectIndices = [];
if (objectCount == 0) {
root = -1;
return;
}
var indices = [for (i in 0...objectCount) i];
root = buildRecursive(indices, cullData);
}
static inline function surfaceArea(minX: Float, minY: Float, minZ: Float,
maxX: Float, maxY: Float, maxZ: Float): Float {
var wx = maxX - minX;
var wy = maxY - minY;
var wz = maxZ - minZ;
return 2.0 * (wx * wy + wx * wz + wy * wz);
}
function buildRecursive(indices: Array<Int>, cullData: CullData): Int {
var nodeIdx = nodes.length;
var node = new BVHNode();
nodes.push(node);
var minX = 1e20, minY = 1e20, minZ = 1e20;
var maxX = -1e20, maxY = -1e20, maxZ = -1e20;
var centMinX = 1e20, centMinY = 1e20, centMinZ = 1e20;
var centMaxX = -1e20, centMaxY = -1e20, centMaxZ = -1e20;
var cx = 0.0, cy = 0.0, cz = 0.0;
for (idx in indices) {
var x = cullData.getCenterX(idx);
var y = cullData.getCenterY(idx);
var z = cullData.getCenterZ(idx);
if (cullData.getMinX(idx) < minX) minX = cullData.getMinX(idx);
if (cullData.getMinY(idx) < minY) minY = cullData.getMinY(idx);
if (cullData.getMinZ(idx) < minZ) minZ = cullData.getMinZ(idx);
if (cullData.getMaxX(idx) > maxX) maxX = cullData.getMaxX(idx);
if (cullData.getMaxY(idx) > maxY) maxY = cullData.getMaxY(idx);
if (cullData.getMaxZ(idx) > maxZ) maxZ = cullData.getMaxZ(idx);
if (x < centMinX) centMinX = x;
if (y < centMinY) centMinY = y;
if (z < centMinZ) centMinZ = z;
if (x > centMaxX) centMaxX = x;
if (y > centMaxY) centMaxY = y;
if (z > centMaxZ) centMaxZ = z;
cx += x;
cy += y;
cz += z;
}
var n = indices.length;
cx /= n;
cy /= n;
cz /= n;
node.minX = minX; node.minY = minY; node.minZ = minZ;
node.maxX = maxX; node.maxY = maxY; node.maxZ = maxZ;
node.centerX = cx;
node.centerY = cy;
node.centerZ = cz;
if (n <= LEAF_THRESHOLD) {
node.isLeaf = true;
node.firstObject = objectIndices.length;
for (idx in indices) objectIndices.push(idx);
node.objectCount = n;
return nodeIdx;
}
var parentSA = surfaceArea(minX, minY, minZ, maxX, maxY, maxZ);
var leafCost = INTERSECT_COST * n;
var bestCost = 1e20;
var bestAxis = -1;
var bestSplitBin = -1;
var centBounds = [
[centMinX, centMaxX],
[centMinY, centMaxY],
[centMinZ, centMaxZ]
];
for (axis in 0...3) {
var lo = centBounds[axis][0];
var hi = centBounds[axis][1];
var range = hi - lo;
if (range < 1e-8) continue;
var invRange = NUM_BINS / range;
for (b in 0...NUM_BINS) {
binCounts[b] = 0;
binMinX[b] = 1e20; binMinY[b] = 1e20; binMinZ[b] = 1e20;
binMaxX[b] = -1e20; binMaxY[b] = -1e20; binMaxZ[b] = -1e20;
}
for (idx in indices) {
var cv = axis == 0 ? cullData.getCenterX(idx) :
axis == 1 ? cullData.getCenterY(idx) :
cullData.getCenterZ(idx);
var b = Std.int((cv - lo) * invRange);
if (b < 0) b = 0;
if (b >= NUM_BINS) b = NUM_BINS - 1;
binCounts[b]++;
if (cullData.getMinX(idx) < binMinX[b]) binMinX[b] = cullData.getMinX(idx);
if (cullData.getMinY(idx) < binMinY[b]) binMinY[b] = cullData.getMinY(idx);
if (cullData.getMinZ(idx) < binMinZ[b]) binMinZ[b] = cullData.getMinZ(idx);
if (cullData.getMaxX(idx) > binMaxX[b]) binMaxX[b] = cullData.getMaxX(idx);
if (cullData.getMaxY(idx) > binMaxY[b]) binMaxY[b] = cullData.getMaxY(idx);
if (cullData.getMaxZ(idx) > binMaxZ[b]) binMaxZ[b] = cullData.getMaxZ(idx);
}
var rightCount = [for (b in 0...NUM_BINS) 0];
var rightMinX = [for (b in 0...NUM_BINS) 0.0];
var rightMinY = [for (b in 0...NUM_BINS) 0.0];
var rightMinZ = [for (b in 0...NUM_BINS) 0.0];
var rightMaxX = [for (b in 0...NUM_BINS) 0.0];
var rightMaxY = [for (b in 0...NUM_BINS) 0.0];
var rightMaxZ = [for (b in 0...NUM_BINS) 0.0];
var cumCount = 0;
var cumMinX = 1e20, cumMinY = 1e20, cumMinZ = 1e20;
var cumMaxX = -1e20, cumMaxY = -1e20, cumMaxZ = -1e20;
for (b in 0...NUM_BINS) {
var bi = NUM_BINS - 1 - b;
cumCount += binCounts[bi];
if (binMinX[bi] < cumMinX) cumMinX = binMinX[bi];
if (binMinY[bi] < cumMinY) cumMinY = binMinY[bi];
if (binMinZ[bi] < cumMinZ) cumMinZ = binMinZ[bi];
if (binMaxX[bi] > cumMaxX) cumMaxX = binMaxX[bi];
if (binMaxY[bi] > cumMaxY) cumMaxY = binMaxY[bi];
if (binMaxZ[bi] > cumMaxZ) cumMaxZ = binMaxZ[bi];
rightCount[bi] = cumCount;
rightMinX[bi] = cumMinX; rightMinY[bi] = cumMinY; rightMinZ[bi] = cumMinZ;
rightMaxX[bi] = cumMaxX; rightMaxY[bi] = cumMaxY; rightMaxZ[bi] = cumMaxZ;
}
var leftCount = 0;
var leftMinX = 1e20, leftMinY = 1e20, leftMinZ = 1e20;
var leftMaxX = -1e20, leftMaxY = -1e20, leftMaxZ = -1e20;
for (b in 0...NUM_BINS - 1) {
if (binCounts[b] == 0) continue;
leftCount += binCounts[b];
if (binMinX[b] < leftMinX) leftMinX = binMinX[b];
if (binMinY[b] < leftMinY) leftMinY = binMinY[b];
if (binMinZ[b] < leftMinZ) leftMinZ = binMinZ[b];
if (binMaxX[b] > leftMaxX) leftMaxX = binMaxX[b];
if (binMaxY[b] > leftMaxY) leftMaxY = binMaxY[b];
if (binMaxZ[b] > leftMaxZ) leftMaxZ = binMaxZ[b];
var rc = rightCount[b + 1];
if (rc == 0) continue;
var leftSA = surfaceArea(leftMinX, leftMinY, leftMinZ, leftMaxX, leftMaxY, leftMaxZ);
var rightSA = surfaceArea(rightMinX[b + 1], rightMinY[b + 1], rightMinZ[b + 1],
rightMaxX[b + 1], rightMaxY[b + 1], rightMaxZ[b + 1]);
var cost = TRAVERSAL_COST + (leftSA * leftCount + rightSA * rc) / parentSA;
if (cost < bestCost) {
bestCost = cost;
bestAxis = axis;
bestSplitBin = b;
}
}
}
if (bestAxis < 0 || bestCost >= leafCost) {
node.isLeaf = true;
node.firstObject = objectIndices.length;
for (idx in indices) objectIndices.push(idx);
node.objectCount = n;
return nodeIdx;
}
var lo = centBounds[bestAxis][0];
var hi = centBounds[bestAxis][1];
var invRange = NUM_BINS / (hi - lo);
var splitBin = bestSplitBin;
var left = [];
var right = [];
for (idx in indices) {
var cv = bestAxis == 0 ? cullData.getCenterX(idx) :
bestAxis == 1 ? cullData.getCenterY(idx) :
cullData.getCenterZ(idx);
var b = Std.int((cv - lo) * invRange);
if (b < 0) b = 0;
if (b >= NUM_BINS) b = NUM_BINS - 1;
if (b <= splitBin) left.push(idx);
else right.push(idx);
}
if (left.length == 0 || right.length == 0) {
indices.sort(function(a, b): Int {
var av = bestAxis == 0 ? cullData.getCenterX(a) :
bestAxis == 1 ? cullData.getCenterY(a) :
cullData.getCenterZ(a);
var bv = bestAxis == 0 ? cullData.getCenterX(b) :
bestAxis == 1 ? cullData.getCenterY(b) :
cullData.getCenterZ(b);
if (av < bv) return -1;
if (av > bv) return 1;
return 0;
});
var mid = n >> 1;
left = indices.slice(0, mid);
right = indices.slice(mid);
}
node.isLeaf = false;
node.leftChild = buildRecursive(left, cullData);
node.rightChild = buildRecursive(right, cullData);
return nodeIdx;
}
public function refit(cullData: CullData) {
var i = nodes.length - 1;
while (i >= 0) {
var node = nodes[i];
if (node.isLeaf) {
var minX = 1e20, minY = 1e20, minZ = 1e20;
var maxX = -1e20, maxY = -1e20, maxZ = -1e20;
for (j in 0...node.objectCount) {
var idx = objectIndices[node.firstObject + j];
if (cullData.getMinX(idx) < minX) minX = cullData.getMinX(idx);
if (cullData.getMinY(idx) < minY) minY = cullData.getMinY(idx);
if (cullData.getMinZ(idx) < minZ) minZ = cullData.getMinZ(idx);
if (cullData.getMaxX(idx) > maxX) maxX = cullData.getMaxX(idx);
if (cullData.getMaxY(idx) > maxY) maxY = cullData.getMaxY(idx);
if (cullData.getMaxZ(idx) > maxZ) maxZ = cullData.getMaxZ(idx);
}
node.minX = minX; node.minY = minY; node.minZ = minZ;
node.maxX = maxX; node.maxY = maxY; node.maxZ = maxZ;
} else {
var l = nodes[node.leftChild];
var r = nodes[node.rightChild];
node.minX = l.minX < r.minX ? l.minX : r.minX;
node.minY = l.minY < r.minY ? l.minY : r.minY;
node.minZ = l.minZ < r.minZ ? l.minZ : r.minZ;
node.maxX = l.maxX > r.maxX ? l.maxX : r.maxX;
node.maxY = l.maxY > r.maxY ? l.maxY : r.maxY;
node.maxZ = l.maxZ > r.maxZ ? l.maxZ : r.maxZ;
}
node.centerX = (node.minX + node.maxX) * 0.5;
node.centerY = (node.minY + node.maxY) * 0.5;
node.centerZ = (node.minZ + node.maxZ) * 0.5;
i--;
}
}
public function forEachLeafInSubtree(node: BVHNode, cb: (Int) -> Void) {
if (node.isLeaf) {
for (i in 0...node.objectCount) {
cb(objectIndices[node.firstObject + i]);
}
} else {
forEachLeafInSubtree(nodes[node.leftChild], cb);
forEachLeafInSubtree(nodes[node.rightChild], cb);
}
}
public function traverseFrontToBack(
camX: Float, camY: Float, camZ: Float,
shouldSkip: (BVHNode) -> Bool,
onLeaf: (Int, Int) -> Void
) {
if (root < 0) return;
stack.resize(0);
stack.push(root);
while (stack.length > 0) {
var nodeIdx = stack.pop();
var node = nodes[nodeIdx];
if (shouldSkip(node)) continue;
if (node.isLeaf) {
var start = node.firstObject;
for (i in 0...node.objectCount) {
onLeaf(objectIndices[start + i], i);
}
} else {
var leftNode = nodes[node.leftChild];
var rightNode = nodes[node.rightChild];
var leftDist = distSq(leftNode.centerX, leftNode.centerY, leftNode.centerZ, camX, camY, camZ);
var rightDist = distSq(rightNode.centerX, rightNode.centerY, rightNode.centerZ, camX, camY, camZ);
if (leftDist > rightDist) {
stack.push(node.leftChild);
stack.push(node.rightChild);
} else {
stack.push(node.rightChild);
stack.push(node.leftChild);
}
}
}
}
inline function distSq(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float): Float {
var dx = ax - bx;
var dy = ay - by;
var dz = az - bz;
return dx * dx + dy * dy + dz * dz;
}
public static function aabbInFrustumRaw(
minX: Float, minY: Float, minZ: Float,
maxX: Float, maxY: Float, maxZ: Float,
planes: Array<iron.object.FrustumPlane>
): Bool {
for (i in 0...planes.length) {
var plane = planes[i];
var px = plane.normal.x >= 0 ? maxX : minX;
var py = plane.normal.y >= 0 ? maxY : minY;
var pz = plane.normal.z >= 0 ? maxZ : minZ;
if (SimdOps.dot3(px, py, pz, plane.normal.x, plane.normal.y, plane.normal.z) + plane.constant < 0) {
return false;
}
}
return true;
}
public static function aabbInFrustum(
node: BVHNode,
planes: Array<iron.object.FrustumPlane>
): Bool {
for (i in 0...planes.length) {
var plane = planes[i];
var px = plane.normal.x >= 0 ? node.maxX : node.minX;
var py = plane.normal.y >= 0 ? node.maxY : node.minY;
var pz = plane.normal.z >= 0 ? node.maxZ : node.minZ;
if (SimdOps.dot3(px, py, pz, plane.normal.x, plane.normal.y, plane.normal.z) + plane.constant < 0) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,71 @@
package iron.culling;
class OccluderProxy {
public var positions: kha.arrays.Float32Array;
public var indices: kha.arrays.Uint32Array;
public var triangleCount: Int;
public var meshUid: Int;
var worldPositions: kha.arrays.Float32Array;
var mvp: iron.math.Mat4;
public function new() {
indices = new kha.arrays.Uint32Array(0);
triangleCount = 0;
}
public function setData(pos: kha.arrays.Float32Array, idx: kha.arrays.Uint32Array, uid: Int) {
#if js
if (!Std.isOfType(pos, js.lib.DataView)) {
var srcF: Array<Float> = cast pos;
var dstF = new kha.arrays.Float32Array(srcF.length);
for (i in 0...srcF.length) dstF[i] = srcF[i];
pos = dstF;
}
if (!Std.isOfType(idx, js.lib.DataView)) {
var srcI: Array<Int> = cast idx;
var dstI = new kha.arrays.Uint32Array(srcI.length);
for (i in 0...srcI.length) dstI[i] = srcI[i];
idx = dstI;
}
#end
positions = pos;
indices = idx;
triangleCount = Std.int(idx.length / 3);
meshUid = uid;
}
public function rasterize(
rasterizer: SoftwareOcclusionRasterizer,
world: iron.math.Mat4,
vp: iron.math.Mat4,
screenW: Int, screenH: Int
) {
if (positions == null || indices == null || triangleCount == 0) return;
var vertCount = Std.int(positions.length / 3);
var clipSize = vertCount * 4;
if (worldPositions == null || worldPositions.length < clipSize) {
worldPositions = new kha.arrays.Float32Array(clipSize);
}
if (mvp == null) mvp = iron.math.Mat4.identity();
mvp.multmats(vp, world);
for (i in 0...vertCount) {
var x = positions[i * 3];
var y = positions[i * 3 + 1];
var z = positions[i * 3 + 2];
var off = i * 4;
worldPositions[off] = mvp._00 * x + mvp._10 * y + mvp._20 * z + mvp._30;
worldPositions[off + 1] = mvp._01 * x + mvp._11 * y + mvp._21 * z + mvp._31;
worldPositions[off + 2] = mvp._02 * x + mvp._12 * y + mvp._22 * z + mvp._32;
worldPositions[off + 3] = mvp._03 * x + mvp._13 * y + mvp._23 * z + mvp._33;
}
rasterizer.rasterizeProxyTransformed(worldPositions, indices, screenW, screenH);
}
}

View File

@ -0,0 +1,83 @@
package iron.culling;
#if cpp
@:include("smmintrin.h")
@:include("emmintrin.h")
@:include("xmmintrin.h")
#end
class SimdOps {
public static inline function dot4(
a0: Float, a1: Float, a2: Float, a3: Float,
b0: Float, b1: Float, b2: Float, b3: Float
): Float {
#if cpp
return untyped __cpp__("_mm_cvtss_f32(_mm_dp_ps(_mm_setr_ps({0}, {1}, {2}, {3}), _mm_setr_ps({4}, {5}, {6}, {7}), 0xF1))",
a0, a1, a2, a3, b0, b1, b2, b3);
#else
return a0 * b0 + a1 * b1 + a2 * b2 + a3 * b3;
#end
}
public static inline function dot3(
a0: Float, a1: Float, a2: Float,
b0: Float, b1: Float, b2: Float
): Float {
#if cpp
return untyped __cpp__("_mm_cvtss_f32(_mm_dp_ps(_mm_setr_ps({0}, {1}, {2}, 0.0f), _mm_setr_ps({3}, {4}, {5}, 0.0f), 0x71))",
a0, a1, a2, b0, b1, b2);
#else
return a0 * b0 + a1 * b1 + a2 * b2;
#end
}
public static inline function reciprocal(x: Float): Float {
#if cpp
return untyped __cpp__("({ auto _r = _mm_rcp_ss(_mm_set_ss({0})); _mm_cvtss_f32(_mm_add_ss(_mm_mul_ss(_r, _mm_sub_ss(_mm_set_ss(2.0f), _mm_mul_ss(_r, _mm_set_ss({0})))), _r)); })", x);
#else
return 1.0 / x;
#end
}
public static inline function max4f(
a: Float, b: Float, c: Float, d: Float
): Float {
#if cpp
return untyped __cpp__("_mm_cvtss_f32(_mm_max_ss(_mm_max_ss(_mm_set_ss({0}), _mm_set_ss({1})), _mm_max_ss(_mm_set_ss({2}), _mm_set_ss({3}))))",
a, b, c, d);
#else
var m = a;
if (b > m) m = b;
if (c > m) m = c;
if (d > m) m = d;
return m;
#end
}
public static inline function min4f(
a: Float, b: Float, c: Float, d: Float
): Float {
#if cpp
return untyped __cpp__("_mm_cvtss_f32(_mm_min_ss(_mm_min_ss(_mm_set_ss({0}), _mm_set_ss({1})), _mm_min_ss(_mm_set_ss({2}), _mm_set_ss({3}))))",
a, b, c, d);
#else
var m = a;
if (b < m) m = b;
if (c < m) m = c;
if (d < m) m = d;
return m;
#end
}
public static inline function frustumSphereTest6(
planeData: kha.arrays.Float32Array,
cx: Float, cy: Float, cz: Float, radius: Float
): Bool {
for (i in 0...6) {
var base = i * 4;
var dist = dot3(cx, cy, cz, planeData[base], planeData[base + 1], planeData[base + 2]) + planeData[base + 3];
if (dist + radius < 0) return false;
}
return true;
}
}

View File

@ -0,0 +1,507 @@
package iron.culling;
class SoftwareOcclusionRasterizer {
public var hiz: HiZPyramid;
public var width: Int;
public var height: Int;
public var nearClip: Float = 0.0001;
static inline var TILE_SIZE = 32;
public var trianglesRasterized: Int = 0;
public var collectMode = false;
var screenTris: Array<Float> = [];
var screenTriCount = 0;
static final BOX_FACES = [
[0, 1, 3, 2], // -Z
[4, 5, 7, 6], // +Z
[0, 1, 5, 4], // -Y
[2, 3, 7, 6], // +Y
[0, 2, 6, 4], // -X
[1, 3, 7, 5], // +X
];
var inX: Array<Float> = [0,0,0];
var inY: Array<Float> = [0,0,0];
var inZ: Array<Float> = [0,0,0];
var inW: Array<Float> = [0,0,0];
var outX: Array<Float> = [0,0,0,0];
var outY: Array<Float> = [0,0,0,0];
var outZ: Array<Float> = [0,0,0,0];
var outW: Array<Float> = [0,0,0,0];
var projSx: Array<Float> = [0,0,0,0];
var projSy: Array<Float> = [0,0,0,0];
var projSz: Array<Float> = [0,0,0,0];
var boxClipX: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipY: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipZ: Array<Float> = [0,0,0,0,0,0,0,0];
var boxClipW: Array<Float> = [0,0,0,0,0,0,0,0];
public function new(width: Int = 256, height: Int = 128) {
this.width = width;
this.height = height;
hiz = new HiZPyramid(width, height);
}
public function clear() {
hiz.clear(1e20);
trianglesRasterized = 0;
}
public function rasterizeTriangle(
v0x: Float, v0y: Float, v0z: Float,
v1x: Float, v1y: Float, v1z: Float,
v2x: Float, v2y: Float, v2z: Float
) {
var e0a = v1y - v0y; var e0b = v0x - v1x;
var e1a = v2y - v1y; var e1b = v1x - v2x;
var e2a = v0y - v2y; var e2b = v2x - v0x;
var e0c = -e0a * v0x - e0b * v0y;
var e1c = -e1a * v1x - e1b * v1y;
var e2c = -e2a * v2x - e2b * v2y;
var area = e0a * v2x + e0b * v2y + e0c;
if (area < 0) {
area = -area;
e0a = -e0a; e0b = -e0b; e0c = -e0c;
e1a = -e1a; e1b = -e1b; e1c = -e1c;
e2a = -e2a; e2b = -e2b; e2c = -e2c;
}
if (area < 1e-8) return;
var minX = Std.int(Math.max(0, Math.floor(Math.min(v0x, Math.min(v1x, v2x)))));
var maxX = Std.int(Math.min(width - 1, Math.ceil(Math.max(v0x, Math.max(v1x, v2x)))));
var minY = Std.int(Math.max(0, Math.floor(Math.min(v0y, Math.min(v1y, v2y)))));
var maxY = Std.int(Math.min(height - 1, Math.ceil(Math.max(v0y, Math.max(v1y, v2y)))));
if (minX > maxX || minY > maxY) return;
var d = (v1x - v0x) * (v2y - v0y) - (v2x - v0x) * (v1y - v0y);
var invD = 1.0 / d;
var dzdx = ((v1z - v0z) * (v2y - v0y) - (v2z - v0z) * (v1y - v0y)) * invD;
var dzdy = ((v2z - v0z) * (v1x - v0x) - (v1z - v0z) * (v2x - v0x)) * invD;
var zPush = (Math.abs(dzdx) + Math.abs(dzdy)) * 0.5;
var zMaxTri = v0z;
if (v1z > zMaxTri) zMaxTri = v1z;
if (v2z > zMaxTri) zMaxTri = v2z;
var cx0 = minX + 0.5;
var cy0 = minY + 0.5;
var e0row = e0a * cx0 + e0b * cy0 + e0c;
var e1row = e1a * cx0 + e1b * cy0 + e1c;
var e2row = e2a * cx0 + e2b * cy0 + e2c;
var zrow = v0z + dzdx * (cx0 - v0x) + dzdy * (cy0 - v0y);
var buf = hiz.mipBuffers[0];
var w = width;
for (py in minY...maxY + 1) {
var e0 = e0row; var e1 = e1row; var e2 = e2row;
var z = zrow;
var idx = py * w + minX;
for (px in minX...maxX + 1) {
if (e0 >= 0 && e1 >= 0 && e2 >= 0) {
var zPix = z + zPush;
if (zPix > zMaxTri) zPix = zMaxTri;
if (zPix < 0.0) zPix = 0.0;
if (zPix > 1.0) zPix = 1.0;
if (zPix < buf[idx]) buf[idx] = zPix;
}
e0 += e0a; e1 += e1a; e2 += e2a;
z += dzdx;
idx++;
}
e0row += e0b; e1row += e1b; e2row += e2b;
zrow += dzdy;
}
}
function clipNearAndRasterize(
cx0: Float, cy0: Float, cz0: Float, cw0: Float,
cx1: Float, cy1: Float, cz1: Float, cw1: Float,
cx2: Float, cy2: Float, cz2: Float, cw2: Float
) {
inX[0] = cx0; inY[0] = cy0; inZ[0] = cz0; inW[0] = cw0;
inX[1] = cx1; inY[1] = cy1; inZ[1] = cz1; inW[1] = cw1;
inX[2] = cx2; inY[2] = cy2; inZ[2] = cz2; inW[2] = cw2;
var outCount = 0;
for (i in 0...3) {
var aIdx = (i + 2) % 3;
var bIdx = i;
var aInside = inW[aIdx] >= nearClip;
var bInside = inW[bIdx] >= nearClip;
if (aInside) {
if (bInside) {
outX[outCount] = inX[bIdx]; outY[outCount] = inY[bIdx];
outZ[outCount] = inZ[bIdx]; outW[outCount] = inW[bIdx];
outCount++;
} else {
var t = (inW[aIdx] - nearClip) / (inW[aIdx] - inW[bIdx]);
outX[outCount] = inX[aIdx] + t * (inX[bIdx] - inX[aIdx]);
outY[outCount] = inY[aIdx] + t * (inY[bIdx] - inY[aIdx]);
outZ[outCount] = inZ[aIdx] + t * (inZ[bIdx] - inZ[aIdx]);
outW[outCount] = inW[aIdx] + t * (inW[bIdx] - inW[aIdx]);
outCount++;
}
} else {
if (bInside) {
var t = (inW[aIdx] - nearClip) / (inW[aIdx] - inW[bIdx]);
outX[outCount] = inX[aIdx] + t * (inX[bIdx] - inX[aIdx]);
outY[outCount] = inY[aIdx] + t * (inY[bIdx] - inY[aIdx]);
outZ[outCount] = inZ[aIdx] + t * (inZ[bIdx] - inZ[aIdx]);
outW[outCount] = inW[aIdx] + t * (inW[bIdx] - inW[aIdx]);
outCount++;
outX[outCount] = inX[bIdx]; outY[outCount] = inY[bIdx];
outZ[outCount] = inZ[bIdx]; outW[outCount] = inW[bIdx];
outCount++;
}
}
}
if (outCount < 3) return;
for (i in 0...outCount) {
var invW = SimdOps.reciprocal(outW[i]);
projSx[i] = (outX[i] * invW * 0.5 + 0.5) * width;
projSy[i] = (outY[i] * invW * 0.5 + 0.5) * height;
projSz[i] = outZ[i] * invW;
}
var signedArea = (projSx[1] - projSx[0]) * (projSy[2] - projSy[0]) -
(projSy[1] - projSy[0]) * (projSx[2] - projSx[0]);
if (Math.abs(signedArea) < 1e-8) return;
trianglesRasterized++;
if (collectMode) {
screenTris[screenTriCount++] = projSx[0]; screenTris[screenTriCount++] = projSy[0]; screenTris[screenTriCount++] = projSz[0];
screenTris[screenTriCount++] = projSx[1]; screenTris[screenTriCount++] = projSy[1]; screenTris[screenTriCount++] = projSz[1];
screenTris[screenTriCount++] = projSx[2]; screenTris[screenTriCount++] = projSy[2]; screenTris[screenTriCount++] = projSz[2];
if (outCount == 4) {
screenTris[screenTriCount++] = projSx[0]; screenTris[screenTriCount++] = projSy[0]; screenTris[screenTriCount++] = projSz[0];
screenTris[screenTriCount++] = projSx[2]; screenTris[screenTriCount++] = projSy[2]; screenTris[screenTriCount++] = projSz[2];
screenTris[screenTriCount++] = projSx[3]; screenTris[screenTriCount++] = projSy[3]; screenTris[screenTriCount++] = projSz[3];
}
} else {
rasterizeTriangle(
projSx[0], projSy[0], projSz[0],
projSx[1], projSy[1], projSz[1],
projSx[2], projSy[2], projSz[2]
);
if (outCount == 4) {
rasterizeTriangle(
projSx[0], projSy[0], projSz[0],
projSx[2], projSy[2], projSz[2],
projSx[3], projSy[3], projSz[3]
);
}
}
}
public function rasterizeProxy(
positions: kha.arrays.Float32Array,
indices: kha.arrays.Uint32Array,
vp: iron.math.Mat4,
screenW: Int, screenH: Int
) {
var triCount = Std.int(indices.length / 3);
var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30;
var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31;
var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32;
var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33;
for (t in 0...triCount) {
var i0 = indices[t * 3] * 3;
var i1 = indices[t * 3 + 1] * 3;
var i2 = indices[t * 3 + 2] * 3;
var px0 = positions[i0], py0 = positions[i0 + 1], pz0 = positions[i0 + 2];
var px1 = positions[i1], py1 = positions[i1 + 1], pz1 = positions[i1 + 2];
var px2 = positions[i2], py2 = positions[i2 + 1], pz2 = positions[i2 + 2];
var cw0 = m03 * px0 + m13 * py0 + m23 * pz0 + m33;
var cw1 = m03 * px1 + m13 * py1 + m23 * pz1 + m33;
var cw2 = m03 * px2 + m13 * py2 + m23 * pz2 + m33;
if (cw0 < nearClip && cw1 < nearClip && cw2 < nearClip) continue;
var cx0 = m00 * px0 + m10 * py0 + m20 * pz0 + m30;
var cy0 = m01 * px0 + m11 * py0 + m21 * pz0 + m31;
var cz0 = m02 * px0 + m12 * py0 + m22 * pz0 + m32;
var cx1 = m00 * px1 + m10 * py1 + m20 * pz1 + m30;
var cy1 = m01 * px1 + m11 * py1 + m21 * pz1 + m31;
var cz1 = m02 * px1 + m12 * py1 + m22 * pz1 + m32;
var cx2 = m00 * px2 + m10 * py2 + m20 * pz2 + m30;
var cy2 = m01 * px2 + m11 * py2 + m21 * pz2 + m31;
var cz2 = m02 * px2 + m12 * py2 + m22 * pz2 + m32;
clipNearAndRasterize(
cx0, cy0, cz0, cw0,
cx1, cy1, cz1, cw1,
cx2, cy2, cz2, cw2
);
}
}
public function rasterizeProxyTransformed(
clipPositions: kha.arrays.Float32Array,
indices: kha.arrays.Uint32Array,
screenW: Int, screenH: Int
) {
var triCount = Std.int(indices.length / 3);
for (t in 0...triCount) {
var i0 = indices[t * 3] * 4;
var i1 = indices[t * 3 + 1] * 4;
var i2 = indices[t * 3 + 2] * 4;
var cx0 = clipPositions[i0], cy0 = clipPositions[i0 + 1], cz0 = clipPositions[i0 + 2], cw0 = clipPositions[i0 + 3];
var cx1 = clipPositions[i1], cy1 = clipPositions[i1 + 1], cz1 = clipPositions[i1 + 2], cw1 = clipPositions[i1 + 3];
var cx2 = clipPositions[i2], cy2 = clipPositions[i2 + 1], cz2 = clipPositions[i2 + 2], cw2 = clipPositions[i2 + 3];
if (cw0 < nearClip && cw1 < nearClip && cw2 < nearClip) continue;
clipNearAndRasterize(
cx0, cy0, cz0, cw0,
cx1, cy1, cz1, cw1,
cx2, cy2, cz2, cw2
);
}
}
public function rasterizeBox(
minX: Float, minY: Float, minZ: Float,
maxX: Float, maxY: Float, maxZ: Float,
vp: iron.math.Mat4,
screenW: Int, screenH: Int
) {
var m00 = vp._00, m10 = vp._10, m20 = vp._20, m30 = vp._30;
var m01 = vp._01, m11 = vp._11, m21 = vp._21, m31 = vp._31;
var m02 = vp._02, m12 = vp._12, m22 = vp._22, m32 = vp._32;
var m03 = vp._03, m13 = vp._13, m23 = vp._23, m33 = vp._33;
for (c in 0...8) {
var x = (c & 1) != 0 ? maxX : minX;
var y = (c & 2) != 0 ? maxY : minY;
var z = (c & 4) != 0 ? maxZ : minZ;
boxClipX[c] = m00 * x + m10 * y + m20 * z + m30;
boxClipY[c] = m01 * x + m11 * y + m21 * z + m31;
boxClipZ[c] = m02 * x + m12 * y + m22 * z + m32;
boxClipW[c] = m03 * x + m13 * y + m23 * z + m33;
}
for (face in BOX_FACES) {
clipNearAndRasterize(
boxClipX[face[0]], boxClipY[face[0]], boxClipZ[face[0]], boxClipW[face[0]],
boxClipX[face[1]], boxClipY[face[1]], boxClipZ[face[1]], boxClipW[face[1]],
boxClipX[face[2]], boxClipY[face[2]], boxClipZ[face[2]], boxClipW[face[2]]
);
clipNearAndRasterize(
boxClipX[face[0]], boxClipY[face[0]], boxClipZ[face[0]], boxClipW[face[0]],
boxClipX[face[2]], boxClipY[face[2]], boxClipZ[face[2]], boxClipW[face[2]],
boxClipX[face[3]], boxClipY[face[3]], boxClipZ[face[3]], boxClipW[face[3]]
);
}
}
public function buildHiZ() {
hiz.buildPyramid();
}
public function beginCollect() {
collectMode = true;
screenTriCount = 0;
}
var tileTriCounts: Array<Int> = [];
var tileTriOffsets: Array<Int> = [];
var tileTriLists: Array<Array<Int>> = [];
public function flushCollected() {
collectMode = false;
var nTris = Std.int(screenTriCount / 9);
if (nTris == 0) return;
var tilesX = Std.int(Math.ceil(width / TILE_SIZE));
var tilesY = Std.int(Math.ceil(height / TILE_SIZE));
var totalTiles = tilesX * tilesY;
if (tileTriCounts.length < totalTiles) {
tileTriCounts.resize(totalTiles);
tileTriOffsets.resize(totalTiles);
tileTriLists.resize(totalTiles);
}
for (t in 0...totalTiles) tileTriCounts[t] = 0;
for (t in 0...nTris) {
var off = t * 9;
var tx0 = Std.int(Math.max(0, Math.floor(Math.min(
Math.min(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE)));
var tx1 = Std.int(Math.min(tilesX - 1, Math.ceil(Math.max(
Math.max(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE)));
var ty0 = Std.int(Math.max(0, Math.floor(Math.min(
Math.min(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE)));
var ty1 = Std.int(Math.min(tilesY - 1, Math.ceil(Math.max(
Math.max(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE)));
for (ty in ty0...ty1 + 1) {
for (tx in tx0...tx1 + 1) {
tileTriCounts[ty * tilesX + tx]++;
}
}
}
for (t in 0...totalTiles) {
var l = tileTriLists[t];
if (l == null) { l = []; tileTriLists[t] = l; }
if (l.length < tileTriCounts[t]) l.resize(tileTriCounts[t]);
tileTriOffsets[t] = 0;
}
for (t in 0...nTris) {
var off = t * 9;
var tx0 = Std.int(Math.max(0, Math.floor(Math.min(
Math.min(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE)));
var tx1 = Std.int(Math.min(tilesX - 1, Math.ceil(Math.max(
Math.max(screenTris[off], screenTris[off + 3]), screenTris[off + 6]) / TILE_SIZE)));
var ty0 = Std.int(Math.max(0, Math.floor(Math.min(
Math.min(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE)));
var ty1 = Std.int(Math.min(tilesY - 1, Math.ceil(Math.max(
Math.max(screenTris[off + 1], screenTris[off + 4]), screenTris[off + 7]) / TILE_SIZE)));
for (ty in ty0...ty1 + 1) {
for (tx in tx0...tx1 + 1) {
var ti = ty * tilesX + tx;
tileTriLists[ti][tileTriOffsets[ti]++] = t;
}
}
}
var buf = hiz.mipBuffers[0];
var w = width;
var tris = screenTris;
#if (target.threaded)
JobSystem.parallelFor(totalTiles, function(start, end) {
for (ti in start...end) {
var tx = ti % tilesX;
var ty = Std.int(ti / tilesX);
var px0 = tx * TILE_SIZE;
var py0 = ty * TILE_SIZE;
var px1 = Std.int(Math.min(w - 1, px0 + TILE_SIZE - 1));
var py1 = Std.int(Math.min(height - 1, py0 + TILE_SIZE - 1));
var list = tileTriLists[ti];
var listLen = tileTriCounts[ti];
for (li in 0...listLen) {
var off = list[li] * 9;
rasterizeTriangleBounded(
tris[off], tris[off + 1], tris[off + 2],
tris[off + 3], tris[off + 4], tris[off + 5],
tris[off + 6], tris[off + 7], tris[off + 8],
px0, py0, px1, py1, buf, w
);
}
}
});
#else
for (ti in 0...totalTiles) {
var tx = ti % tilesX;
var ty = Std.int(ti / tilesX);
var px0 = tx * TILE_SIZE;
var py0 = ty * TILE_SIZE;
var px1 = Std.int(Math.min(w - 1, px0 + TILE_SIZE - 1));
var py1 = Std.int(Math.min(height - 1, py0 + TILE_SIZE - 1));
var list = tileTriLists[ti];
for (li in 0...list.length) {
var off = list[li] * 9;
rasterizeTriangleBounded(
tris[off], tris[off + 1], tris[off + 2],
tris[off + 3], tris[off + 4], tris[off + 5],
tris[off + 6], tris[off + 7], tris[off + 8],
px0, py0, px1, py1, buf, w
);
}
}
#end
}
function rasterizeTriangleBounded(
v0x: Float, v0y: Float, v0z: Float,
v1x: Float, v1y: Float, v1z: Float,
v2x: Float, v2y: Float, v2z: Float,
tileX0: Int, tileY0: Int, tileX1: Int, tileY1: Int,
buf: kha.arrays.Float32Array, w: Int
) {
var e0a = v1y - v0y; var e0b = v0x - v1x;
var e1a = v2y - v1y; var e1b = v1x - v2x;
var e2a = v0y - v2y; var e2b = v2x - v0x;
var e0c = -e0a * v0x - e0b * v0y;
var e1c = -e1a * v1x - e1b * v1y;
var e2c = -e2a * v2x - e2b * v2y;
var area = e0a * v2x + e0b * v2y + e0c;
if (area < 0) {
area = -area;
e0a = -e0a; e0b = -e0b; e0c = -e0c;
e1a = -e1a; e1b = -e1b; e1c = -e1c;
e2a = -e2a; e2b = -e2b; e2c = -e2c;
}
if (area < 1e-8) return;
var minX = Std.int(Math.max(tileX0, Math.floor(Math.min(v0x, Math.min(v1x, v2x)))));
var maxX = Std.int(Math.min(tileX1, Math.ceil(Math.max(v0x, Math.max(v1x, v2x)))));
var minY = Std.int(Math.max(tileY0, Math.floor(Math.min(v0y, Math.min(v1y, v2y)))));
var maxY = Std.int(Math.min(tileY1, Math.ceil(Math.max(v0y, Math.max(v1y, v2y)))));
if (minX > maxX || minY > maxY) return;
var d = (v1x - v0x) * (v2y - v0y) - (v2x - v0x) * (v1y - v0y);
var invD = 1.0 / d;
var dzdx = ((v1z - v0z) * (v2y - v0y) - (v2z - v0z) * (v1y - v0y)) * invD;
var dzdy = ((v2z - v0z) * (v1x - v0x) - (v1z - v0z) * (v2x - v0x)) * invD;
var zPush = (Math.abs(dzdx) + Math.abs(dzdy)) * 0.5;
var zMaxTri = v0z;
if (v1z > zMaxTri) zMaxTri = v1z;
if (v2z > zMaxTri) zMaxTri = v2z;
var cx0 = minX + 0.5;
var cy0 = minY + 0.5;
var e0row = e0a * cx0 + e0b * cy0 + e0c;
var e1row = e1a * cx0 + e1b * cy0 + e1c;
var e2row = e2a * cx0 + e2b * cy0 + e2c;
var zrow = v0z + dzdx * (cx0 - v0x) + dzdy * (cy0 - v0y);
for (py in minY...maxY + 1) {
var e0 = e0row; var e1 = e1row; var e2 = e2row;
var z = zrow;
var idx = py * w + minX;
for (px in minX...maxX + 1) {
if (e0 >= 0 && e1 >= 0 && e2 >= 0) {
var zPix = z + zPush;
if (zPix > zMaxTri) zPix = zMaxTri;
if (zPix > 1.0) zPix = 1.0;
if (zPix < buf[idx]) { buf[idx] = zPix; }
}
e0 += e0a; e1 += e1a; e2 += e2a;
z += dzdx;
idx++;
}
e0row += e0b; e1row += e1b; e2row += e2b;
zrow += dzdy;
}
}
public function isOccluded(
minX: Float, minY: Float, minZ: Float,
maxX: Float, maxY: Float, maxZ: Float,
vp: iron.math.Mat4,
screenW: Int, screenH: Int
): Bool {
return hiz.isOccluded(minX, minY, minZ, maxX, maxY, maxZ, vp, screenW, screenH);
}
}

View File

@ -0,0 +1,79 @@
package iron.culling;
class TemporalState {
public var visibleLastFrame: haxe.ds.IntMap<Bool>;
public var visibleLastShadow: haxe.ds.IntMap<Bool>;
public var frameLastTested: haxe.ds.IntMap<Int>;
public var occlusionConfidence: haxe.ds.IntMap<Int>;
public var capacity: Int = 0;
public var frameCount: Int = 0;
public function new() {
visibleLastFrame = new haxe.ds.IntMap<Bool>();
visibleLastShadow = new haxe.ds.IntMap<Bool>();
frameLastTested = new haxe.ds.IntMap<Int>();
occlusionConfidence = new haxe.ds.IntMap<Int>();
}
public function ensureCapacity(cap: Int) {
if (capacity < cap) capacity = cap;
}
public inline function wasVisibleLastFrame(i: Int): Bool {
return visibleLastFrame.exists(i);
}
public inline function wasVisibleShadowLastFrame(i: Int): Bool {
return visibleLastShadow.exists(i);
}
public inline function needsTest(i: Int, maxSkipFrames: Int): Bool {
if (visibleLastFrame.exists(i)) return false; // assumed visible
var lastTested = frameLastTested.get(i);
if (lastTested == null) return true;
return frameCount - lastTested >= maxSkipFrames;
}
public inline function needsVisibleReTest(i: Int, retestInterval: Int): Bool {
var lastTested = frameLastTested.get(i);
if (lastTested == null) return true;
return frameCount - lastTested >= retestInterval;
}
public inline function setVisible(i: Int, visible: Bool) {
if (visible) {
visibleLastFrame.set(i, true);
occlusionConfidence.remove(i);
} else {
visibleLastFrame.remove(i);
var conf = occlusionConfidence.get(i);
occlusionConfidence.set(i, conf == null ? 1 : Std.int(Math.min(255, conf + 1)));
}
}
public inline function markTested(i: Int) {
frameLastTested.set(i, frameCount);
}
public inline function setVisibleShadow(i: Int, visible: Bool) {
if (visible) visibleLastShadow.set(i, true);
else visibleLastShadow.remove(i);
}
public inline function advanceFrame() {
frameCount++;
}
public function clear() {
visibleLastFrame = new haxe.ds.IntMap<Bool>();
visibleLastShadow = new haxe.ds.IntMap<Bool>();
frameLastTested = new haxe.ds.IntMap<Int>();
occlusionConfidence = new haxe.ds.IntMap<Int>();
}
public inline function getConfidence(i: Int): Int {
var c = occlusionConfidence.get(i);
return c == null ? 0 : c;
}
}

View File

@ -14,6 +14,7 @@ import iron.math.Vec4;
import iron.math.Mat4;
import iron.data.SceneFormat;
import iron.data.MeshData;
import iron.data.ShaderData;
class Geometry {
#if lnx_deinterleaved
@ -57,6 +58,7 @@ class Geometry {
// Skinned
#if lnx_skin
public var skeletonTransformsI: Array<Mat4> = null;
public var skeletonBoneRefs: Array<String> = null;
public var actions: Map<String, Array<iron.data.SceneFormat.TObj>> = new Map();
public var mats: Map<String, Array<Mat4>> = new Map();
#end
@ -208,7 +210,7 @@ class Geometry {
for (v in 0...vertexBuffers.length)
if (vertexBuffers[v].name == e.name) {
vbs.push(vertexBuffers[v].buffer);
continue;
break;
}
if (e.name == "ipos" && instancedVB != null) {
vbs.push(instancedVB);
@ -264,7 +266,7 @@ class Geometry {
for (i in 0...vaLength)
vertexBuffers[i] = {
name: vertexArrays[i].attrib,
buffer: makeDeinterleavedVB(vertexArrays[i].values, vertexArrays[i].attrib, vertexArrays[i].size)
buffer: makeDeinterleavedVB(vertexArrays[i])
};
#else
@ -295,14 +297,20 @@ class Geometry {
}
#if lnx_deinterleaved
function makeDeinterleavedVB(data: ByteArray, name: String, structLength: Int): VertexBuffer {
function makeDeinterleavedVB(va: TVertexArray): VertexBuffer {
var struct = new VertexStructure();
struct.add(name, structLength == 2 ? VertexData.Short2Norm : VertexData.Short4Norm);
struct.add(va.attrib, ShaderContext.parseData(va.data));
var vertexBuffer = new VertexBuffer(Std.int(data.byteLength / 2 / structLength), struct, usage);
var vcount = verticesCount(va);
var vertexBuffer = new VertexBuffer(vcount, struct, usage);
var stride = Std.int(struct.byteSize() / 2);
var vertices = vertexBuffer.lock();
for (i in 0...Std.int(vertices.byteLength / 2)) vertices.setInt16(i * 2, data.getInt16(i * 2));
var di = -1;
for (i in 0...vcount) {
for (o in 0...va.size) vertices.setInt16(++di * 2, va.values[i * va.size + o]);
for (o in va.size...stride) vertices.setInt16(++di * 2, 0);
}
vertexBuffer.unlock();
return vertexBuffer;
@ -319,6 +327,33 @@ class Geometry {
// Skinned
#if lnx_skin
public function addArmature(armature: iron.data.Armature) {
for (a in armature.actions) {
addAction(a.bones, a.name);
}
}
public function addAction(bones: Array<TObj>, name: String) {
if (bones == null || skeletonBoneRefs == null) return;
if (actions.get(name) != null) return;
var actionBones: Array<TObj> = [];
for (s in skeletonBoneRefs) {
for (b in bones) {
if (b.name == s) {
actionBones.push(b);
}
}
}
actions.set(name, actionBones);
var actionMats: Array<Mat4> = [];
for (b in actionBones) {
actionMats.push(Mat4.fromFloat32Array(b.transform.values));
}
mats.set(name, actionMats);
}
public function initSkeletonTransforms(transformsI: Array<Float32Array>) {
skeletonTransformsI = [];
for (t in transformsI) {

View File

@ -57,10 +57,8 @@ class MeshBatch {
}
public function removeMesh(m: MeshObject) {
var shader = m.materials[0].shader;
var b = buckets.get(shader);
if (b != null) b.removeMesh(m);
else nonBatched.remove(m);
var b = m.materials.length > 0 ? buckets.get(m.materials[0].shader) : null;
if (b == null || !b.removeMesh(m)) nonBatched.remove(m);
}
@:access(iron.RenderPath)
@ -71,30 +69,52 @@ class MeshBatch {
if (b.meshes.length > 0 && b.meshes[0].cullMaterial(context)) continue;
var scontext = b.shader.getContext(context);
if (scontext == null) continue;
g.setPipeline(scontext.pipeState);
// #if lnx_deinterleaved // TODO
// g.setVertexBuffers(b.vertexBuffers);
// #else
#if lnx_deinterleaved
g.setVertexBuffers(b.getVertexBuffers(scontext.raw.vertex_elements));
#else
g.setVertexBuffer(b.getVertexBuffer(scontext.raw.vertex_elements));
// #end
#end
g.setIndexBuffer(b.indexBuffer);
Uniforms.setContextConstants(g, scontext, bindParams);
RenderPath.sortMeshesDistance(b.meshes);
var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent";
if (!isShadowCtx) RenderPath.sortMeshesDistance(b.meshes);
for (m in b.meshes) {
if (!m.visible) continue; // Skip render if object is hidden
#if lnx_occlusion_culling
if (!m.occlusionVisible && (context == "mesh" || context == "depth" || context == "translucent")) continue;
if (!m.shadowOcclusionVisible && isShadowCtx) continue;
#end
if (m.cullMesh(context, Scene.active.camera, RenderPath.active.light)) continue;
// Get context
var materialContexts: Array<MaterialContext> = [];
var shaderContexts: Array<ShaderContext> = [];
m.getContexts(context, m.materials, materialContexts, shaderContexts);
var materialContexts: Array<MaterialContext> = null;
var shaderContexts: Array<ShaderContext> = null;
if (m.cachedMaterialContexts != null && MeshObject.matsEqual(m.cachedMats.get(context), m.materials)) {
materialContexts = m.cachedMaterialContexts.get(context);
shaderContexts = m.cachedShaderContexts.get(context);
}
if (materialContexts == null) {
materialContexts = [];
shaderContexts = [];
m.getContexts(context, m.materials, materialContexts, shaderContexts);
if (m.cachedMaterialContexts == null) {
m.cachedMats = new Map();
m.cachedMaterialContexts = new Map();
m.cachedShaderContexts = new Map();
}
m.cachedMats.set(context, m.materials);
m.cachedMaterialContexts.set(context, materialContexts);
m.cachedShaderContexts.set(context, shaderContexts);
}
Uniforms.posUnpack = m.data.scalePos;
Uniforms.texUnpack = m.data.scaleTex;
m.transform.update();
if (!isShadowCtx) m.transform.update();
// Render mesh
Uniforms.setObjectConstants(g, scontext, m);
@ -126,8 +146,12 @@ class Bucket {
public var batched = false;
public var shader: ShaderData;
#if lnx_deinterleaved
public var vertexBuffers: Array<InterleavedVertexBuffer> = [];
#else
var vertexBuffer: VertexBuffer;
var vertexBufferMap: Map<String, VertexBuffer> = new Map();
#end
public var indexBuffer: IndexBuffer;
public var meshes: Array<MeshObject> = [];
@ -137,8 +161,12 @@ class Bucket {
public function remove() {
indexBuffer.delete();
#if lnx_deinterleaved
for (buf in vertexBuffers) buf.buffer.delete();
#else
// this.vertexBuffer is in the map, so it's also deleted here
for (buf in vertexBufferMap) buf.delete();
#end
meshes = [];
}
@ -146,10 +174,24 @@ class Bucket {
meshes.push(m);
}
public function removeMesh(m: MeshObject) {
meshes.remove(m);
public function removeMesh(m: MeshObject): Bool {
return meshes.remove(m);
}
#if lnx_deinterleaved
public function getVertexBuffers(elems: Array<TVertexElement>): Array<VertexBuffer> {
var vbs: Array<VertexBuffer> = [];
for (e in elems) {
for (v in vertexBuffers) {
if (v.name == e.name) {
vbs.push(v.buffer);
break;
}
}
}
return vbs;
}
#else
function copyAttribute(attribSize: Int, count: Int, to: ByteArray, toStride: Int, toOffset: Int, from: ByteArray, fromStride: Int, fromOffset: Int) {
for (i in 0...count) {
for (j in 0...attribSize) {
@ -195,6 +237,7 @@ class Bucket {
}
return vb;
}
#end
function vertexCount(g: Geometry, hasUVs: Bool): Int {
var vcount = g.getVerticesLength();
@ -239,6 +282,50 @@ class Bucket {
if (mdatas.length == 0) return;
#if lnx_deinterleaved
var attrNames: Array<String> = [];
var attrReps = new Map<String, TVertexArray>();
for (md in mdatas) {
for (va in md.geom.vertexArrays) {
if (attrReps.get(va.attrib) == null) {
attrReps.set(va.attrib, va);
attrNames.push(va.attrib);
}
}
}
for (name in attrNames) {
var rep = attrReps.get(name);
var attrStruct = new VertexStructure();
attrStruct.add(name, ShaderContext.parseData(rep.data));
var stride = Std.int(attrStruct.byteSize() / 2);
var vtotal = 0;
for (md in mdatas) vtotal += md.geom.getVerticesCount();
var vb = new VertexBuffer(vtotal, attrStruct, Usage.StaticUsage);
var vertices = vb.lock();
var ai = -1;
for (md in mdatas) {
var va: TVertexArray = null;
for (v in md.geom.vertexArrays) {
if (v.attrib == name) {
va = v;
break;
}
}
if (va != null && va.values != null) {
var nverts = md.geom.getVerticesCount();
for (i in 0...nverts) {
for (o in 0...va.size) vertices.setInt16(++ai * 2, va.values[i * va.size + o]);
for (o in va.size...stride) vertices.setInt16(++ai * 2, 0);
}
}
else {
for (i in 0...md.geom.getVerticesCount() * stride) vertices.setInt16(++ai * 2, 0);
}
}
vb.unlock();
vertexBuffers.push({name: name, buffer: vb});
}
#else
// Pick UVs if present
var vs = mdatas[0].geom.struct;
for (md in mdatas) if (md.geom.struct.size() > vs.size()) vs = md.geom.struct;
@ -256,6 +343,7 @@ class Bucket {
var s = "";
for (e in vs.elements) s += e.name;
vertexBufferMap.set(s, vertexBuffer);
#end
indexBuffer = new IndexBuffer(icount, Usage.StaticUsage);
var indices = indexBuffer.lock();

View File

@ -108,6 +108,7 @@ class MeshData {
// Skinned
#if lnx_skin
if (raw.skin != null) {
dat.geom.skeletonBoneRefs = raw.skin.bone_ref_array;
dat.geom.initSkeletonTransforms(raw.skin.transformsI);
}
#end

View File

@ -60,6 +60,16 @@ typedef TMeshData = {
@:optional public var scale_pos: Null<FastFloat>; // Unpack pos from (-1,1) coords
@:optional public var scale_tex: Null<FastFloat>; // Unpack tex from (-1,1) coords
@:optional public var morph_target: TMorphTarget;
@:optional public var occluder_proxy: TOccluderProxy;
}
#if js
typedef TOccluderProxy = {
#else
@:structInit class TOccluderProxy {
#end
public var positions: Float32Array;
public var indices: Uint32Array;
}
#if js

View File

@ -55,6 +55,7 @@ class ShaderData {
if (raw == null) {
trace('Shader data "$name" not found!');
done(null);
return;
}
new ShaderData(raw, done, overrideContext);
});
@ -101,8 +102,9 @@ class ShaderContext {
constants = [];
textureUnits = [];
var instStruct: VertexStructure = null;
if (instancingType > 0) { // Instancing
var instStruct = new VertexStructure();
instStruct = new VertexStructure();
instStruct.add("ipos", VertexData.Float3);
if (instancingType == 2 || instancingType == 4) {
instStruct.add("irot", VertexData.Float3);
@ -116,11 +118,19 @@ class ShaderContext {
this.instanceStride = Std.int(instStruct.byteSize() / 4);
instStruct.instanced = true;
pipeState.inputLayout = [structure, instStruct];
}
else { // Regular
pipeState.inputLayout = [structure];
#if lnx_deinterleaved
var layouts: Array<VertexStructure> = [];
for (e in structure.elements) {
var s = new VertexStructure();
s.add(e.name, e.data);
layouts.push(s);
}
#else
var layouts = [structure];
#end
if (instStruct != null) layouts.push(instStruct);
pipeState.inputLayout = layouts;
// Depth
pipeState.depthWrite = raw.depth_write;

View File

@ -49,7 +49,7 @@ class Reader {
* @see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318229(v=vs.85).aspx
* @see https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header
*/
public function read() : format.bmp.Data {
public function read() : iron.format.bmp.Data {
// Read Header
for (b in ["B".code, "M".code]) {
if (input.readByte() != b) throw "Invalid header";

View File

@ -1,5 +1,5 @@
package format.gif;
import format.gif.Data;
package iron.format.gif;
import iron.format.gif.Data;
import haxe.ds.Vector;
import haxe.io.Bytes;
import haxe.io.Output;

View File

@ -29,7 +29,7 @@
*/
package iron.format.jpg;
class Writer {
class WriterOriginal {
var ZigZag: Array<Int>;
// Static table initialization

View File

@ -40,11 +40,6 @@ class Animation {
public var paused = false;
var frameTime: FastFloat = 1 / 60;
var blendTime: FastFloat = 0.0;
var blendCurrent: FastFloat = 0.0;
var blendFactor: FastFloat = 0.0;
var lastFrameIndex = -1;
var markerEvents: Map<ActionSampler, Map<String, Array<Void->Void>>> = null;
public var activeActions: Map<String, ActionSampler> = null;
@ -63,8 +58,6 @@ class Animation {
public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) {
if (blendTime > 0) {
this.blendTime = blendTime;
this.blendCurrent = 0.0;
frameIndex = 0;
time = 0.0;
}
@ -93,6 +86,7 @@ class Animation {
}
public function update(delta: FastFloat) {
if (paused) return;
if(activeActions == null) return;
for(sampler in activeActions){
@ -160,24 +154,25 @@ class Animation {
while (checkFrameIndex(track.frames, time, frameIndex, speed)) frameIndex += sign;
// Marker events
if (markerEvents != null && anim.marker_names != null && frameIndex != lastFrameIndex) {
if(markerEvents.get(sampler) != null){
if (markerEvents != null && anim.marker_names != null && frameIndex != sampler.offsetOld) {
var markerListeners = markerEvents.get(sampler);
if (markerListeners != null) {
var fromFrame = sampler.offsetOld < 0 ?
(speed > 0 ? -1 : track.frames[track.frames.length - 1] + 1) :
track.frames[sampler.offsetOld];
var toFrame = track.frames[frameIndex];
for (i in 0...anim.marker_frames.length) {
if (frameIndex == anim.marker_frames[i]) {
var markerAct = markerEvents.get(sampler);
var ar = markerAct.get(anim.marker_names[i]);
if (ar != null) for (f in ar) f();
} else {
for (j in 0...(frameIndex - lastFrameIndex)) {
if (lastFrameIndex + j + 1 == anim.marker_frames[i]) {
var markerAct = markerEvents.get(sampler);
var ar = markerAct.get(anim.marker_names[i]);
if (ar != null) for (f in ar) f();
}
}
var markerFrame = anim.marker_frames[i];
var crossed =
(speed > 0 && toFrame >= fromFrame && markerFrame > fromFrame && markerFrame <= toFrame) ||
(speed < 0 && toFrame <= fromFrame && markerFrame <= fromFrame && markerFrame > toFrame) ||
(speed > 0 && toFrame < fromFrame && (markerFrame > fromFrame || markerFrame <= toFrame)) ||
(speed < 0 && toFrame > fromFrame && (markerFrame >= toFrame || markerFrame < fromFrame));
if (crossed) {
var listeners = markerListeners.get(anim.marker_names[i]);
if (listeners != null) for (listener in listeners) listener();
}
}
lastFrameIndex = frameIndex;
}
}
@ -191,8 +186,8 @@ class Animation {
else {
frameIndex -= sign;
sampler.paused = true;
if (sampler.onComplete != null) for(func in sampler.onComplete){ func();};
}
if (sampler.onComplete != null) for(func in sampler.onComplete){ func();};
sampler.trackEnd = true;
}

View File

@ -112,6 +112,10 @@ class BoneAnimation extends Animation {
this.data = mo != null ? mo.data : null;
this.isSkinned = data != null ? data.isSkinned : false;
if (this.isSkinned) {
if (armature != null && data.geom.skeletonBoneRefs != null
&& data.geom.actions.keys().hasNext() == false) {
data.geom.addArmature(armature);
}
var boneSize = 12; // Dual-quat skinning + scaling
this.skinBuffer = new Float32Array(skinMaxBones * boneSize);
for (i in 0...this.skinBuffer.length) this.skinBuffer[i] = 0;
@ -205,18 +209,37 @@ class BoneAnimation extends Animation {
}
function setAction(action: String) {
if (armature == null) return;
armature.initMats();
var a = armature.getAction(action);
if (a == null) return;
skeletonBones = a.bones;
skeletonMats = a.mats;
if(! rootMotionCacheInit) skeletonMats.push(Mat4.identity());
if (isSkinned) {
skeletonBones = data.geom.actions.get(action);
var geomMats = data.geom.mats.get(action);
if (geomMats != null) {
skeletonMats = [];
for (i in 0...geomMats.length) {
var m = Mat4.identity();
m.setFrom(geomMats[i]);
skeletonMats.push(m);
}
}
else skeletonMats = null;
}
if (skeletonBones == null) {
if (armature == null) return;
armature.initMats();
var a = armature.getAction(action);
if (a == null) return;
skeletonBones = a.bones;
skeletonMats = a.mats;
}
if (skeletonMats.length == skeletonBones.length) skeletonMats.push(Mat4.identity()); // Root motion cache
rootMotionCacheInit = true;
setMats();
}
function getAction(action: String): Array<TObj> {
if (isSkinned) {
var bones = data.geom.actions.get(action);
if (bones != null) return bones;
}
if (armature == null) return null;
armature.initMats();
var a = armature.getAction(action);
@ -251,15 +274,91 @@ class BoneAnimation extends Animation {
updateAnimation = f;
}
var blendSampler1: ActionSampler = null;
var blendSampler2: ActionSampler = null;
var blendMats: Array<Mat4> = null;
public function blend(action1: String, action2: String, factor: FastFloat) {
var actionName1: String = getName(action1);
var actionName2: String = getName(action2);
if (blendSampler1 == null || blendSampler1.action != actionName1) {
blendSampler1 = new ActionSampler(actionName1);
registerAction("blendSampler1", blendSampler1);
}
if (blendSampler2 == null || blendSampler2.action != actionName2) {
blendSampler2 = new ActionSampler(actionName2);
registerAction("blendSampler2", blendSampler2);
}
blendSampler1.paused = false;
blendSampler2.paused = false;
if (transitionSampler != null) {
transitionSampler.paused = true;
transitionSampler = null;
}
if (currentPlaySampler != null) currentPlaySampler.paused = true;
currentPlaySampler = blendSampler1;
if (skeletonMats == null) setAction(actionName1);
if (skeletonMats == null) return;
if (blendMats == null) blendMats = initMatsEmpty();
updateAnimation = function(mats) {
sampleAction(blendSampler1, mats);
sampleAction(blendSampler2, blendMats);
blendAction(mats, blendMats, mats, factor);
}
}
var playSamplers: Map<String, ActionSampler> = [];
var currentPlaySampler: ActionSampler = null;
var transitionSampler: ActionSampler = null;
var transitionMats: Array<Mat4> = null;
var transitionTime: FastFloat = 0.0;
var transitionDuration: FastFloat = 0.0;
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) {
var actionName: String = getName(action);
if (actionName != "") {
setAction(actionName);
super.play(actionName, onComplete, blendTime, speed, loop);
var tempAnimParam = new ActionSampler(actionName);
registerAction("tempAction", tempAnimParam);
var sampler = playSamplers.get(actionName);
if (sampler == null) {
if (activeActions != null) sampler = activeActions.get(actionName);
if (sampler == null) sampler = new ActionSampler(actionName);
playSamplers.set(actionName, sampler);
}
if (transitionSampler != null) {
transitionSampler.paused = true;
transitionSampler = null;
}
if (blendTime > 0 && currentPlaySampler != null && currentPlaySampler != sampler && skeletonMats != null) {
transitionSampler = currentPlaySampler;
if (transitionMats == null) transitionMats = initMatsEmpty();
transitionTime = 0.0;
transitionDuration = blendTime;
if (blendSampler1 != null && blendSampler1 != transitionSampler) blendSampler1.paused = true;
if (blendSampler2 != null && blendSampler2 != transitionSampler) blendSampler2.paused = true;
}
else if (currentPlaySampler != null) currentPlaySampler.paused = true;
currentPlaySampler = sampler;
sampler.speed = speed;
sampler.loop = loop;
sampler.onComplete = onComplete != null ? [onComplete] : null;
sampler.restartAction();
registerAction(actionName, sampler);
updateAnimation = function(mats){
sampleAction(tempAnimParam, mats);
if (transitionSampler != null) {
sampleAction(transitionSampler, transitionMats);
sampleAction(sampler, mats);
var f = transitionTime / transitionDuration;
if (f > 1.0) f = 1.0;
f = f * f * (3.0 - 2.0 * f); // Smoothstep
blendAction(transitionMats, mats, mats, f, -1, 0.0);
transitionTime += delta;
if (transitionTime >= transitionDuration) {
transitionSampler.paused = true;
transitionSampler = null;
}
}
else sampleAction(sampler, mats);
}
}
}
@ -279,6 +378,7 @@ class BoneAnimation extends Animation {
#end
super.update(delta);
if (paused) return;
if(updateAnimation != null) {
updateAnimation(skeletonMats);
}
@ -540,12 +640,14 @@ class BoneAnimation extends Animation {
inline function interpolateSample(track: TTrack, m: Mat4, t: FastFloat, ti: Int, sign: Int) {
var tj = ti + sign;
if (tj < 0 || tj >= track.frames.length) tj = ti;
var t1 = track.frames[ti] * frameTime;
var t2 = track.frames[ti + sign] * frameTime;
var s: FastFloat = (t - t1) / (t2 - t1); // Linear
var t2 = track.frames[tj] * frameTime;
var s: FastFloat = t2 != t1 ? (t - t1) / (t2 - t1) : 0.0; // Linear
m1.setF32(track.values, ti * 16); // Offset to 4x4 matrix array
m2.setF32(track.values, (ti + sign) * 16);
m2.setF32(track.values, tj * 16);
// Decompose
m1.decompose(vpos, q1, vscl);
@ -643,7 +745,7 @@ class BoneAnimation extends Animation {
return actionMats != null ? actionMats[getBoneIndex(bone)] : null;
}
function getWorldMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
public function getWorldMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
if (applyParent == null) {
applyParent = [];
for (m in actionMats) applyParent.push(true);
@ -728,7 +830,7 @@ class BoneAnimation extends Animation {
// Skip if root motion is disabled
if(j < 0) continue;
// Use Action 2
if(skeletonBones[j].bone_layers[layerMask] || layerMask < 0){
if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]){
resultMat[i].setFrom(actionMats2[i]);
}
// Use Action 1 if not in layer
@ -744,7 +846,7 @@ class BoneAnimation extends Animation {
// Skip if root motion is disabled
if(j < 0) continue;
// Blend
if(skeletonBones[j].bone_layers[j] || layerMask < 0) {
if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]) {
// Decompose
m.setFrom(actionMats1[i]);
m1.setFrom(actionMats2[i]);
@ -786,8 +888,10 @@ class BoneAnimation extends Animation {
}
else{
for(i in 0...baseActionMats.length){
var j = i == skeletonBones.length ? rootMotionIndex : i;
if(j < 0) continue;
if(skeletonBones[i].bone_layers[layerMask] || layerMask < 0) {
if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]) {
// Decompose
m.setFrom(baseActionMats[i]);
m1.setFrom(addActionMats[i]);

View File

@ -247,25 +247,3 @@ class CameraObject extends Object {
return new Vec4(-transform.world._20, -transform.world._21, -transform.world._22);
}
}
class FrustumPlane {
public var normal = new Vec4(1.0, 0.0, 0.0);
public var constant = 0.0;
public function new() {}
public function normalize() {
var inverseNormalLength = 1.0 / normal.length();
normal.mult(inverseNormalLength);
constant *= inverseNormalLength;
}
public function distanceToSphere(sphereCenter: Vec4, sphereRadius: Float): Float {
return (normal.dot(sphereCenter) + constant) - sphereRadius;
}
public inline function setComponents(x: Float, y: Float, z: Float, w: Float) {
normal.set(x, y, z);
constant = w;
}
}

View File

@ -13,7 +13,10 @@ class Constraint {
}
public function apply(transform: Transform) {
if (target == null && raw.target != null) target = Scene.active.getChild(raw.target).transform;
if (target == null && raw.target != null) {
var o = Scene.active.getChild(raw.target);
if (o != null) target = o.transform;
}
if (target == null && raw.type != "LIMIT_LOCATION" && raw.type != "LIMIT_ROTATION" && raw.type != "LIMIT_SCALE") return;

View File

@ -0,0 +1,25 @@
package iron.object;
import iron.math.Vec4;
class FrustumPlane {
public var normal = new Vec4(1.0, 0.0, 0.0);
public var constant = 0.0;
public function new() {}
public function normalize() {
var inverseNormalLength = 1.0 / normal.length();
normal.mult(inverseNormalLength);
constant *= inverseNormalLength;
}
public function distanceToSphere(sphereCenter: Vec4, sphereRadius: Float): Float {
return (normal.dot(sphereCenter) + constant) - sphereRadius;
}
public inline function setComponents(x: Float, y: Float, z: Float, w: Float) {
normal.set(x, y, z);
constant = w;
}
}

View File

@ -34,7 +34,7 @@ class LightObject extends Object {
// Cascades
public static var cascadeCount = 1;
public static var cascadeSplitFactor = 0.8;
public static var cascadeBounds = 1.0;
public static var cascadeBounds = 2.0; // occlusion culling shadows pop with 1.0
#if lnx_csm
var cascadeData: Float32Array = null;
var cascadeVP: Array<Mat4>;
@ -90,8 +90,16 @@ class LightObject extends Object {
#if lnx_spot
if (data.raw.type == "spot"){
this.size = data.raw.spot_size;
this.blend = data.raw.spot_blend;
this.size = 0.0;
this.blend = 0.0;
if (data.raw.spot_size != null) {
var s: kha.FastFloat = data.raw.spot_size;
this.size = s;
}
if (data.raw.spot_blend != null) {
var b: kha.FastFloat = data.raw.spot_blend;
this.blend = b;
}
}
#end
@ -132,6 +140,9 @@ class LightObject extends Object {
}
#end
#end
#if lnx_occlusion_culling
if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.removeLight(this);
#end
super.remove();
}
@ -270,12 +281,13 @@ class LightObject extends Object {
var worldPerTexelX = (maxx - minx) / smsize;
var worldPerTexelY = (maxy - miny) / smsize;
var worldPerTexelZ = (maxz - minz) / smsize;
var ex = maxx - minx, ey = maxy - miny, ez = maxz - minz;
minx = Math.floor(minx / worldPerTexelX) * worldPerTexelX;
miny = Math.floor(miny / worldPerTexelY) * worldPerTexelY;
minz = Math.floor(minz / worldPerTexelZ) * worldPerTexelZ;
maxx = Math.floor(maxx / worldPerTexelX) * worldPerTexelX;
maxy = Math.floor(maxy / worldPerTexelY) * worldPerTexelY;
maxz = Math.floor(maxz / worldPerTexelZ) * worldPerTexelZ;
maxx = minx + ex;
maxy = miny + ey;
maxz = minz + ez;
var hx = (maxx - minx) / 2;
var hy = (maxy - miny) / 2;
@ -662,7 +674,7 @@ class LightObject extends Object {
break;
}
if (discardLightCulled(light)) continue;
if (light.data.raw.type == type) {
if (light.data.raw.type == type || (type == "spot" && light.data.raw.type == "area")) {
m.setFrom(light.VP);
m.multmat(Uniforms.biasMat);
#if lnx_shadowmap_atlas
@ -699,8 +711,8 @@ class LightObject extends Object {
LWVPMatrixArray[i * 16 + 13] = m._31;
LWVPMatrixArray[i * 16 + 14] = m._32;
LWVPMatrixArray[i * 16 + 15] = m._33;
i++; // only increment in light type
}
}
i++;
}
return LWVPMatrixArray;
}
@ -779,7 +791,7 @@ class LightObject extends Object {
public static var spotTileBoundsArray: Float32Array = null;
public static function updateSpotTileBoundsArray(): Float32Array {
var maxLights = getMaxLights();
var maxLights = maxLightsCluster;
if (spotTileBoundsArray == null) {
spotTileBoundsArray = new Float32Array(maxLights * 4);
}
@ -787,19 +799,20 @@ class LightObject extends Object {
var i = 0;
for (light in Scene.active.lights) {
if (i >= maxLights) break;
if (!light.visible || light.data.raw.type != "spot") continue;
if (!light.data.raw.cast_shadow || light.data.raw.strength == 0.0) continue;
var idx = i * 4;
spotTileBoundsArray[idx ] = light.tileOffsetX[0];
spotTileBoundsArray[idx + 2] = light.tileOffsetX[0] + light.tileScale[0];
#if (!kha_opengl)
// flip bounds
spotTileBoundsArray[idx + 1] = 1.0 - (light.tileOffsetY[0] + light.tileScale[0]);
spotTileBoundsArray[idx + 3] = 1.0 - light.tileOffsetY[0];
#else
spotTileBoundsArray[idx + 1] = light.tileOffsetY[0];
spotTileBoundsArray[idx + 3] = light.tileOffsetY[0] + light.tileScale[0];
#end
if (discardLightCulled(light)) continue;
if ((light.data.raw.type == "spot" || light.data.raw.type == "area") && light.data.raw.cast_shadow) {
var idx = i * 4;
spotTileBoundsArray[idx ] = light.tileOffsetX[0];
spotTileBoundsArray[idx + 2] = light.tileOffsetX[0] + light.tileScale[0];
#if (!kha_opengl)
// flip bounds
spotTileBoundsArray[idx + 1] = 1.0 - (light.tileOffsetY[0] + light.tileScale[0]);
spotTileBoundsArray[idx + 3] = 1.0 - light.tileOffsetY[0];
#else
spotTileBoundsArray[idx + 1] = light.tileOffsetY[0];
spotTileBoundsArray[idx + 3] = light.tileOffsetY[0] + light.tileScale[0];
#end
}
i++;
}
return spotTileBoundsArray;

View File

@ -30,13 +30,24 @@ class MeshObject extends Object {
public var frustumCulling = true;
public var activeTilesheet: Tilesheet = null;
public var tilesheets: Array<Tilesheet> = null;
#if lnx_occlusion_culling
public var occlusionVisible = true;
public var shadowOcclusionVisible = true;
#end
var shadowCameraCulled: Bool = false;
var shadowCameraCullFrame: Int = -1;
var shadowCameraCullCamera: CameraObject = null;
var shadowCameraCullLight: LightObject = null;
public var skip_context: String = null; // Do not draw this context
public var force_context: String = null; // Draw only this context
static var lastPipeline: PipelineState = null;
var cachedMats: Map<String, Vector<MaterialData>> = null;
var cachedMaterialContexts: Map<String, Array<MaterialContext>> = null;
var cachedShaderContexts: Map<String, Array<ShaderContext>> = null;
#if lnx_morph_target
public var morphTarget: MorphTarget = null;
#end
public var vertexGroups: Map<String, Array<Vec4>> = null;
#if lnx_veloc
public var prevMatrix = Mat4.identity();
@ -48,6 +59,9 @@ class MeshObject extends Object {
this.materials = materials;
setData(data);
Scene.active.meshes.push(this);
#if lnx_occlusion_culling
if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.markDirty();
#end
}
public function setData(data: MeshData) {
@ -96,6 +110,9 @@ class MeshObject extends Object {
if (activeTilesheet != null) activeTilesheet.remove();
if (tilesheets != null) { for (ts in tilesheets) { ts.remove(); } tilesheets = null; }
if (Scene.active != null) Scene.active.meshes.remove(this);
#if lnx_occlusion_culling
if (iron.culling.CullSystem.active != null) iron.culling.CullSystem.active.markDirty();
#end
#if (rp_renderer == "Deferred")
if (Scene.active != null) Scene.active.markMaterialParamsDirty();
#end
@ -196,7 +213,7 @@ class MeshObject extends Object {
var isShadow = context == "shadowmap";
if (camera == null) return false;
if (camera.data.raw.frustum_culling && frustumCulling) {
if (camera.data.raw.frustum_culling && frustumCulling && RenderPath.active != null && RenderPath.active.frustumCulling) {
// Scale radius for skinned mesh and particle system
// TODO: define skin & particle bounds
var radiusScale = data.isSkinned ? 2.0 : 1.0;
@ -211,7 +228,8 @@ class MeshObject extends Object {
var frustumPlanes = isShadow ? light.frustumPlanes : camera.frustumPlanes;
if (isShadow && light.data.raw.type != "sun") { // Non-sun light bounds intersect camera frustum
light.transform.radius = light.data.raw.far_plane;
var farPlane: kha.FastFloat = light.data.raw.far_plane;
light.transform.radius = farPlane * 1.25;
if (!CameraObject.sphereInFrustum(camera.frustumPlanes, light.transform)) {
return setCulled(isShadow, true);
}
@ -220,10 +238,40 @@ class MeshObject extends Object {
if (!CameraObject.sphereInFrustum(frustumPlanes, transform, radiusScale)) {
return setCulled(isShadow, true);
}
}
//culled = false;
//return culled;
#if rp_shadowmap
if (isShadow && light.data.raw.type == "sun") {
var frame = RenderPath.active.frame;
if (shadowCameraCullFrame == frame && shadowCameraCullCamera == camera &&
shadowCameraCullLight == light) {
if (shadowCameraCulled) return setCulled(isShadow, true);
} else {
shadowCameraCullFrame = frame;
shadowCameraCullCamera = camera;
shadowCameraCullLight = light;
shadowCameraCulled = false;
var ldx = -light.V._02;
var ldy = -light.V._12;
var ldz = -light.V._22;
var radius = transform.radius * radiusScale;
var cx = transform.worldx();
var cy = transform.worldy();
var cz = transform.worldz();
var range = camera.data.raw.far_plane * (LightObject.cascadeBounds * 4.0);
var margin = radius * 0.1;
for (plane in camera.frustumPlanes) {
var reach = plane.normal.x * cx + plane.normal.y * cy + plane.normal.z * cz + plane.constant + radius + margin;
var dot = plane.normal.x * ldx + plane.normal.y * ldy + plane.normal.z * ldz;
if (dot > 0) reach += dot * range;
if (reach < 0) {
shadowCameraCulled = true;
return setCulled(isShadow, true);
}
}
}
}
#end
}
return setCulled(isShadow, false);
}
@ -253,6 +301,12 @@ class MeshObject extends Object {
}
}
static function matsEqual(a: Vector<MaterialData>, b: Vector<MaterialData>): Bool {
if (a == null || b == null || a.length != b.length) return false;
for (i in 0...a.length) if (a[i] != b[i]) return false;
return true;
}
public function render(g: Graphics, context: String, bindParams: Array<String>) {
if (data == null || !data.geom.ready) return; // Data not yet streamed
if (!visible) return; // Skip render if object is hidden
@ -293,6 +347,7 @@ class MeshObject extends Object {
// Get lod
var mats = materials;
var lod = this;
var isShadowCtx = context == "shadowmap" || context == "shadowmap_transparent";
if (raw != null && raw.lods != null && raw.lods.length > 0) {
computeScreenSize(Scene.active.camera);
initLods();
@ -317,13 +372,29 @@ class MeshObject extends Object {
if (isLodMaterial() && !validContext(mats, context)) return;
// Get context
var materialContexts: Array<MaterialContext> = [];
var shaderContexts: Array<ShaderContext> = [];
getContexts(context, mats, materialContexts, shaderContexts);
var materialContexts: Array<MaterialContext> = null;
var shaderContexts: Array<ShaderContext> = null;
if (cachedMaterialContexts != null && matsEqual(cachedMats.get(context), mats)) {
materialContexts = cachedMaterialContexts.get(context);
shaderContexts = cachedShaderContexts.get(context);
}
if (materialContexts == null) {
materialContexts = [];
shaderContexts = [];
getContexts(context, mats, materialContexts, shaderContexts);
if (cachedMaterialContexts == null) {
cachedMats = new Map();
cachedMaterialContexts = new Map();
cachedShaderContexts = new Map();
}
cachedMats.set(context, mats);
cachedMaterialContexts.set(context, materialContexts);
cachedShaderContexts.set(context, shaderContexts);
}
Uniforms.posUnpack = data.scalePos;
Uniforms.texUnpack = data.scaleTex;
transform.update();
if (!isShadowCtx) transform.update();
// Render mesh
var ldata = lod.data;

View File

@ -15,6 +15,7 @@ class Object {
public var transform: Transform;
public var constraints: Array<Constraint> = null;
public var traits: Array<Trait> = [];
public var vertexGroups: Map<String, Array<iron.math.Vec4>> = null;
public var parent: Object = null;
public var children: Array<Object> = [];

View File

@ -17,6 +17,7 @@ class ObjectAnimation extends Animation {
public var oactions: Array<TSceneFormat>;
var oaction: TObj;
var s0: FastFloat = 0.0;
var delta: FastFloat = 0;
var bezierFrameIndex = -1;
var updateAnimation: Map<String, FastFloat> -> Void;
@ -26,7 +27,11 @@ class ObjectAnimation extends Animation {
public var transformMap: Map<String, FastFloat>;
var defaultSampler: ActionSampler = null;
static inline var DEFAULT_SAMPLER_ID = "__object_default_action__";
var playSamplers: Map<String, ActionSampler> = [];
var transitionSampler: ActionSampler = null;
var transitionMap: Map<String, FastFloat> = null;
var transitionTime: FastFloat = 0.0;
var transitionDuration: FastFloat = 0.0;
public static var trackNames: Array<String> = [ "xloc", "yloc", "zloc",
"xrot", "yrot", "zrot",
@ -67,27 +72,59 @@ class ObjectAnimation extends Animation {
oaction = getAction(this.action);
if (oaction != null) {
isSampled = oaction.sampled != null && oaction.sampled;
if (defaultSampler != null) {
deRegisterAction(DEFAULT_SAMPLER_ID);
var sampler = playSamplers.get(this.action);
if (sampler == null) {
if (activeActions != null) sampler = activeActions.get(this.action);
if (sampler == null) sampler = new ActionSampler(this.action);
playSamplers.set(this.action, sampler);
}
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;
if (transitionSampler != null) {
transitionSampler.paused = true;
transitionSampler = null;
}
if (blendTime > 0 && defaultSampler != null && defaultSampler != sampler) {
transitionSampler = defaultSampler;
transitionMap = initTransformMap();
transitionTime = 0.0;
transitionDuration = blendTime;
}
else if (defaultSampler != null) defaultSampler.paused = true;
defaultSampler = sampler;
sampler.speed = speed;
sampler.loop = loop;
sampler.onComplete = onComplete != null ? [onComplete] : null;
sampler.restartAction();
registerAction(this.action, sampler);
updateAnimation = function(map: Map<String, FastFloat>) {
sampleAction(defaultSampler, map);
if (transitionSampler != null) {
sampleAction(transitionSampler, transitionMap);
sampleAction(sampler, map);
var f = transitionTime / transitionDuration;
if (f > 1.0) f = 1.0;
f = f * f * (3.0 - 2.0 * f); // Smoothstep
blendActionObject(transitionMap, map, map, f);
transitionTime += delta;
if (transitionTime >= transitionDuration) {
transitionSampler.paused = true;
transitionSampler = null;
}
}
else sampleAction(sampler, map);
};
}
else {
if (defaultSampler != null) {
deRegisterAction(DEFAULT_SAMPLER_ID);
defaultSampler = null;
if (transitionSampler != null) {
transitionSampler.paused = true;
transitionSampler = null;
}
if (defaultSampler != null) defaultSampler.paused = true;
defaultSampler = null;
updateAnimation = null;
}
}
override public function update(delta: FastFloat) {
this.delta = delta;
if (!object.visible || object.culled) return;
#if lnx_debug
@ -98,7 +135,6 @@ class ObjectAnimation extends Animation {
transformMap = initTransformMap();
super.update(delta);
if (defaultSampler != null) defaultSampler.paused = paused;
if (paused) return;
if (updateAnimation == null) return;
if (!isSkinned) updateObjectAnimation();
@ -160,11 +196,13 @@ class ObjectAnimation extends Animation {
var ti = sampler.offset;
//ti = ti < 0 ? 1 : ti;
var tj = ti + sign;
if (tj < 0 || tj >= track.frames.length) tj = ti;
var t1 = track.frames[ti] * frameTime;
var t2 = track.frames[ti + sign] * frameTime;
var t2 = track.frames[tj] * frameTime;
var v1 = track.values[ti];
var v2 = track.values[ti + sign];
var v2 = track.values[tj];
var value = interpolateLinear(t, t1, t2, v1, v2);
@ -202,6 +240,7 @@ class ObjectAnimation extends Animation {
}
inline function interpolateLinear(t: FastFloat, t1: FastFloat, t2: FastFloat, v1: FastFloat, v2: FastFloat): Null<FastFloat> {
if (t2 == t1) return v1;
var s = (t - t1) / (t2 - t1);
return (1.0 - s) * v1 + s * v2;
}

View File

@ -131,13 +131,20 @@ class Transform {
dscale = new Vec4().set(1, 1, 1);
}
static var _cdLoc = new Vec4();
static var _cdRot = new Quat();
static var _cdScale = new Vec4();
function composeDelta() {
// Delta transform
dloc.addvecs(loc, dloc);
dscale.addvecs(dscale, scale);
drot.fromEuler(_deulerX, _deulerY, _deulerZ);
drot.multquats(rot, drot);
local.compose(dloc, drot, dscale);
// Delta transform: additive location, multiplicative rotation and scale
_cdLoc.addvecs(loc, dloc);
_cdScale.x = scale.x * dscale.x;
_cdScale.y = scale.y * dscale.y;
_cdScale.z = scale.z * dscale.z;
_cdRot.fromEuler(_deulerX, _deulerY, _deulerZ);
_cdRot.multquats(_cdRot, drot);
_cdRot.multquats(rot, _cdRot);
local.compose(_cdLoc, _cdRot, _cdScale);
}
/**

View File

@ -928,6 +928,14 @@ class Uniforms {
case "_tileBoundsSunArray": {
fa = LightObject.updateSunTileBoundsArray();
}
case "_tileBoundsSpotArray": {
fa = LightObject.updateSpotTileBoundsArray();
}
#end
#if (!lnx_clusters && lnx_spot)
case "_biasLightWorldViewProjectionMatrixSpotArray": {
fa = updateSpotMatrixFloats();
}
#end
}
@ -1387,6 +1395,11 @@ class Uniforms {
fa = LightObject.updateLWVPMatrixArray(object, "spot");
}
#end // lnx_clusters
#if (!lnx_clusters && lnx_spot)
case "_biasLightWorldViewProjectionMatrixSpotArray": {
fa = updateSpotMatrixFloats();
}
#end
#if lnx_morph_target
case "_morphWeights": {
fa = cast(object, MeshObject).morphTarget.morphWeights;
@ -1473,6 +1486,32 @@ class Uniforms {
}
return null;
}
static var spotMatrixFloats: Float32Array = null;
static function updateSpotMatrixFloats(): Float32Array {
var light = getSpot(0);
if (light == null) return null;
if (spotMatrixFloats == null) spotMatrixFloats = new Float32Array(16);
helpMat.setFrom(light.VP);
helpMat.multmat(biasMat);
spotMatrixFloats[0 ] = helpMat._00;
spotMatrixFloats[1 ] = helpMat._01;
spotMatrixFloats[2 ] = helpMat._02;
spotMatrixFloats[3 ] = helpMat._03;
spotMatrixFloats[4 ] = helpMat._10;
spotMatrixFloats[5 ] = helpMat._11;
spotMatrixFloats[6 ] = helpMat._12;
spotMatrixFloats[7 ] = helpMat._13;
spotMatrixFloats[8 ] = helpMat._20;
spotMatrixFloats[9 ] = helpMat._21;
spotMatrixFloats[10] = helpMat._22;
spotMatrixFloats[11] = helpMat._23;
spotMatrixFloats[12] = helpMat._30;
spotMatrixFloats[13] = helpMat._31;
spotMatrixFloats[14] = helpMat._32;
spotMatrixFloats[15] = helpMat._33;
return spotMatrixFloats;
}
#end
static function currentMat(object: Object): MaterialData {

View File

@ -166,6 +166,7 @@ class LnxPack {
case "anim": TAnimation;
case "tracks": TTrack;
case "morph_target": TMorphTarget;
case "occluder_proxy": TOccluderProxy;
case "vertex_groups": TVertex_groups;
case "tilesheet": TTilesheetData;
case "events": TTilesheetEvent;

View File

@ -38,14 +38,14 @@ class Time {
static var normalModeLogged: Bool = false;
#end
static var lastTime = 0.0;
static var lastTime = -1.0;
static var _delta = 0.0;
public static var delta(get, never): Float;
static function get_delta(): Float {
return _delta;
}
static var lastRenderTime = 0.0;
static var lastRenderTime = -1.0;
static var _renderDelta = 0.0;
public static var renderDelta(get, never): Float;
static function get_renderDelta(): Float {
@ -79,12 +79,14 @@ class Time {
}
#end
_delta = realTime() - lastTime;
lastTime = realTime();
var now = realTime();
_delta = lastTime < 0.0 ? 0.0 : now - lastTime;
lastTime = now;
}
public static function render() {
_renderDelta = realTime() - lastRenderTime;
lastRenderTime = realTime();
var now = realTime();
_renderDelta = lastRenderTime < 0.0 ? 0.0 : now - lastRenderTime;
lastRenderTime = now;
}
}

View File

@ -1,79 +0,0 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if js
import leenkx.network.Leenkx;
#end
import iron.object.Object;
class AddTorrentNode extends LogicNode {
public var title: String;
public var values: Array<Dynamic>;
public var net_Url: String;
public var data: Dynamic;
public var runner: Bool = false;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection,torrentid,net_Url){
#if js
if (runner == false){
values = [];
var script = 'var torrentId = "' + torrentid + '";
lx_' + net_Url +'.wt.add(torrentId, function (torrent) {
leenkx.network.Leenkx.torrent.set("' + net_Url +'", torrent.files);
torrent.on("done", function () {
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
})
});
';
js.Syntax.code('(1, eval)({0})', script.toString());
runner = true;
promiseResult(connection,torrentid,net_Url);
return;
}else{
try{
values = leenkx.network.Leenkx.torrent.get(net_Url);
if(values.length > 0){
runOutput(0);
runner = false;
return;
}
}
catch(error){
haxe.Timer.delay(function () {
promiseResult(connection,torrentid,net_Url);
return;
}, 100);
}
}
#end
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var torrentid = inputs[2].get();
if (torrentid == null) return;
#if js
net_Url = connection._url;
promiseResult(connection,torrentid,net_Url);
#end
}
override function get(from: Int): Dynamic {
#if js
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: leenkx.network.Leenkx.torrent.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -35,13 +35,13 @@ class AnimActionNode extends LogicNode {
if(object.animation == null) {
#if lnx_skin
animationBone = object.getBoneAnimation(object.uid);
animationBone.registerAction(property0, sampler);
animationBone.registerAction(sampler.action, sampler);
func = sampleBonaAction;
#end
}
else{
animationObject = cast(object.animation, ObjectAnimation);
animationObject.registerAction(property0, sampler);
animationObject.registerAction(sampler.action, sampler);
func = sampleObjectAction;
}

View File

@ -61,14 +61,16 @@ class BlendActionNode extends LogicNode {
return;
}
if(factor > 0.95) {
inputs[2].get()(animMats);
var action2 = inputs[2].get();
if (action2 != null) action2(animMats); else inputs[1].get()(animMats);
return;
}
}
inputs[1].get()(animMats);
inputs[2].get()(tempMats);
var action2 = inputs[2].get();
if (action2 == null) return;
action2(tempMats);
animationBone.blendAction(animMats, tempMats, animMats, factor, boneLayer);
}

View File

@ -1,131 +1,170 @@
package leenkx.logicnode;
import iron.object.Object;
#if js
import leenkx.network.Types;
import leenkx.network.Util;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
import leenkx.network.torrent.TorrentClient;
import leenkx.network.torrent.peer.RtcPeerPool;
#end
import leenkx.system.Event;
class CreateLeenkxNode extends LogicNode {
public var property0: String;
public var property1: Bool;
public var property2: String;
public var property3: String;
public var net_Url: String;
public var data: Dynamic;
public var client: Dynamic;
public var value: String;
public var listener: TEvent = null;
public function new(tree:LogicTree) {
super(tree);
super(tree);
}
override function run(from:Int) {
#if js
net_Url = inputs[1].get();
if (net_Url == null) return;
if(Leenkx.connections[net_Url] == null){
var object = tree.object;
value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid);
client = new leenkx.network.Leenkx(net_Url, object);
} else {
return;
}
#end
}
function onEvent() {
#if js
var lnxjs:Dynamic = js.Lib.global;
var lnxCxNew = lnxjs.lnxNew;
if (lnxCxNew == null) {
trace("ERROR: lnxNew not available");
return;
#if lnx_torrent
net_Url = inputs[1].get();
if (net_Url == null) return;
if (Leenkx.connections[net_Url] == null) {
value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid);
new leenkx.network.Leenkx(net_Url, tree.object);
}
var lnxCn:Dynamic = lnxCxNew(net_Url);
lnxCn.on("connections", function(c) {
leenkx.network.Leenkx.data.set(net_Url, c + 1);
leenkx.network.Leenkx.connections[net_Url].onconnections();
});
lnxCn.on("message", function(address, message) {
leenkx.network.Leenkx.data.set(net_Url, message);
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onmessage();
});
lnxCn.on("seen", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onseen();
});
lnxCn.on("left", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onleft();
});
lnxCn.on("server", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onserver();
});
lnxCn.on("ping", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onping();
});
lnxCn.on("timeout", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].ontimeout();
});
lnxCn.on("rpc", function(address, call, args, nonce) {
leenkx.network.Leenkx.data.set(net_Url, call);
leenkx.network.Leenkx.id.set(net_Url, address);
call(args);
leenkx.network.Leenkx.connections[net_Url].onrpc();
});
lnxCn.on("rpc-response", function(address, nonce, response) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onrpcresponse();
});
lnxCn.on("wireleft", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireleft();
});
lnxCn.on("wireseen", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireseen();
});
lnxCn.on("torrent", function(identifier, torrent) {
leenkx.network.Leenkx.data.set(net_Url, torrent);
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontorrent();
});
lnxCn.on("tracker", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontracker();
});
lnxCn.on("announce", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].onannounce();
});
Reflect.setField(lnxjs, "lnx_" + net_Url, lnxCn);
Leenkx.connections[net_Url].client = lnxCn;
var script = 'globalThis.addEventListener("beforeunload", function (e) {
leenkx.network.Leenkx.connections.h["' + net_Url + '"].client.destroy();
delete e["returnValue"];
});';
js.Syntax.code('(1, eval)({0})', script);
runOutput(0);
#end
}
function onEvent() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
#if lnx_torrent
var sock = Leenkx.connections[net_Url];
if (sock == null) return;
if (sock.client != null) {
runOutput(0);
return;
}
var opts:leenkx.network.torrent.Types.LeenkxOptions = {deferJoin: true};
var nextInput = 2;
if (property0 == "datachannel") {
opts.transport = "datachannel";
var imode = property2 != null ? property2 : "inherit";
if (imode != "inherit") {
if (inputs.length > nextInput) {
var ice = TorrentClient.trackerUrls(inputs[nextInput].get());
if (ice != null) {
opts.iceServers = imode == "override" ? ice : RtcPeerPool.defaultIceServers.concat(ice);
}
}
nextInput++;
}
}
var tmode = property3 != null ? property3 : "inherit";
var trackers:Array<String> = null;
if (tmode != "inherit" && inputs.length > nextInput) {
trackers = TorrentClient.trackerUrls(inputs[nextInput].get());
}
opts.announce = tmode == "override" && trackers != null ? trackers
: trackers != null ? TorrentClient.defaultTrackers.concat(trackers)
: TorrentClient.defaultTrackers;
var lnxCn = new LeenkxClient(net_Url, opts);
lnxCn.isRelay = property1 == true;
lnxCn.onOpen = function() {
Leenkx.data.set(net_Url, lnxCn.lastPeerIndex);
Leenkx.id.set(net_Url, lnxCn.lastPeerId);
sock.onopen();
};
lnxCn.onConnections = function(c) {
Leenkx.data.set(net_Url, c + 1);
sock.onconnections();
};
lnxCn.onMessage = function(address, message, nonce) {
Leenkx.data.set(net_Url, message);
Leenkx.id.set(net_Url, address);
sock.onmessage();
};
lnxCn.onSeen = function(address) {
Leenkx.id.set(net_Url, address);
sock.onseen();
};
lnxCn.onLeft = function(address) {
Leenkx.id.set(net_Url, address);
sock.onleft();
sock.onclose();
};
lnxCn.onServer = function(address) {
Leenkx.id.set(net_Url, address);
sock.onserver();
};
lnxCn.onPing = function(address) {
Leenkx.id.set(net_Url, address);
sock.onping();
};
lnxCn.onTimeout = function(address) {
Leenkx.id.set(net_Url, address);
sock.ontimeout();
sock.onclose();
};
lnxCn.onWireLeft = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireleft();
};
lnxCn.onWireSeen = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireseen();
};
lnxCn.onTorrent = function(identifier, torrent) {
Leenkx.data.set(net_Url, torrent);
Leenkx.id.set(net_Url, identifier);
sock.ontorrent();
};
lnxCn.onTracker = function(identifier, tracker) {
Leenkx.id.set(net_Url, identifier);
sock.ontracker();
};
lnxCn.onAnnounce = function(identifier) {
Leenkx.id.set(net_Url, identifier);
sock.onannounce();
};
lnxCn.onError = function(msg) {
Leenkx.data.set(net_Url, msg);
sock.onerror();
};
lnxCn.onTorrentAdded = function(t) {
Leenkx.torrent.set(net_Url, t);
Leenkx.file.set(net_Url, t.files);
Leenkx.data.set(net_Url, t);
sock.ontorrentadded();
};
lnxCn.onTorrentDone = function(t) {
Leenkx.data.set(net_Url, t);
sock.ontorrentdone();
};
sock.client = lnxCn;
lnxCn.connect();
#if (js && !kha_krom)
js.Browser.window.addEventListener("beforeunload", function(e) {
lnxCn.destroy();
});
#end
#end
runOutput(0);
}
override function get(from: Int): Dynamic {
#if js
#if lnx_torrent
return switch (from) {
case 1: Leenkx.connections[net_Url];
case 2: Leenkx.id.get(net_Url);

View File

@ -1,43 +1,100 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import iron.object.Object;
import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.TorrentClient;
#end
class CreateTorrentNode extends LogicNode {
public var torrent: Dynamic;
public var net_Url: String;
public var meta:Dynamic;
public var metaBytes:Dynamic;
public var magnetUri:Dynamic;
public var infoHashHex:String;
public var property0:String;
public var property1:String;
public function new(tree:LogicTree) {
super(tree);
}
}
override function run(from:Int) {
#if lnx_torrent
meta = null;
metaBytes = null;
magnetUri = null;
infoHashHex = null;
var connection = inputs[1].get();
if (connection == null) return;
var data = inputs[2].get();
if (data == null) return;
net_Url = connection._url;
var data = inputs[1].get();
if (data == null) return;
var mode = property1 != null ? property1 : "file";
var opts:Dynamic = {};
var mv = property0 != null ? Std.parseInt(property0) : null;
if (mv != null) Reflect.setField(opts, "metaVersion", mv);
field(2, opts, "name");
field(3, opts, "comment");
field(4, opts, "createdBy");
field(5, opts, "source");
field(6, opts, "announce");
field(7, opts, "private");
field(8, opts, "pieceLength");
field(9, opts, "urlList");
var script = 'var file = ' + data + ';
lx_' + net_Url +'.wt.add(file, function (torrent) {
leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files);
torrent.on("done", function () {
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
});
});
';
if (mode == "folder") {
#if sys
data = TorrentClient.readFolder(Std.string(data));
var first:Dynamic = data[0];
if (first != null && Reflect.field(opts, "name") == null) {
var p = Std.string(first.path);
var i = p.indexOf("/");
Reflect.setField(opts, "name", p.substring(0, i >= 0 ? i : p.length));
}
#else
trace("Folder mode requires a sys target";
#end
}
js.Syntax.code('(1, eval)({0})', script.toString());
var m;
try {
m = TorrentClient.buildMeta(data, opts);
} catch(e:Dynamic) {
trace(e);
return;
}
meta = m;
metaBytes = m.toTorrentBytes();
infoHashHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
magnetUri = Magnet.build(m.infoHash, m.name, m.announce, m.urlList, m.infoHashV2);
#end
runOutput(0);
}
#if lnx_torrent
function field(i:Int, opts:Dynamic, name:String):Void {
if (i >= inputs.length) return;
var v:Dynamic = inputs[i].get();
if (v == null || Std.string(v) == "") {
return;
}
if (name == "announce") {
v = TorrentClient.trackerUrls(v);
if (v == null) return;
}
Reflect.setField(opts, name, v);
}
#end
override function get(from: Int): Dynamic {
return switch (from) {
case 1: Leenkx.file.get(net_Url);
default: throw "Unreachable";
}
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: meta;
case 2: metaBytes;
case 3: infoHashHex;
case 4: magnetUri;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,88 @@
package leenkx.logicnode;
#if (lnx_torrent && (sys || kha_krom))
import leenkx.network.torrent.tracker.HttpTracker.HttpTrackerServer;
import leenkx.network.torrent.tracker.UdpTracker.UdpTrackerServer;
import leenkx.network.torrent.transport.WsTrackerClient.WsTrackerServer;
#end
#if (lnx_torrent && sys)
import leenkx.network.torrent.transport.WsTrackerClient.WssTrackerServer;
#end
class CreateTrackerNode extends LogicNode {
public var property0:String;
public var tracker:Dynamic;
public var url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
tracker = null;
url = null;
startServer();
runOutput(0);
}
function startServer():Void {
#if (lnx_torrent && (sys || kha_krom))
var hv = inputs[1].get();
var host = hv != null ? Std.string(hv) : "127.0.0.1";
if (host == "") host = "127.0.0.1";
var pv = inputs[2].get();
var port = pv != null ? Std.int(Std.parseFloat(Std.string(pv))) : 0;
if (port <= 0) port = 6969;
var scheme = property0 != null ? property0 : "udp";
switch (scheme) {
case "http":
var s = new HttpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "http://" + host + ":" + port + "/announce";
case "ws":
var s = new WsTrackerServer(host, port);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "ws://" + host + ":" + port;
case "wss":
#if sys
var cert = inputs.length > 3 ? Std.string(inputs[3].get()) : null;
var key = inputs.length > 4 ? Std.string(inputs[4].get()) : null;
if (cert == null || cert == "" || key == null || key == "") {
trace("wss server needs Cert File and Key File");
return;
}
var s = new WssTrackerServer(host, port, cert, key);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "wss://" + host + ":" + port;
#else
trace("wss server requires a sys target");
#end
default:
var s = new UdpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "udp://" + host + ":" + port;
}
#else
trace("server mode needs a sys or krom target");
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: tracker;
case 2: url;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,101 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.TorrentClient;
#end
class DownloadTorrentNode extends LogicNode {
public var net_Url:String;
public var torrent:Dynamic;
public var property0:String;
public var property1:String;
var listener:TEvent = null;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection, torrentid, net_Url, ?opts:Dynamic) {
#if lnx_torrent
var socket = Leenkx.connections[net_Url];
if (socket == null) {
if (listener == null) {
var self = this;
listener = Event.add(Leenkx.onLoadEvent, function() {
if (Leenkx.connections[net_Url] == null) return;
if (self.listener != null) {
Event.removeListener(self.listener);
self.listener = null;
}
self.promiseResult(connection, torrentid, net_Url, opts);
});
}
return;
}
var client = socket.client;
if (client == null) return;
var self = this;
client.add(torrentid, opts, function(torrent) {
self.torrent = torrent;
var onReady = function() {
self.runOutput(0);
};
var onDone = function() {
self.runOutput(4);
};
if (torrent.ready) onReady();
else torrent.on("ready", onReady);
if (torrent.done) onDone();
else torrent.on("done", onDone);
});
#end
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var v = inputs[2].get();
if (v == null) return;
#if lnx_torrent
net_Url = connection._url;
var torrentId:Dynamic = switch (property0 != null ? property0 : "magnet") {
case "file": v;
case "id": TorrentClient.torrentIdFor(v);
default: Std.string(v);
};
var opts:Dynamic = null;
var tmode = property1 != null ? property1 : "inherit";
if (tmode != "inherit" && inputs.length > 3) {
var announce = TorrentClient.trackerUrls(inputs[3].get());
opts = {};
if (announce != null) opts.announce = announce;
if (tmode == "override") opts.announceOverride = true;
}
promiseResult(connection, torrentId, net_Url, opts);
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:leenkx.network.torrent.Torrent = torrent;
return switch (from) {
case 1: torrent;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.files : null;
case 5: t != null ? t.infoHashHex : null;
case 6: t != null ? t.magnetURI : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,83 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentFile;
#end
class GetTorrentFileNode extends LogicNode {
public var property0:String;
public var file:Dynamic;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
file = null;
var id = inputs[2].get();
var match = inputs[3].get();
if (id != null && match != null) {
var connection = inputs[1].get();
if (connection != null) net_Url = connection._url;
var socket = net_Url != null ? Leenkx.connections[net_Url] : null;
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t != null) file = findFile(t, match);
}
#end
runOutput(0);
}
#if lnx_torrent
function findFile(t:Torrent, match:Dynamic):TorrentFile {
var files = t.files;
switch (property0) {
case "index":
var i = Std.parseInt(Std.string(match));
if (i != null && i >= 0 && i < files.length) {
return files[i];
}
case "name":
var s = Std.string(match);
for (f in files) {
if (f.name == s) return f;
}
case "path":
var s = Std.string(match);
for (f in files) {
if (f.path == s) return f;
}
default:
var s = Std.string(match);
if (!StringTools.startsWith(s, ".")) s = "." + s;
for (f in files) {
if (StringTools.endsWith(f.name, s)) return f;
}
}
return null;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
var f:TorrentFile = file;
return switch (from) {
case 1: file;
case 2: f != null ? f.name : null;
case 3: f != null ? f.path : null;
case 4: f != null ? f.length : 0;
case 5: f != null ? f.type : null;
case 6: f != null ? f.getBytes() : null;
case 7: f != null;
case 8: f != null ? f.downloaded : 0.0;
case 9: f != null ? f.progress : 0.0;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,8 @@
package leenkx.logicnode;
import leenkx.system.Event;
import leenkx.network.Connect;
import iron.object.Object;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
#end
class LeenkxCloseConnectionNode extends LogicNode {
public var property0: String;
@ -12,41 +12,28 @@ class LeenkxCloseConnectionNode extends LogicNode {
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
if (connection == null) return;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
if(property0 == "client") {
var script = '
for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){
try{
leenkx.network.Leenkx.id.set("' + connection._url + '",p);
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close();
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.connections.h["' + connection._url + '"].onclose();
}catch(error){
console.log("Error: " + error);
}
}
';
js.Syntax.code('(1, eval)({0})', script.toString());
} else {
var script = '
try{
var lnxConn = leenkx.network.Leenkx.connections.h["' + connection._url + '"];
if (lnxConn && lnxConn.client && lnxConn.client.torrent && lnxConn.client.torrent._peers) {
for (var p in lnxConn.client.torrent._peers) {
lnxConn.client.torrent._peers[p].conn._pc.close();
lnxConn.client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.id.set("' + connection._url + '", p);
lnxConn.onclose();
}
}
}catch(error){
console.log("Error: " + error);
}
';
js.Syntax.code('(1, eval)({0})', script.toString());
var lnxCn:LeenkxClient = socket.client;
if (property0 == "peer") {
var id:String = inputs.length > 2 ? inputs[2].get() : null;
if (id != null && lnxCn != null) {
lnxCn.peers.remove(id);
}
runOutput(0);
return;
}
if (lnxCn != null) {
lnxCn.destroy();
socket.client = null;
}
Leenkx.connections.remove(connection._url);
socket.onclose();
#end
runOutput(0);
}
}

View File

@ -1,10 +1,10 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import iron.object.Object;
#end
class LeenkxEventNode extends LogicNode {
public var property0: String;
public var property1: String;
public var value: String;
@ -14,90 +14,63 @@ class LeenkxEventNode extends LogicNode {
public function new(tree:LogicTree) {
super(tree);
tree.notifyOnInit(init);
tree.notifyOnRemove(onRemove);
}
function init() {
if (property0 == "client") {
net_Url = inputs[0].get();
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onseen": value = Leenkx.onSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onserver": value = Leenkx.onServerEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onconnections": value = Leenkx.onConnectionsEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onping": value = Leenkx.onPingEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onleft": value = Leenkx.onLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontimeout": value = Leenkx.onTimeOutEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onrpc": value = Leenkx.onRpcEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onrpcresponse": value = Leenkx.onRpcResponseEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onwireleft": value = Leenkx.onWireLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onwireseen": value = Leenkx.onWireSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontorrent": value = Leenkx.onTorrentEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "ontracker": value = Leenkx.onTrackerEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onannounce": value = Leenkx.onAnnounceEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set client event type.";
}
} else if (property0 == "host") {
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
}
#end
} else if (property0 == "securehost"){
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
}
#end
#if lnx_torrent
var u:Dynamic = inputs[0].get();
net_Url = Std.isOfType(u, String) ? u : (u != null ? u._url : null);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;
case "onmessage": value = Leenkx.onMessageEvent;
case "onerror": value = Leenkx.onErrorEvent;
case "onclose": value = Leenkx.onCloseEvent;
case "onseen": value = Leenkx.onSeenEvent;
case "onserver": value = Leenkx.onServerEvent;
case "onconnections": value = Leenkx.onConnectionsEvent;
case "onping": value = Leenkx.onPingEvent;
case "onleft": value = Leenkx.onLeftEvent;
case "ontimeout": value = Leenkx.onTimeOutEvent;
case "onwireleft": value = Leenkx.onWireLeftEvent;
case "onwireseen": value = Leenkx.onWireSeenEvent;
case "ontorrent": value = Leenkx.onTorrentEvent;
case "ontorrentadded": value = Leenkx.onTorrentAddedEvent;
case "ontracker": value = Leenkx.onTrackerEvent;
case "onannounce": value = Leenkx.onAnnounceEvent;
case "ontorrentdone": value = Leenkx.onTorrentDoneEvent;
case "onload": value = Leenkx.onLoadEvent;
default:
trace("Failed to set client event type");
}
listener = Event.add(value, onEvent, tree.object.uid);
#end
}
function onEvent() {
runOutput(0);
}
override function get(from: Int): Dynamic {
if (property0 == "host") {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
else if (property0 == "securehost") {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
else {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function get(from: Int): Dynamic {
#if lnx_torrent
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,6 @@
package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Buffer;
import haxe.io.Bytes;
import iron.object.Object;
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
@ -22,140 +20,107 @@ class LeenkxMessageParserNode extends LogicNode {
if (data == null) return;
api = inputs[1].get();
if (api == null) api = "";
var buffer:Buffer;
if (Std.isOfType(data, Buffer)) {
buffer = cast data;
} else {
var rawBytes:Bytes;
try {
rawBytes = haxe.crypto.Base64.decode(Std.string(data));
} catch(e) {
trace("Base64 decode failed: " + e);
return;
}
buffer = new Buffer();
buffer.writeBytes(rawBytes);
}
/**
var buffer = cast(data, Buffer);
api = inputs[1].get();
if(buffer.endsWith(api) == false){
return;
}
**/
var buffer = new Buffer();
if(!StringTools.startsWith(data,api)){
return;
}
switch (property0) {
case "string":
parsed = data.substr(api.length, data.length);
runOutput(0);
case "vector":
var vec = data.substr(api.length, data.length);
vec = StringTools.replace(vec, "(", "");
vec = StringTools.replace(vec, ")", "");
vec = StringTools.replace(vec, " ", "");
vec = StringTools.trim(vec);
var array = vec.split(",").map(Std.parseFloat);
parsed = new Vec4( array[0], array[1], array[2], array[3] );
runOutput(0);
case "float":
var flt:String = data.substr(api.length, data.length);
var float = StringTools.trim(flt);
parsed = Std.parseFloat(float);
runOutput(0);
case "integer":
var int:String = data.substr(api.length, data.length);
var integer = StringTools.trim(int);
parsed = Std.parseInt(integer);
runOutput(0);
case "boolean":
var boolean:String = data.substr(api.length, data.length);
if(boolean == "true"){
parsed = true;
} else {
parsed = false;
}
runOutput(0);
case "transform":
var tsfm = data.substr(api.length, data.length);
tsfm = StringTools.replace(tsfm, "[", "");
tsfm = StringTools.replace(tsfm, "]", "");
tsfm = StringTools.replace(tsfm, " ", "");
tsfm = StringTools.trim(tsfm);
var array = tsfm.split(",").map(Std.parseFloat);
parsed = new Mat4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var rot = new Quat();
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
parsed = rot;
runOutput(0);
default: throw "Failed to parse data.";
}
return;
switch (property0) {
case "string":
case "string":
var bytes = buffer.readAllAvailableBytes();
parsed = bytes.toString().substr(0, bytes.length - api.length);
trace(parsed);
runOutput(0);
case "vector":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
case "vector":
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var vec = new Vec4();
for (f in Reflect.fields(vec)) {
Reflect.setField(vec, f, bytes.readFloat());
}
vec.x = bytes.readFloat();
vec.y = bytes.readFloat();
vec.z = bytes.readFloat();
vec.w = bytes.readFloat();
parsed = vec;
runOutput(0);
case "float":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
case "float":
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var float: Float = bytes.readFloat();
parsed = float;
runOutput(0);
case "integer":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
case "integer":
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var integer: Int = bytes.readInt32();
parsed = integer;
runOutput(0);
case "boolean":
case "boolean":
var bytes = buffer.readAllAvailableBytes();
var boolean = bytes.toString().substr(0, bytes.length - api.length);
if(boolean == "true"){
if (boolean == "true") {
parsed = true;
} else {
parsed = false;
}
runOutput(0);
case "transform":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var loc: Vec4 = new Vec4();
var rot: Quat = new Quat();
var scl: Vec4 = new Vec4();
for (f in Reflect.fields(loc)) {
Reflect.setField(loc, f, bytes.readFloat());
try {
loc.x = bytes.readFloat();
loc.y = bytes.readFloat();
loc.z = bytes.readFloat();
loc.w = bytes.readFloat();
rot.x = bytes.readFloat();
rot.y = bytes.readFloat();
rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
scl.x = bytes.readFloat();
scl.y = bytes.readFloat();
scl.z = bytes.readFloat();
scl.w = bytes.readFloat();
} catch(e: Dynamic) {
trace("readFloat error: " + e);
return;
}
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
for (f in Reflect.fields(scl)) {
Reflect.setField(scl, f, bytes.readFloat());
}
var transform = Mat4.identity();
var transform = Mat4.identity();
parsed = transform.compose(loc, rot, scl);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
case "rotation":
var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var rot = new Quat();
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
rot.x = bytes.readFloat();
rot.y = bytes.readFloat();
rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
parsed = rot;
runOutput(0);
default: throw "Failed to parse data.";
}
}
}
override function get(from: Int): Dynamic {
return switch (from) {

View File

@ -1,401 +1,124 @@
package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Leenkx;
import leenkx.network.Buffer;
import haxe.io.Bytes;
import iron.object.Object;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
#end
import iron.math.Vec4;
import iron.math.Quat;
import iron.math.Mat4;
class LeenkxSendMessageNode extends LogicNode {
public var property0: String;
public var property1: String;
public var id: String;
public var all: Bool;
public var property2: Bool;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if js
var connection = inputs[1].get();
if (connection == null) return;
var api: String = inputs[2].get();
if (connection == null) return;
var api: String = inputs[2].get();
var message: Dynamic = inputs[3].get();
if (message == null) return;
var broadcast: Bool = inputs.length > 4 && inputs[4].get() == true;
var targetAddress: String = inputs.length > 5 ? inputs[5].get() : null;
if (!broadcast && (targetAddress == null || targetAddress == "")) {
return;
}
#if lnx_torrent
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var lnxCn:LeenkxClient = socket.client;
if (lnxCn == null) return;
var buffer = new Buffer();
switch (property1) {
case "string":
buffer.writeBytes(Bytes.ofString(message));
case "vector":
var bytesOut = new haxe.io.BytesOutput();
var f:Null<Float>;
if ((f = message.x) != null) bytesOut.writeFloat(f);
if ((f = message.y) != null) bytesOut.writeFloat(f);
if ((f = message.z) != null) bytesOut.writeFloat(f);
if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes());
case "float":
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message);
buffer.writeBytes(bytesOut.getBytes());
case "integer":
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message);
buffer.writeBytes(bytesOut.getBytes());
case "boolean":
if (message == true) {
buffer.writeBytes(Bytes.ofString("true"));
} else {
buffer.writeBytes(Bytes.ofString("false"));
}
case "transform":
var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message;
var loc = new Vec4();
var rot = new Quat();
var scale = new Vec4();
transform.decompose(loc, rot, scale);
bytesOut.writeFloat(loc.x);
bytesOut.writeFloat(loc.y);
bytesOut.writeFloat(loc.z);
bytesOut.writeFloat(loc.w);
bytesOut.writeFloat(rot.x);
bytesOut.writeFloat(rot.y);
bytesOut.writeFloat(rot.z);
bytesOut.writeFloat(rot.w);
bytesOut.writeFloat(scale.x);
bytesOut.writeFloat(scale.y);
bytesOut.writeFloat(scale.z);
bytesOut.writeFloat(scale.w);
buffer.writeBytes(bytesOut.getBytes());
case "rotation":
var bytesOut = new haxe.io.BytesOutput();
var f:Null<Float>;
if ((f = message.x) != null) bytesOut.writeFloat(f);
if ((f = message.y) != null) bytesOut.writeFloat(f);
if ((f = message.z) != null) bytesOut.writeFloat(f);
if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes());
default: trace("Failed to send data.");
}
if (api != null && api != "") {
buffer.writeBytes(Bytes.ofString(api));
}
var rawBytes = buffer.readAllAvailableBytes();
try {
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(`'+ api + message + '`);
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
if (property2) {
var payload = haxe.crypto.Base64.encode(rawBytes);
if (broadcast) {
lnxCn.send(null, payload);
} else {
lnxCn.send(targetAddress, payload);
}
} else {
var script = 'lnx_' + connection._url +'.send(`' + inputs[4].get() + '`, `'+ api + message + '` );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress);
}
} catch(error) {
trace("Error: " + error);
}
return;
runOutput(0);
switch (property1) {
case "string":
if(property0 == "client"){
var buffer = new Buffer();
buffer.writeBytes(Bytes.ofString(message));
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "vector":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message);
for (b in i) {
bytesOut.writeFloat(Reflect.field(message, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
#else
runOutput(0);
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "float":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "integer":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "boolean":
if(property0 == "client"){
var buffer = new Buffer();
if(message == true){
buffer.writeBytes(Bytes.ofString("true"));
} else {
buffer.writeBytes(Bytes.ofString("false"));
}
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "transform":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message;
var loc = new Vec4();
var rot = new Quat();
var scale = new Vec4();
transform.decompose(loc, rot, scale);
var l = Reflect.fields(loc);
for (b in l) {
bytesOut.writeFloat(Reflect.field(loc, b));
}
var r = Reflect.fields(rot);
for (b in r) {
bytesOut.writeFloat(Reflect.field(rot, b));
}
var s = Reflect.fields(scale);
for (b in s) {
bytesOut.writeFloat(Reflect.field(scale, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
//console.log("Mine: " + lx_8001.torrent.discovery.peerId + " || Incomming: " + p);
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
//var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
var script = 'lnx_' + connection._url +'.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
case "rotation":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message);
for (b in i) {
bytesOut.writeFloat(Reflect.field(message, b));
}
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
}
#end
}
}
/*******_________________________________________
try {
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lx_' + connection._url +'.torrent._peers[' + inputs[4].get() + '].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
return;
_________________________________________*****/

View File

@ -23,6 +23,7 @@ class NetworkMessageParserNode extends LogicNode {
var buffer = cast(data, Buffer);
api = inputs[1].get();
if (api == null) api = "";
if(buffer.endsWith(api) == false){
return;
}

View File

@ -7,6 +7,9 @@ class OnActionMarkerNode extends LogicNode {
public var property0: String;
public var property1: String;
var registeredSamplers: Array<iron.object.Animation.ActionSampler> = [];
var registeredCallbacks: Map<iron.object.Animation.ActionSampler, Void->Void> = new Map();
public function new(tree: LogicTree) {
super(tree);
@ -21,9 +24,33 @@ class OnActionMarkerNode extends LogicNode {
assert(Error, object != null, "Object input cannot be null");
var animation = object.animation;
if (animation == null) animation = object.getBoneAnimation(object.uid);
var action = animation.activeActions.get(actionID);
if(action == null) return;
animation.notifyOnMarker(action, marker, function() { runOutput(0); });
tree.removeUpdate(init);
if (animation == null) return;
var active = animation.activeActions;
if (active == null) return;
var i = registeredSamplers.length;
while (i-- > 0) {
var alive = false;
for (sampler in active) if (sampler == registeredSamplers[i]) {
alive = true;
break;
}
if (!alive) {
var s = registeredSamplers[i];
animation.removeMarker(s, marker, registeredCallbacks.get(s));
registeredCallbacks.remove(s);
registeredSamplers.splice(i, 1);
}
}
for (key in active.keys()) {
var sampler = active.get(key);
if (actionID != "" && key != actionID && sampler.action != actionID) continue;
if (registeredSamplers.indexOf(sampler) != -1) continue;
registeredSamplers.push(sampler);
var cb = function() { runOutput(0); };
registeredCallbacks.set(sampler, cb);
animation.notifyOnMarker(sampler, marker, cb);
}
}
}

View File

@ -0,0 +1,65 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class OnTorrentEventNode extends LogicNode {
public var property0:String;
public var eventValue:Dynamic;
var net_Url:String;
var torrent:Dynamic;
var registeredTorrents:Map<String, Bool> = new Map();
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t == null) return;
var event = property0 != null ? property0 : "done";
var self = this;
var fire = function(v:Dynamic) {
self.eventValue = v;
self.runOutput(0);
};
var key = t.infoHashHex != null ? t.infoHashHex : "v2:" + t.infoHashV2Hex;
var alreadyRegistered = registeredTorrents.exists(key);
if (!alreadyRegistered) registeredTorrents.set(key, true);
switch (event) {
case "ready":
if (t.ready) fire(null);
else if (!alreadyRegistered) t.on("ready", function() fire(null));
case "piece" | "wire" | "metadata" | "error":
if (!alreadyRegistered) t.on(event, function(v) fire(v));
default:
if (t.done) fire(null);
else if (!alreadyRegistered) t.on("done", function() fire(null));
}
torrent = t;
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: torrent;
case 2: eventValue;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,73 @@
package leenkx.logicnode;
#if lnx_torrent
import haxe.io.Bytes;
import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.MetaInfo;
#end
class ParseTorrentNode extends LogicNode {
var meta:Dynamic;
var magnet:Dynamic;
var idHex:String;
public var property0:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
meta = null;
magnet = null;
idHex = null;
var src = inputs[1].get();
if (src == null) return;
switch (property0 != null ? property0 : "magnet") {
case "file":
if (Std.isOfType(src, MetaInfo)) {
meta = src;
} else {
var b = Std.isOfType(src, Bytes) ? src : Bytes.ofString(Std.string(src));
try {
meta = MetaInfo.fromTorrentBytes(b);
} catch(e:Dynamic) {
trace(e);
return;
}
}
case "id":
var s = Std.string(src);
idHex = s.length == 64 ? "v2:" + s : s;
default:
var m = Magnet.parse(Std.string(src));
if (m == null) {
return;
}
magnet = m;
idHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var m:MetaInfo = meta;
var mg:Magnet = magnet;
return switch (from) {
case 1: m;
case 2: m != null ? (m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex) : idHex;
case 3: m != null ? m.name : (mg != null ? mg.name : null);
case 4: m != null ? m.announce : (mg != null ? mg.trackers : null);
case 5: m != null ? m.urlList : (mg != null ? mg.webSeeds : null);
case 6: m != null ? m.files : null;
case 7: m != null ? m.totalLength : 0;
case 8: m != null ? m.toTorrentBytes() : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -27,11 +27,11 @@ class PlayAnimationTreeNode extends LogicNode {
}
function playAnim() {
if (init = false) return;
if (init == false) return;
init = false;
tree.removeUpdate(playAnim);
var animation = object.animation;
if(animation == null) {
#if lnx_skin

View File

@ -16,8 +16,10 @@ class RestartActionNode extends LogicNode {
var animation = object.animation;
if (animation == null) animation = object.getBoneAnimation(object.uid);
if(animation.activeActions == null) return;
animation.activeActions.get(action).restartAction();
if (animation == null || animation.activeActions == null) return;
var sampler = animation.activeActions.get(action);
if (sampler == null) return;
sampler.restartAction();
runOutput(0);
}

View File

@ -1,52 +1,124 @@
package leenkx.logicnode;
import leenkx.system.Event;
import iron.object.Object;
#if lnx_torrent
import haxe.io.Bytes;
import leenkx.network.Leenkx;
import leenkx.network.torrent.MetaInfo;
import leenkx.network.torrent.Torrent;
#end
class SeedTorrentNode extends LogicNode {
public var torrent: Dynamic;
public var net_Url: Dynamic;
public var torrent:Dynamic;
public var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
}
override function run(from:Int) {
#if js
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
trace("got connection");
var data = inputs[3].get();
if (data == null) return;
trace("got data");
var opts = inputs[2].get();
if (connection == null) return;
var src = inputs[2].get();
net_Url = connection._url;
var cx = "lx_" + net_Url;
var lx:Dynamic = untyped js.Browser.window[cx];
var args = opts.toString();
trace(args);
var func = Reflect.field(lx.wt, "seed");
if (func != null) {
torrent = func(data, function (torrent) {
trace('Client is seeding ' + torrent.magnetURI);
});
//trace(torrent);
var socket = Leenkx.connections[connection._url];
if (socket == null || socket.client == null) return;
var client = socket.client;
var self = this;
var onSeeded = function(t:Torrent) {
if (t == null) return;
self.torrent = t;
var onDone = function() {
self.runOutput(2);
};
if (t.done) onDone();
else t.on("done", onDone);
};
if (Std.isOfType(src, Torrent)) {
var t:Torrent = src;
t.start();
torrent = t;
runOutput(0);
return;
}
var meta:MetaInfo = null;
if (Std.isOfType(src, MetaInfo)) {
meta = cast src;
} else if (Std.isOfType(src, Bytes)) {
try {
meta = MetaInfo.fromTorrentBytes(cast src);
} catch(e:Dynamic) {
return;
}
}
if (meta == null) {
return;
}
var files = filesFor(meta, inputs.length > 3 ? inputs[3].get() : null);
if (files == null) {
return;
}
client.seedMeta(meta, files, null, onSeeded);
runOutput(0);
#end
}
override function get(from: Int): Dynamic {
return switch (from) {
case 1: torrent;
default: throw "Unreachable";
}
}
#if lnx_torrent
function filesFor(meta:MetaInfo, data:Dynamic):Array<{path:String, data:Bytes}> {
if (data == null) return null;
var entries = meta.files;
if (Std.isOfType(data, Bytes)) {
var blob:Bytes = data;
if (blob.length != meta.totalLength) return null;
var files:Array<{path:String, data:Bytes}> = [];
var cursor = 0;
for (f in entries) {
files.push({path: f.path, data: blob.sub(cursor, f.length)});
cursor += f.length;
}
return files;
}
if (Std.isOfType(data, Array)) {
var items:Array<Dynamic> = data;
var files:Array<{path:String, data:Bytes}> = [];
for (it in items) {
var p = Reflect.field(it, "path");
var d:Bytes = Reflect.field(it, "data");
if (d == null) d = Reflect.field(it, "content");
if (p == null || d == null) continue;
var sp = Std.string(p);
var matched = false;
for (f in entries) {
if (f.path == sp || StringTools.endsWith(f.path, "/" + sp)) {
files.push({path: f.path, data: d});
matched = true;
break;
}
}
if (!matched) files.push({path: sp, data: d});
}
if (files.length == entries.length) return files;
return null;
}
return null;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) {
case 1: torrent;
case 3: t != null ? t.infoHashHex : null;
case 4: t != null ? t.magnetURI : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -19,8 +19,10 @@ class SetActionPausedNode extends LogicNode {
var animation = object.animation;
if (animation == null) animation = object.getBoneAnimation(object.uid);
if(animation.activeActions == null) return;
animation.activeActions.get(action).paused = paused;
if (animation == null || animation.activeActions == null) return;
var sampler = animation.activeActions.get(action);
if (sampler == null) return;
sampler.paused = paused;
runOutput(0);
}

View File

@ -0,0 +1,90 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class SetTrackersNode extends LogicNode {
public var property0:String;
public var target:Dynamic;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var p = property0 != null ? property0 : "add";
switch (p) {
case "add" | "override":
setTorrentTrackers(p == "override");
default:
setTrackerProperty(p);
}
#end
runOutput(0);
}
#if lnx_torrent
function setTorrentTrackers(trackerOverride:Bool):Void {
var id = inputs[2].get();
var urls = TorrentClient.trackerUrls(inputs[3].get());
if (urls == null) return;
var t:Torrent = Std.isOfType(id, Torrent) ? id : null;
if (t == null && id != null) {
var connection = inputs[1].get();
var socket = connection != null ? Leenkx.connections[connection._url] : null;
var client = socket != null ? socket.client : null;
if (client != null) t = client.get(id);
}
if (t != null) {
target = t;
if (trackerOverride) {
t.setTrackers(urls);
} else {
for (u in urls) t.addTracker(u);
}
} else if (id == null) {
target = null;
if (trackerOverride) {
TorrentClient.defaultTrackers = urls;
} else {
TorrentClient.mergeTrackers(TorrentClient.defaultTrackers, urls);
}
}
}
function setTrackerProperty(prop:String):Void {
var tracker:Dynamic = inputs[1].get();
if (tracker == null) return;
var v = inputs[2].get();
if (v == null) return;
var field = prop == "interval" ? "intervalSec" : prop;
var val:Dynamic = field == "trackerId" ? Std.string(v)
: Std.int(Std.parseFloat(Std.string(v)));
var setter:Dynamic = field == "intervalSec"
&& Reflect.field(tracker, "intervalSec") == null
? Reflect.field(tracker, "setAnnounceInterval") : null;
if (setter != null) {
try {
Reflect.callMethod(tracker, setter, [(val:Int) * 1000]);
} catch(e:Dynamic) {}
} else {
Reflect.setField(tracker, field, val);
}
target = tracker;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: target;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentClientInfoNode extends LogicNode {
var stats:Dynamic;
var list:Array<Dynamic>;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
list = [];
stats = {downRate: 0.0, upRate: 0.0, progress: 0.0, ratio: 0.0};
var connection = inputs[1].get();
if (connection != null) {
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
if (client != null) collect(client);
}
#end
runOutput(0);
}
#if lnx_torrent
function collect(client:Dynamic) {
var down = 0.0;
var up = 0.0;
var received = 0.0;
var sent = 0.0;
var total = 0.0;
var left = 0.0;
var torrents:Map<String, Torrent> = client.torrents;
for (t in torrents) {
list.push(t);
var s = t.getStats();
down += s.downRate;
up += s.upRate;
received += s.downloaded;
sent += s.uploaded;
if (t.meta != null) {
total += t.meta.totalLength;
left += s.left;
}
}
stats = {
downRate: down,
upRate: up,
progress: total > 0 ? 1.0 - left / total : 0.0,
ratio: received > 0 ? sent / received : 0.0
};
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: list;
case 2: list.length;
case 3: stats.downRate;
case 4: stats.upRate;
case 5: stats.progress;
case 6: stats.ratio;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,93 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentControlNode extends LogicNode {
public var property0:String;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var client = socket.client;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
var action = property0 != null ? property0 : "remove";
var file:Dynamic = inputs.length > 3 ? inputs[3].get() : null;
var t2 = t;
var startB = 0;
var endB = -1;
if (file != null) {
startB = file.offset;
endB = file.offset + file.length - 1;
if (t2 == null) t2 = file.torrent;
} else {
var sv = inputs.length > 4 ? inputs[4].get() : null;
var ev = inputs.length > 5 ? inputs[5].get() : null;
if (sv != null && Std.string(sv) != "") {
startB = Std.int(Std.parseFloat(Std.string(sv)));
}
if (ev != null && Std.string(ev) != "") {
endB = Std.int(Std.parseFloat(Std.string(ev)));
}
}
var pv = inputs.length > 6 ? inputs[6].get() : null;
var priority = pv != null && Std.string(pv) != "" ? Std.int(Std.parseFloat(Std.string(pv))) : null;
switch (action) {
case "start":
if (t != null) t.start();
case "stop":
if (t != null) t.stop();
case "select", "critical", "deselect":
if (t2 != null) {
if (endB < 0) {
endB = t2.meta != null ? t2.meta.totalLength - 1 : 0;
}
if (action == "critical") {
t2.critical(startB, endB);
} else if (action == "select") {
t2.select(startB, endB, priority);
} else {
t2.deselect(startB, endB, priority);
}
}
case "add-peer":
var peer = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && peer != null) {
t.addPeer(Std.string(peer));
}
case "add-web-seed":
var url = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && url != null) {
t.addWebSeed(Std.string(url));
}
case "remove", "remove-data":
var hex = t != null ? t.infoHashHex : Std.string(id);
if (hex == null) hex = Std.string(id);
if (client != null) {
client.remove(hex, action == "remove-data");
}
if (t != null && Leenkx.torrent.get(net_Url) == t) {
Leenkx.torrent.remove(net_Url);
Leenkx.file.remove(net_Url);
}
default:
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class TorrentInfoNode extends LogicNode {
public var torrent:Dynamic;
public var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var id = inputs[2].get();
if (id == null) return;
torrent = null;
if (Std.isOfType(id, Torrent)) {
torrent = id;
} else {
var connection = inputs[1].get();
if (connection == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[net_Url];
if (socket == null || socket.client == null) return;
torrent = socket.client.get(id);
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) {
case 1: t != null ? t.infoHashHex : null;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.numPeers : 0;
case 4: progress(t);
case 5: t != null ? t.getStats().downloaded : 0.0;
case 6: t != null ? t.getStats().uploaded : 0.0;
case 7: t != null ? t.done : false;
case 8: t != null ? t.magnetURI : null;
case 9: t != null ? t.files : null;
case 10: t != null && t.meta != null ? t.meta.numPieces : 0;
case 11: t != null && t.meta != null ? t.meta.pieceLength : 0;
case 12: t != null && t.meta != null ? t.meta.totalLength : 0;
case 13: t != null && t.meta != null ? t.meta.comment : null;
case 14: t != null && t.meta != null ? t.meta.createdBy : null;
case 15: t != null && t.meta != null ? t.meta.protocolVersion : 1;
case 16: t != null ? t.infoHashV2Hex : null;
case 17: t != null ? t.getStats().downRate : 0.0;
case 18: t != null ? t.getStats().upRate : 0.0;
case 19: t != null ? t.stopped : false;
case 20: t;
default: throw "Unreachable";
}
#else
return null;
#end
}
#if lnx_torrent
function progress(t:Torrent):Float {
if (t == null || t.meta == null || t.meta.totalLength == 0) return 0;
var stats = t.getStats();
return 1.0 - stats.left / t.meta.totalLength;
}
#end
}

View File

@ -63,10 +63,10 @@ class Buffer {
public function readUntil(delimiter:String):Bytes {
var dl = delimiter.length;
for (i in 0 ... available - dl) {
for (i in 0 ... available - dl + 1) {
var matched = true;
for (j in 0 ... dl) {
if (peekByte(currentOffset + i + j + 1) == delimiter.charCodeAt(j)) {
if (peekByte(i + j) == delimiter.charCodeAt(j)) {
continue;
}
matched = false;
@ -74,7 +74,8 @@ class Buffer {
}
if (matched) {
var bytes = readBytes(i + dl + 1);
var bytes = readBytes(i);
readBytes(dl);
return bytes;
}
}
@ -85,7 +86,19 @@ class Buffer {
public function readBytes(count:Int):Bytes {
var count2 = Std.int(Math.min(count, available));
var out = Bytes.alloc(count2);
for (n in 0 ... count2) out.set(n, readByte());
var written = 0;
while (written < count2) {
if (currentData == null || currentOffset >= currentData.length) {
currentOffset = 0;
currentData = chunks.shift();
}
var n = Std.int(Math.min(count2 - written, currentData.length - currentOffset));
out.blit(written, currentData, currentOffset, n);
written += n;
currentOffset += n;
available -= n;
}
length = available;
return out;
}
@ -115,16 +128,23 @@ class Buffer {
}
public function peekByte(offset:Int):Int {
if (available <= 0) throw 'No bytes available';
var tempOffset = offset;
var tempData = chunks[0];
if (tempData == null) {
tempData = currentData;
if (available <= 0 || offset < 0 || offset >= available) throw 'No bytes available';
var tempData = currentData;
var chunkIndex = -1;
var tempOffset:Int;
if (tempData != null && currentOffset < tempData.length) {
tempOffset = currentOffset + offset;
} else {
tempData = chunks[0];
chunkIndex = 0;
tempOffset = offset;
}
var chunkIndex = 0;
while (tempOffset >= tempData.length) {
tempOffset -= tempData.length;
chunkIndex++;
if (chunkIndex >= chunks.length){
trace('No bytes available');
}
tempData = chunks[chunkIndex];
}
return tempData.get(tempOffset);
@ -154,9 +174,9 @@ class Buffer {
public function endsWith(e:String):Bool {
var i = available - e.length;
var n = currentOffset;
var n = 0;
if (i <= 0) {
if (i < 0) {
return false;
}

View File

@ -1,6 +1,8 @@
package leenkx.network;
class Handler extends WebSocketCommon {
public var validateHandshake:(HttpRequest, HttpResponse, (HttpResponse) -> Void) -> Void = null;
public function new(socket:SocketImpl) {
super(socket);
isClient = false;

View File

@ -4,6 +4,7 @@ class HttpHeader {
public static inline var SEC_WEBSOCKET_KEY:String = "Sec-WebSocket-Key";
public static inline var SEC_WEBSOSCKET_ACCEPT:String = "Sec-WebSocket-Accept";
public static inline var SEC_WEBSOSCKET_VERSION:String = "Sec-WebSocket-Version";
public static inline var SEC_WEBSOCKET_PROTOCOL:String = "Sec-WebSocket-Protocol";
public static inline var UPGRADE:String = "Upgrade";
public static inline var HOST:String = "Host";
public static inline var CONNECTION:String = "Connection";

View File

@ -1,10 +1,12 @@
package leenkx.network;
#if lnx_torrent
import leenkx.network.Types;
import haxe.io.Bytes;
import iron.object.Object;
import leenkx.system.Event;
import leenkx.network.Buffer;
import leenkx.network.torrent.LeenkxClient;
@:expose
class Leenkx {
@ -20,24 +22,20 @@ class Leenkx {
public static var onPingEvent: String = "Leenkx.onPing";
public static var onLeftEvent: String = "Leenkx.onLeft";
public static var onTimeOutEvent: String = "Leenkx.onTimeOut";
public static var onRpcEvent: String = "Leenkx.onRpc";
public static var onRpcResponseEvent: String = "Leenkx.onRpcResponse";
public static var onWireLeftEvent: String = "Leenkx.onWireLeft";
public static var onWireSeenEvent: String = "Leenkx.onWireSeen";
public static var onTorrentEvent: String = "Leenkx.onTorrent";
public static var onTorrentAddedEvent: String = "Leenkx.onTorrentAdded";
public static var onTrackerEvent: String = "Leenkx.onTracker";
public static var onAnnounceEvent: String = "Leenkx.onAnnounce";
public static var onTorrentDoneEvent: String = "Leenkx.onTorrentDone";
public static var connections:Map<String, leenkx.network.LeenkxSocket> = [];
#if js
public static var peers:js.lib.Map<String, String> = new js.lib.Map<String,String>();
public static var data:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
public static var id:js.lib.Map<String, String> = new js.lib.Map<String,String>();
public static var torrent:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
public static var file:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
#end
public static var lxNew:Void->Void;
public static var peers:Map<String, String> = [];
public static var data:Map<String, Dynamic> = [];
public static var id:Map<String, String> = [];
public static var torrent:Map<String, Dynamic> = [];
public static var file:Map<String, Dynamic> = [];
public function new(net_Url: String, net_object: Object) {
if (net_Url != null && net_object != null) {
@ -54,224 +52,65 @@ class Leenkx {
}
if (object != null) {
final loadEvent = Event.get(Leenkx.onLoadEvent);
final openEvent = Event.get(Leenkx.onOpenEvent);
final messageEvent = Event.get(Leenkx.onMessageEvent);
final errorEvent = Event.get(Leenkx.onErrorEvent);
final closeEvent = Event.get(Leenkx.onCloseEvent);
final seenEvent = Event.get(Leenkx.onSeenEvent);
final serverEvent = Event.get(Leenkx.onServerEvent);
final connectionsEvent = Event.get(Leenkx.onConnectionsEvent);
final pingEvent = Event.get(Leenkx.onPingEvent);
final leftEvent = Event.get(Leenkx.onLeftEvent);
final timeoutEvent = Event.get(Leenkx.onTimeOutEvent);
final rpcEvent = Event.get(Leenkx.onRpcEvent);
final rpcresponseEvent = Event.get(Leenkx.onRpcResponseEvent);
final wireleftEvent = Event.get(Leenkx.onWireLeftEvent);
final wireseenEvent = Event.get(Leenkx.onWireSeenEvent);
final torrentEvent = Event.get(Leenkx.onTorrentEvent);
final trackerEvent = Event.get(Leenkx.onTrackerEvent);
final announceEvent = Event.get(Leenkx.onAnnounceEvent);
final torrentDoneEvent = Event.get(Leenkx.onTorrentDoneEvent);
var uid = object.uid;
Leenkx.connections[net_Url].onopen = function() {
if (openEvent != null) {
for (e in openEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onOpenEvent, uid);
};
Leenkx.connections[net_Url].onmessage = function() {
if (messageEvent != null) {
for (e in messageEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onMessageEvent, uid);
};
Leenkx.connections[net_Url].onerror = function() {
if (errorEvent != null) {
for (e in errorEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onErrorEvent, uid);
};
Leenkx.connections[net_Url].onclose = function() {
if (closeEvent != null) {
for (e in closeEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onCloseEvent, uid);
};
Leenkx.connections[net_Url].onseen = function() {
if (seenEvent != null) {
for (e in seenEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onSeenEvent, uid);
};
Leenkx.connections[net_Url].onserver = function() {
if (serverEvent != null) {
for (e in serverEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onServerEvent, uid);
};
Leenkx.connections[net_Url].onconnections = function() {
if (connectionsEvent != null) {
for (e in connectionsEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onConnectionsEvent, uid);
};
Leenkx.connections[net_Url].onping = function() {
if (pingEvent != null) {
for (e in pingEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onPingEvent, uid);
};
Leenkx.connections[net_Url].onleft = function() {
if (leftEvent != null) {
for (e in leftEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onLeftEvent, uid);
};
Leenkx.connections[net_Url].ontimeout = function() {
if (timeoutEvent != null) {
for (e in timeoutEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Leenkx.connections[net_Url].onrpc = function() {
if (rpcEvent != null) {
for (e in rpcEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Leenkx.connections[net_Url].onrpcresponse = function() {
if (rpcresponseEvent != null) {
for (e in rpcresponseEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onTimeOutEvent, uid);
};
Leenkx.connections[net_Url].onwireleft = function() {
if (wireleftEvent != null) {
for (e in wireleftEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onWireLeftEvent, uid);
};
Leenkx.connections[net_Url].onwireseen = function() {
if (wireseenEvent != null) {
for (e in wireseenEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onWireSeenEvent, uid);
};
Leenkx.connections[net_Url].ontorrent = function() {
if (torrentEvent != null) {
for (e in torrentEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Event.send(Leenkx.onTorrentEvent, uid);
};
Leenkx.connections[net_Url].ontorrentadded = function() {
Event.send(Leenkx.onTorrentAddedEvent, uid);
};
Leenkx.connections[net_Url].ontracker = function() {
if (trackerEvent != null) {
for (e in trackerEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Event.send(Leenkx.onTrackerEvent, uid);
};
Leenkx.connections[net_Url].onannounce = function() {
if (announceEvent != null) {
for (e in announceEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onAnnounceEvent, uid);
};
Leenkx.connections[net_Url].onload = function() {
if (loadEvent != null) {
for (e in loadEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Event.send(Leenkx.onLoadEvent, uid);
};
Leenkx.connections[net_Url].ontorrentdone = function() {
if (torrentDoneEvent != null) {
for (e in torrentDoneEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
Event.send(Leenkx.onTorrentDoneEvent, uid);
};
#if js
var lnxjs:Dynamic = js.Lib.global;
var connectionUrl = net_Url;
if (lnxjs.Leenkx != null) {
if (lnxjs.lnxNew == null) {
js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }');
}
Leenkx.connections[connectionUrl].onload();
} else {
kha.Assets.loadBlobFromPath("Leenkx.js", function(b: kha.Blob) {
if (b != null) {
js.Syntax.code("(1,eval)({0})", b.toString());
if (lnxjs.Leenkx != null && lnxjs.lnxNew == null) {
js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }');
}
} else {
trace("Warning: Leenkx.js blob is null - file may not be in assets");
}
Leenkx.connections[connectionUrl].onload();
}, function(err: kha.AssetError) {
trace("ERROR loading Leenkx.js: " + err.url + " - " + err.error);
Leenkx.connections[connectionUrl].onload();
});
}
#end
Leenkx.connections[connectionUrl].onload();
}
}
}
@ -281,11 +120,11 @@ class Leenkx {
class LeenkxSocket {
public var _url:String;
public var client:haxe.DynamicAccess<Dynamic>;
public var client:LeenkxClient;
public var buffer:Dynamic;
public var onopen:Void->Void;
public var onclose:Void->Void;
public var onmessage:Void->Void;
public var onmessage:Void->Void;
public var onerror:Void->Void;
public var onseen:Void->Void;
public var onserver:Void->Void;
@ -293,11 +132,10 @@ class LeenkxSocket {
public var onping:Void->Void;
public var onleft:Void->Void;
public var ontimeout:Void->Void;
public var onrpc:Void->Void;
public var onrpcresponse:Void->Void;
public var onwireleft:Void->Void;
public var onwireseen:Void->Void;
public var ontorrent:Void->Void;
public var ontorrentadded:Void->Void;
public var ontracker:Void->Void;
public var onannounce:Void->Void;
public var onload:Void->Void;
@ -308,3 +146,4 @@ class LeenkxSocket {
}
}
#end

View File

@ -1,51 +0,0 @@
package leenkx.network;
class Log {
public static inline var INFO:Int = 0x000001;
public static inline var DEBUG:Int = 0x000010;
public static inline var DATA:Int = 0x000100;
public static var mask:Int = 0;
#if (sys || kha_krom)
public static var logFn:Dynamic->Void = function(data:Dynamic) { trace(data); };
#elseif js
public static var logFn:Dynamic->Void = js.html.Console.log;
#end
public static function info(data:String, id:String = null) {
if (mask & INFO != INFO) {
return;
}
if (id != null) {
logFn('INFO :: ID-${id} :: ${data}');
} else {
logFn('INFO :: ${data}');
}
}
public static function debug(data:String, id:String = null) {
if (mask & DEBUG != DEBUG) {
return;
}
if (id != null) {
logFn('DEBUG :: ID-${id} :: ${data}');
} else {
logFn('DEBUG :: ${data}');
}
}
public static function data(data:String, id:String = null) {
if (mask & DATA != DATA) {
return;
}
if (id != null) {
logFn('DATA :: ID-${id}\n------------------------------\n${data}\n------------------------------');
} else {
logFn('${data}');
}
}
}

View File

@ -136,18 +136,43 @@ import haxe.io.Bytes;
class WebSocket {
private var _url:String;
private var _protocols:Array<String> = null;
private var _ws:js.html.WebSocket = null;
public function new(url:String, immediateOpen=true) {
public var state(get, null): State;
function get_state() {
if (_ws == null) return Closed;
switch (_ws.readyState) {
case js.html.WebSocket.CONNECTING:
return Handshake;
case js.html.WebSocket.OPEN:
return Body;
}
return Closed;
}
public var protocol(get, null): String;
function get_protocol() {
if (_ws == null)
return null;
return _ws.protocol;
}
public function new(url:String, immediateOpen=true, protocols:Array<String> = null) {
_url = url;
_protocols = protocols;
if (immediateOpen) {
open();
}
}
private function createSocket() {
return new js.html.WebSocket(_url);
if (_protocols == null) {
return new js.html.WebSocket(_url);
}
return new js.html.WebSocket(_url, _protocols);
}
public function open() {
@ -292,6 +317,10 @@ class WebSocket extends WebSocketCommon {
public var _host:String;
public var _port:Int = 0;
public var _path:String;
public var _search:String;
private var _protocols:Array<String>;
public var protocol(default, null):String = null;
private var _processThread:Thread;
private var _encodedKey:String = "wskey";
@ -300,8 +329,9 @@ class WebSocket extends WebSocketCommon {
public var additionalHeaders(get, null):Map<String, String>;
public function new(url:String, immediateOpen=true) {
public function new(url:String, immediateOpen=true, protocols:Array<String> = null) {
parseUrl(url);
_protocols = protocols;
super(createSocket());
@ -310,40 +340,42 @@ class WebSocket extends WebSocketCommon {
}
}
inline private function parseUrl(url)
inline private function parseUrl(url:String)
{
/** TO DO - FIND OUT WHAT IS BREAKING REGEX IN THE NEW PCRE2 FOR HL C
var urlRegExp = ~/^(\w+?):\/\/([\w\.-]+)(:(\d+))?(\/.*)?$/;
if ( ! urlRegExp.match(url)) {
var sep = url.indexOf("://");
if (sep < 0) {
throw 'Uri not matching websocket URL "${url}"';
}
_protocol = url.substr(0, sep);
var rest = url.substr(sep + 3);
_protocol = urlRegExp.matched(1);
var pathStart = rest.length;
var slashIdx = rest.indexOf("/");
var qIdx = rest.indexOf("?");
if (slashIdx >= 0 && slashIdx < pathStart) pathStart = slashIdx;
if (qIdx >= 0 && qIdx < pathStart) pathStart = qIdx;
_host = urlRegExp.matched(2);
var hostPort = rest.substr(0, pathStart);
var pathAndSearch = rest.substr(pathStart);
var parsedPort = Std.parseInt(urlRegExp.matched(4));
if (parsedPort > 0 ) {
_port = parsedPort;
var colonIdx = hostPort.indexOf(":");
if (colonIdx >= 0) {
_host = hostPort.substr(0, colonIdx);
var parsedPort = Std.parseInt(hostPort.substr(colonIdx + 1));
if (parsedPort > 0) {
_port = parsedPort;
}
} else {
_host = hostPort;
}
_path = urlRegExp.matched(5);
if (_path == null || _path.length == 0) {
_path = "/";
_path = pathAndSearch;
_search = null;
var searchIdx = pathAndSearch.indexOf("?");
if (searchIdx >= 0) {
_path = pathAndSearch.substr(0, searchIdx);
_search = pathAndSearch.substr(searchIdx);
}
**/
var urlArr = url.split(":");
if ( urlArr.length < 3) {
throw 'Uri not matching websocket URL "${url}"';
}
_protocol = urlArr[0];
_host = urlArr[1].substr(2, urlArr[1].length);
var parsedPort = Std.parseInt(urlArr[2].split("/")[0]);
if (parsedPort > 0 ) {
_port = parsedPort;
}
_path = urlArr[2].substr(urlArr[2].split("/")[0].length, urlArr[2].length);
if (_path == null || _path.length == 0) {
_path = "/";
}
@ -388,9 +420,7 @@ class WebSocket extends WebSocketCommon {
#else
haxe.MainLoop.addThread(function() {
Log.debug("Thread started", this.id);
processLoop(this);
Log.debug("Thread ended", this.id);
});
#end
@ -399,16 +429,13 @@ class WebSocket extends WebSocketCommon {
}
private function processThread() {
Log.debug("Thread started", this.id);
var ws:WebSocket = Thread.readMessage(true);
processLoop(this);
Log.debug("Thread ended", this.id);
}
private function processLoop(ws:WebSocket) {
while (ws.state != State.Closed) { // TODO: should think about mutex
ws.process();
Sys.sleep(.01);
}
}
@ -422,8 +449,7 @@ class WebSocket extends WebSocketCommon {
public function sendHandshake() {
var httpRequest = new HttpRequest();
httpRequest.method = "GET";
// TODO: should propably be hostname+port+path?
httpRequest.uri = _path;
httpRequest.uri = (_search != null) ? _path + _search : _path;
httpRequest.httpVersion = "HTTP/1.1";
httpRequest.headers.set(HttpHeader.HOST, _host + ":" + _port);
@ -435,6 +461,9 @@ class WebSocket extends WebSocketCommon {
httpRequest.headers.set(HttpHeader.CACHE_CONTROL, "no-cache");
httpRequest.headers.set(HttpHeader.ORIGIN, _socket.host().host.toString() + ":" + _socket.host().port);
if (_protocols != null) {
httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_PROTOCOL, _protocols.join(', '));
}
_encodedKey = generateWSKey();
httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_KEY, _encodedKey);
@ -487,16 +516,18 @@ class WebSocket extends WebSocketCommon {
}
}
var protocol = httpResponse.headers.get(HttpHeader.SEC_WEBSOCKET_PROTOCOL);
if (protocol != null) {
this.protocol = protocol;
}
_onopenCalled = false;
state = State.Head;
}
private function generateWSKey():String {
var b = Bytes.alloc(16);
for (i in 0...16) {
b.set(i, Std.random(255));
}
return Base64.encode(b);
return haxe.crypto.Base64.encode(
leenkx.network.torrent.Crypto.randomBytes(16));
}
}

View File

@ -38,7 +38,6 @@ class WebSocketCommon {
public function send(data:Any) {
if (Std.isOfType(data, String)) {
Log.data(data, id);
sendFrame(Utf8Encoder.encode(data), OpCode.Text);
} else if (Std.isOfType(data, Bytes)) {
sendFrame(data, OpCode.Binary);
@ -118,7 +117,6 @@ class WebSocketCommon {
}
} else {
var stringPayload = Utf8Encoder.decode(unmaskedMessageData);
Log.data(stringPayload, id);
if (this.onmessage != null) {
this.onmessage(StrMessage(stringPayload));
}
@ -138,14 +136,12 @@ class WebSocketCommon {
case State.Closed:
close();
case _:
trace('State not impl: ${state}');
}
}
public function close() {
if (state != State.Closed) {
try {
Log.debug("Closed", id);
sendFrame(Bytes.alloc(0), OpCode.Close);
state = State.Closed;
_socket.close();
@ -162,7 +158,6 @@ class WebSocketCommon {
_socket.output.write(data);
_socket.output.flush();
} catch (e:Dynamic) {
Log.debug(Std.string(e), id);
if (onerror != null) {
onerror(Std.string(e));
}
@ -195,17 +190,24 @@ class WebSocketCommon {
}
private static function generateMask() {
var maskData = Bytes.alloc(4);
maskData.set(0, Std.random(256));
maskData.set(1, Std.random(256));
maskData.set(2, Std.random(256));
maskData.set(3, Std.random(256));
return maskData;
return leenkx.network.torrent.Crypto.randomBytes(4);
}
private static function applyMask(payload:Bytes, mask:Bytes) {
var maskedPayload = Bytes.alloc(payload.length);
for (n in 0 ... payload.length) maskedPayload.set(n, payload.get(n) ^ mask.get(n % mask.length));
var m0 = mask.get(0), m1 = mask.get(1), m2 = mask.get(2), m3 = mask.get(3);
var i = 0;
while (i + 3 < payload.length) {
maskedPayload.set(i, payload.get(i) ^ m0);
maskedPayload.set(i + 1, payload.get(i + 1) ^ m1);
maskedPayload.set(i + 2, payload.get(i + 2) ^ m2);
maskedPayload.set(i + 3, payload.get(i + 3) ^ m3);
i += 4;
}
while (i < payload.length) {
maskedPayload.set(i, payload.get(i) ^ mask.get(i & 3));
i++;
}
return maskedPayload;
}
@ -230,7 +232,6 @@ class WebSocketCommon {
try {
result = SocketImpl.select([_socket], null, null, 0.01);
} catch (e:Dynamic) {
Log.debug("Error selecting socket: " + e);
needClose = true;
}
@ -238,12 +239,11 @@ class WebSocketCommon {
if (result.read.length > 0) {
try {
while (true) {
var data = Bytes.alloc(1024);
var data = Bytes.alloc(65536);
var read = _socket.input.readBytes(data, 0, data.length);
if (read <= 0){
break;
}
Log.debug("Bytes read: " + read, id);
_buffer.writeBytes(data.sub(0, read));
}
} catch (e:Dynamic) {
@ -259,7 +259,7 @@ class WebSocketCommon {
}
#else
needClose = !(e == 'Blocking' || (Std.isOfType(e, Error) && (e:Error).match(Error.Blocked)));
needClose = !(e == 'Blocking' || (Std.isOfType(e, Error) && (e:Error).match(Error.Blocked)) || (state == Handshake && e == haxe.io.Eof));
#end
}
@ -273,7 +273,6 @@ class WebSocketCommon {
if (needClose == true) { // dont want to send the Close frame here
if (state != State.Closed) {
try {
Log.debug("Closed", id);
state = State.Closed;
_socket.close();
} catch (e:Dynamic) { }
@ -288,8 +287,6 @@ class WebSocketCommon {
public function sendHttpRequest(httpRequest:HttpRequest) {
var data = httpRequest.build();
Log.data(data, id);
try {
_socket.output.write(Bytes.ofString(data));
_socket.output.flush();
@ -304,8 +301,6 @@ class WebSocketCommon {
public function sendHttpResponse(httpResponse:HttpResponse) {
var data = httpResponse.build();
Log.data(data, id);
_socket.output.write(Bytes.ofString(data));
_socket.output.flush();
}
@ -325,8 +320,6 @@ class WebSocketCommon {
httpRequest.addLine(line);
}
Log.data(httpRequest.toString(), id);
return httpRequest;
}
@ -346,8 +339,6 @@ class WebSocketCommon {
}
Log.data(httpResponse.toString(), id);
return httpResponse;
}

View File

@ -13,7 +13,6 @@ class WebSocketHandler extends Handler {
_creationTime = Sys.time();
#end
_socket.setBlocking(false);
Log.debug('New socket handler', id);
}
public override function handle() {
@ -23,7 +22,6 @@ class WebSocketHandler extends Handler {
var currentTime = Sys.time();
#end
if (this.state == State.Handshake && currentTime - _creationTime > (MAX_WAIT_TIME / 1000)) {
Log.info('No handshake detected in ${MAX_WAIT_TIME}ms, closing connection', id);
this.close();
return;
}
@ -70,10 +68,8 @@ class WebSocketHandler extends Handler {
httpResponse.headers.set(HttpHeader.CONNECTION, "close");
httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported connection header: ${httpRequest.headers.get(HttpHeader.CONNECTION)}.');
} else {
Log.debug('Handshaking', id);
var key = httpRequest.headers.get(HttpHeader.SEC_WEBSOCKET_KEY);
var result = makeWSKeyResponse(key);
Log.debug('Handshaking key - ${result}', id);
httpResponse.code = 101;
httpResponse.text = "Switching Protocols";
@ -82,14 +78,21 @@ class WebSocketHandler extends Handler {
httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_ACCEPT, result);
}
sendHttpResponse(httpResponse);
function callback(httpResponse: HttpResponse) {
sendHttpResponse(httpResponse);
if (httpResponse.code == 101) {
_onopenCalled = false;
state = State.Head;
Log.debug('Connected', id);
if (httpResponse.code == 101) {
_onopenCalled = false;
state = State.Head;
} else {
close();
}
}
if (validateHandshake != null) {
validateHandshake(httpRequest, httpResponse, callback);
} else {
close();
callback(httpResponse);
}
}
}

View File

@ -60,7 +60,6 @@ class WebSocketServer
_serverSocket.bind(new sys.net.Host(_host), _port);
#end
_serverSocket.listen(_maxConnections);
Log.info('Starting server - ${_host}:${_port} (maxConnections: ${_maxConnections})');
#if kha_krom
kha.Scheduler.addTimeTask(function() {
@ -114,7 +113,6 @@ class WebSocketServer
var handler = new T(socket);
handlers.push(handler);
Log.debug("Adding to web server handler to list - total: " + handlers.length, handler.id);
if (onClientAdded != null) {
onClientAdded(handler);
}
@ -174,7 +172,6 @@ class WebSocketServer
for (h in toRemove) {
handlers.remove(h);
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
if (onClientRemoved != null) {
onClientRemoved(h);
}
@ -186,7 +183,6 @@ class WebSocketServer
while (_handlersClosed.length > 0) {
var h = _handlersClosed.shift();
handlers.remove(h);
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
if (onClientRemoved != null) {
onClientRemoved(h);
}

View File

@ -52,8 +52,15 @@ class KromSecureSocket extends KromSocket {
}
override public function connect(host:KromHost, port:Int):Void {
super.connect(host, port);
enableSSL();
setBlocking(true);
try {
super.connect(host, port);
enableSSL();
setBlocking(false);
} catch (e:Dynamic) {
setBlocking(false);
trace('Failed Krom Host');
}
}
public function isSSLEnabled():Bool {

View File

@ -43,6 +43,9 @@ class KromSocket {
}
private function setSocketId(id:Int):Void {
if (_socketId >= 0) {
krom_socket_close(_socketId);
}
_socketId = id;
if (_socketId >= 0) {
input = new KromSocketInput(this);
@ -247,3 +250,29 @@ class KromHost {
return host;
}
}
// TODO: Replace all ticks timers and intervals for events
class KromPump {
static var ticks:Array<Void -> Void> = [];
static var timer:haxe.Timer = null;
public static function add(tick:Void -> Void):Void {
if (!ticks.contains(tick)) ticks.push(tick);
if (timer == null) {
timer = new haxe.Timer(10);
timer.run = runAll;
}
}
public static function remove(tick:Void -> Void):Void {
ticks.remove(tick);
if (ticks.length == 0 && timer != null) {
timer.stop();
timer = null;
}
}
static function runAll():Void {
var snapshot = ticks.copy();
for (tick in snapshot) {
if (ticks.contains(tick)) tick();
}
}
}

View File

@ -0,0 +1,70 @@
package leenkx.network.krom;
import haxe.io.Bytes;
@:native("krom_udp_create") extern function krom_udp_create():Int;
@:native("krom_udp_bind") extern function krom_udp_bind(id:Int,
addr:String, port:Int):Bool;
@:native("krom_udp_sendto") extern function krom_udp_sendto(id:Int,
host:String, port:Int, data:js.lib.ArrayBuffer):Int;
@:native("krom_udp_recvfrom") extern function krom_udp_recvfrom(id:Int,
maxLen:Int):Dynamic;
@:native("krom_socket_close") extern function krom_udp_close(id:Int):Void;
@:native("krom_socket_set_blocking")
extern function krom_udp_set_blocking(id:Int, blocking:Bool):Void;
class KromUdpSocket {
private var _socketId:Int = -1;
public function new() {
_socketId = krom_udp_create();
setBlocking(false);
}
public function bind(host:KromSocket.KromHost, port:Int):Void {
if (_socketId >= 0) {
if (!krom_udp_bind(_socketId, host.host, port)) {
trace("UDP bind failed to " + host.host + ":" + port);
}
}
}
public function sendTo(data:Bytes, pos:Int, len:Int,
host:KromSocket.KromHost, port:Int):Int {
if (_socketId < 0) return 0;
var arrayBuffer:js.lib.ArrayBuffer = data.getData();
var exact:js.lib.ArrayBuffer =
untyped arrayBuffer.slice(pos, pos + len);
return krom_udp_sendto(_socketId, host.host, port, exact);
}
public function recvFrom(maxLength:Int):{data:Bytes, host:String,
port:Int} {
if (_socketId < 0) return null;
var result:Dynamic = krom_udp_recvfrom(_socketId, maxLength);
if (result == -1) {
_socketId = -1;
trace("Closed");
return null;
}
if (result == null || Std.isOfType(result, Int)) return null;
return {
data: Bytes.ofData(result.data),
host: result.host,
port: result.port
};
}
public function setBlocking(blocking:Bool):Void {
if (_socketId >= 0) {
krom_udp_set_blocking(_socketId, blocking);
}
}
public function close():Void {
if (_socketId >= 0) {
krom_udp_close(_socketId);
_socketId = -1;
}
}
}

View File

@ -0,0 +1,209 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Aegis128x4 {
static inline var D = 4;
static inline var RATE = 128;
static var C0 = Bytes.ofHex("000101020305080d1522375990e97962");
static var C1 = Bytes.ofHex("db3d18556dc22ff12011314273b528dd");
var v:Bytes;
var nv:Bytes;
var t:Bytes;
var t2:Bytes;
public function new(key:Bytes, nonce:Bytes) {
v = Bytes.alloc(512);
nv = Bytes.alloc(512);
t = Bytes.alloc(16);
t2 = Bytes.alloc(16);
init(key, nonce);
}
static inline function off(j:Int, i:Int):Int {
return (j * D + i) * 16;
}
static inline function xtime(x:Int):Int {
return ((x << 1) ^ ((x >> 7) * 0x1b)) & 0xff;
}
static function xorInto(a:Bytes, ao:Int, b:Bytes, bo:Int, out:Bytes, oo:Int):Void {
for (k in 0...16) out.set(oo + k, a.get(ao + k) ^ b.get(bo + k));
}
function aesRound(inOff:Int, rk:Bytes, rkOff:Int, out:Bytes, outOff:Int):Void {
for (c in 0...4) for (r in 0...4)
t2.set(4 * c + r, SBOX[v.get(inOff + 4 * ((c + r) & 3) + r)]);
for (c in 0...4) {
var b0 = t2.get(4 * c);
var b1 = t2.get(4 * c + 1);
var b2 = t2.get(4 * c + 2);
var b3 = t2.get(4 * c + 3);
out.set(outOff + 4 * c, (xtime(b0) ^ xtime(b1) ^ b1 ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c));
out.set(outOff + 4 * c + 1, (b0 ^ xtime(b1) ^ xtime(b2) ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c + 1));
out.set(outOff + 4 * c + 2, (b0 ^ b1 ^ xtime(b2) ^ xtime(b3) ^ b3) ^ rk.get(rkOff + 4 * c + 2));
out.set(outOff + 4 * c + 3, (xtime(b0) ^ b0 ^ b1 ^ b2 ^ xtime(b3)) ^ rk.get(rkOff + 4 * c + 3));
}
}
function update(m0:Bytes, m0off:Int, m1:Bytes, m1off:Int):Void {
for (i in 0...D) {
xorInto(v, off(0, i), m0, m0off + i * 16, t, 0);
aesRound(off(7, i), t, 0, nv, off(0, i));
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
xorInto(v, off(4, i), m1, m1off + i * 16, t, 0);
aesRound(off(3, i), t, 0, nv, off(4, i));
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
}
v.blit(0, nv, 0, 512);
}
function init(key:Bytes, nonce:Bytes):Void {
var kn = Bytes.alloc(16);
var kc0 = Bytes.alloc(16);
var kc1 = Bytes.alloc(16);
for (k in 0...16) {
kn.set(k, key.get(k) ^ nonce.get(k));
kc0.set(k, key.get(k) ^ C0.get(k));
kc1.set(k, key.get(k) ^ C1.get(k));
}
for (i in 0...D) {
v.blit(off(0, i), kn, 0, 16);
v.blit(off(1, i), C1, 0, 16);
v.blit(off(2, i), C0, 0, 16);
v.blit(off(3, i), C1, 0, 16);
v.blit(off(4, i), kn, 0, 16);
v.blit(off(5, i), kc0, 0, 16);
v.blit(off(6, i), kc1, 0, 16);
v.blit(off(7, i), kc0, 0, 16);
}
var m0 = Bytes.alloc(64);
var m1 = Bytes.alloc(64);
for (i in 0...D) {
m0.blit(i * 16, nonce, 0, 16);
m1.blit(i * 16, key, 0, 16);
}
for (r in 0...10) {
for (i in 0...D) {
v.set(off(3, i), v.get(off(3, i)) ^ i);
v.set(off(3, i) + 1, v.get(off(3, i) + 1) ^ (D - 1));
v.set(off(7, i), v.get(off(7, i)) ^ i);
v.set(off(7, i) + 1, v.get(off(7, i) + 1) ^ (D - 1));
}
update(m0, 0, m1, 0);
}
}
public function absorb(ai:Bytes, aiOff:Int):Void {
update(ai, aiOff, ai, aiOff + 64);
}
public function enc(xi:Bytes, xiOff:Int):Bytes {
var out = Bytes.alloc(RATE);
for (i in 0...D) {
for (k in 0...16) {
var z0 = v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k));
var z1 = v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k));
out.set(i * 16 + k, xi.get(xiOff + i * 16 + k) ^ z0);
out.set(64 + i * 16 + k, xi.get(xiOff + 64 + i * 16 + k) ^ z1);
}
}
update(xi, xiOff, xi, xiOff + 64);
return out;
}
public function nextBlock():Bytes {
var out = Bytes.alloc(RATE);
for (i in 0...D) {
for (k in 0...16) {
out.set(i * 16 + k, v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k)));
out.set(64 + i * 16 + k, v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k)));
}
}
for (i in 0...D) {
aesRound(off(7, i), v, off(0, i), nv, off(0, i));
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
aesRound(off(3, i), v, off(4, i), nv, off(4, i));
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
}
v.blit(0, nv, 0, 512);
return out;
}
public static function selfTest():Bool {
var key = Bytes.ofHex("000102030405060708090a0b0c0d0e0f");
var nonce = Bytes.ofHex("101112131415161718191a1b1c1d1e1f");
var a = new Aegis128x4(key, nonce);
var expected = Bytes.ofHex(
"924eb07635003a37e6c6575ba8ce1929c8b6a5d91475445e936d48e794be0ce2" +
"fcd37d050e24084befe3bbb219d647602e9f58cfb893a8800220242c373a8b18" +
"1a1f60c4fab64e5471dc72edfcf6fe6bc1e525ebea2d6375a9edd045dce96381" +
"97a3e25abd228a44d4a14a6d3fe9185cc2d4cf7f4287a98744645674265d4ca8" +
"7bb50c534f6ec4780530ff1cce8a16e87b08d57557da0b5ef7b5f7d98b0ba189" +
"6bfcac34ddb68404821a4d665303cb0fd95626f6dfad1aed7467622c38529932" +
"af339fd2d50ee45fc47665c647cf6586d0669b39d140f0e118a4a511efe2f95a" +
"7a94330f35c194fadda2a87e42cdeccc233b640d1f4d56e2757e72c1a9d8ecb1" +
"9f93737d699ba05c11e94f2b201bef5e61caf387cf7cfd3f8300ac7680ccfd76" +
"5825a671ecef03b7a9c98a601ae3211587a1fe4d558161a8f4c38731f3223032" +
"7a5aca78d636c05bbc702b2980196ab6915d868408495d07eb527789f282c575" +
"d0947bfbc1d3309cdffc9be1503aea628834ea57a15b9fbdc0245464a4b8cbef" +
"e46f4cf71a95ac45b6f0823e3aba1a868c4ecef682fc44a8eba911b3fc7d99f9" +
"a4fb61e2c928a2ca760b8772f2ea5f2e3d34ea89da73caa3016c280500a155a3" +
"85075f0080e9d618e7eb40f57c32d9f7d2ab2b320c6e93b155a3787cb83e5281" +
"0b3af0250ae36831a1b072e499929bcb5cce4d00329d69f1aae36aa541347512");
if (expected.compare(a.v) != 0) {
return false;
}
var ad = Bytes.alloc(128);
for (k in 0...8) ad.set(k, [1, 2, 3, 4][k & 3]);
a.absorb(ad, 0);
var msg = Bytes.alloc(128);
for (k in 0...120) msg.set(k, [4, 5, 6, 7][k & 3]);
var ct = a.enc(msg, 0);
var expectedCt = Bytes.ofHex(
"e836118562f4479c9d35c17356a833114c21f9aa39e4dda5e5c87f4152a00fce" +
"9a7c38f832eafe8b1c12f8a7cf12a81a1ad8a9c24ba9dedfbdaa586ffea67ddc" +
"801ea97d9ab4a872f42d0e352e2713dacd609f9442c17517c5a29daf3e2a3fac" +
"4ff6b1380c4e46df7b086af6ce6bc1ed594b8dd64aed2a7e");
for (k in 0...120) {
if (ct.get(k) != expectedCt.get(k)) {
return false;
}
}
return true;
}
static var SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
];
}

View File

@ -0,0 +1,119 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Base58 {
static var ALPHABET:String = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static var ALPHABET_MAP:Map<Int, Int> = {
var m = new Map<Int, Int>();
for (i in 0...ALPHABET.length) {
m.set(ALPHABET.charCodeAt(i), i);
}
m;
};
public static function encode(bytes:Bytes):String {
if (bytes.length == 0) return "";
var zeros = 0;
while (zeros < bytes.length && bytes.get(zeros) == 0) zeros++;
var size = Math.ceil(bytes.length * 138 / 100 + 1);
var b58 = [for (i in 0...size) 0];
for (i in zeros...bytes.length) {
var carry = bytes.get(i);
for (j in 0...size) {
var idx = size - 1 - j;
carry += b58[idx] << 8;
b58[idx] = carry % 58;
carry = Std.int(carry / 58);
}
}
var leadingZeros = 0;
for (i in 0...size) {
if (b58[i] != 0) break;
leadingZeros++;
}
var sb = new StringBuf();
for (i in 0...zeros) sb.addChar(ALPHABET.charCodeAt(0));
for (i in leadingZeros...size) {
sb.addChar(ALPHABET.charCodeAt(b58[i]));
}
return sb.toString();
}
public static function decode(str:String):Bytes {
if (str.length == 0) return Bytes.alloc(0);
var zeros = 0;
while (zeros < str.length && str.charCodeAt(zeros) == ALPHABET.charCodeAt(0)) zeros++;
var size = Math.ceil(str.length * 733 / 1000 + 1);
var bytes = [for (i in 0...size) 0];
for (i in zeros...str.length) {
var ch = str.charCodeAt(i);
if (!ALPHABET_MAP.exists(ch)) {
throw "Base58: invalid character " + str.charAt(i);
}
var carry = ALPHABET_MAP.get(ch);
for (j in 0...size) {
var idx = size - 1 - j;
carry += bytes[idx] * 58;
bytes[idx] = carry & 0xff;
carry = carry >> 8;
}
}
var leadingZeros = 0;
for (i in 0...size) {
if (bytes[i] != 0) break;
leadingZeros++;
}
var result = Bytes.alloc(zeros + size - leadingZeros);
for (i in 0...zeros) result.set(i, 0);
for (i in 0...(size - leadingZeros)) {
result.set(zeros + i, bytes[leadingZeros + i]);
}
return result;
}
}
class Base58Check {
static var CHECKSUM_LEN = 4;
public static function encode(bytes:Bytes):String {
var hash = sha256sha256(bytes);
var payload = Bytes.alloc(bytes.length + CHECKSUM_LEN);
payload.blit(0, bytes, 0, bytes.length);
payload.blit(bytes.length, hash, 0, CHECKSUM_LEN);
return Base58.encode(payload);
}
public static function decode(str:String):Bytes {
var payload = Base58.decode(str);
if (payload.length < CHECKSUM_LEN) {
throw "Base58Check: too short";
}
var data = payload.sub(0, payload.length - CHECKSUM_LEN);
var checksum = payload.sub(payload.length - CHECKSUM_LEN, CHECKSUM_LEN);
var hash = sha256sha256(data);
for (i in 0...CHECKSUM_LEN) {
if (hash.get(i) != checksum.get(i)) {
throw "Base58Check: checksum mismatch";
}
}
return data;
}
static function sha256sha256(bytes:Bytes):Bytes {
var h1 = haxe.crypto.Sha256.make(bytes);
return haxe.crypto.Sha256.make(h1);
}
}

View File

@ -0,0 +1,182 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class RawDict {
public var pairs:Array<{key:Bytes, val:Dynamic}> = [];
public function new() {}
}
class Bencode {
public static function encode(value:Dynamic):Bytes {
var out = new haxe.io.BytesOutput();
encodeValue(out, value);
return out.getBytes();
}
public static function encodeToString(value:Dynamic):String {
return encode(value).toString();
}
static function encodeValue(out:haxe.io.BytesOutput, value:Dynamic):Void {
if (Std.isOfType(value, Bool)) {
encodeInt(out, value ? 1 : 0);
} else if (Std.isOfType(value, Int) || Std.isOfType(value, Float)) {
encodeInt(out, value);
} else if (Std.isOfType(value, String)) {
encodeString(out, value);
} else if (Std.isOfType(value, Bytes)) {
encodeBytes(out, value);
} else if (Std.isOfType(value, RawDict)) {
encodeRawDict(out, value);
} else if (Std.isOfType(value, Array)) {
encodeList(out, value);
} else if (value != null && Reflect.isObject(value)) {
encodeDict(out, value);
} else {
throw "Bencode: cannot encode " + Type.typeof(value);
}
}
static function encodeInt(out:haxe.io.BytesOutput, value:Float):Void {
out.writeString("i");
var s = Std.string(value);
var dot = s.indexOf(".");
if (dot != -1) s = s.substring(0, dot);
if (s.indexOf("e") != -1 || s.indexOf("E") != -1) {
s = Std.string(Math.floor(value));
dot = s.indexOf(".");
if (dot != -1) s = s.substring(0, dot);
}
out.writeString(s);
out.writeString("e");
}
static function encodeString(out:haxe.io.BytesOutput, value:String):Void {
var bytes = Bytes.ofString(value);
encodeBytes(out, bytes);
}
static function encodeBytes(out:haxe.io.BytesOutput, bytes:Bytes):Void {
out.writeString(Std.string(bytes.length));
out.writeString(":");
out.writeBytes(bytes, 0, bytes.length);
}
static function encodeRawDict(out:haxe.io.BytesOutput, dict:RawDict):Void {
out.writeString("d");
for (p in dict.pairs) {
encodeBytes(out, p.key);
encodeValue(out, p.val);
}
out.writeString("e");
}
static function encodeList(out:haxe.io.BytesOutput, arr:Array<Dynamic>):Void {
out.writeString("l");
for (item in arr) {
encodeValue(out, item);
}
out.writeString("e");
}
static function encodeDict(out:haxe.io.BytesOutput, obj:Dynamic):Void {
out.writeString("d");
var keys = Reflect.fields(obj);
keys.sort(Reflect.compare);
for (key in keys) {
if (Reflect.field(obj, key) == null) continue;
encodeString(out, key);
encodeValue(out, Reflect.field(obj, key));
}
out.writeString("e");
}
public static function decode(bytes:Bytes):Dynamic {
return new Decoder(bytes).value();
}
public static function decodeString(str:String):Dynamic {
return decode(Bytes.ofString(str));
}
}
private class Decoder {
static inline var MAX_DECODE_LEN = 10 * 1024 * 1024; // 10 MB
var data:Bytes;
var pos:Int = 0;
public function new(data:Bytes) {
this.data = data;
}
public function value():Dynamic {
if (pos >= data.length) throw "Bencode: truncated input";
var b = data.get(pos++);
switch (b) {
case 'i'.code:
return decodeInt();
case 'l'.code:
return decodeList();
case 'd'.code:
return decodeDict();
default:
return decodeBytes();
}
}
function decodeInt():Float {
var start = pos;
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
if (pos >= data.length) throw "Bencode: unterminated int";
var s = data.getString(start, pos - start);
pos++;
var v = Std.parseFloat(s);
if (Math.isNaN(v)) throw "Bencode: bad int " + s;
return v;
}
function decodeBytes():Bytes {
var start = pos - 1;
if (data.get(start) < '0'.code || data.get(start) > '9'.code) {
throw "Bencode: bad byte length";
}
while (pos < data.length && data.get(pos) != ':'.code) pos++;
if (pos >= data.length) throw "Bencode: unterminated length";
var len = Std.parseInt(data.getString(start, pos - start));
pos++;
if (len == null || len < 0 || len > MAX_DECODE_LEN
|| pos + len > data.length) {
throw "Bencode: invalid byte length " + len;
}
var bytes = Bytes.alloc(len);
bytes.blit(0, data, pos, len);
pos += len;
return bytes;
}
function decodeList():Array<Dynamic> {
var arr:Array<Dynamic> = [];
while (true) {
if (pos >= data.length) throw "Bencode: unterminated list";
if (data.get(pos) == 'e'.code) break;
arr.push(value());
}
pos++;
return arr;
}
function decodeDict():Dynamic {
var obj:Dynamic = {};
while (true) {
if (pos >= data.length) throw "Bencode: unterminated dict";
if (data.get(pos) == 'e'.code) break;
var key:Bytes = value();
Reflect.setField(obj, key.toString(), value());
}
pos++;
return obj;
}
}

View File

@ -0,0 +1,152 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.Base58.Base58Check;
class Crypto {
public static var SEEDPREFIX(default, null) = "490a";
public static var ADDRESSPREFIX(default, null) = "55";
static var drbg:Aegis128x4 = null;
static var drbgBuf:Bytes = null;
static var drbgPos:Int = 0;
static var drbgNative:Int = -1;
public static function randomBytes(length:Int):Bytes {
var bytes = Bytes.alloc(length);
#if (js && !kha_krom)
var webCrypto:Dynamic = js.Syntax.code("(typeof globalThis !== 'undefined' ? globalThis.crypto : null)");
if (webCrypto != null && webCrypto.getRandomValues != null) {
var view = new js.lib.Uint8Array(length);
webCrypto.getRandomValues(view);
for (i in 0...length) bytes.set(i, view[i]);
return bytes;
}
#end
#if lnx_datachannel
if (drbgNative == -1) {
var seed = osEntropy(32);
try {
drbgNative = datachannel.RTC.aegisAvailable()
&& datachannel.RTC.aegisInit(seed.sub(0, 16), seed.sub(16, 16)) ? 1 : 0;
} catch (e:Dynamic) {
drbgNative = 0;
}
}
if (drbgNative == 1) {
return datachannel.RTC.aegisRandom(length);
}
#end
if (drbg == null) {
var seed = osEntropy(32);
drbg = new Aegis128x4(seed.sub(0, 16), seed.sub(16, 16));
drbgBuf = Bytes.alloc(0);
drbgPos = 0;
}
var off = 0;
while (off < length) {
if (drbgPos >= drbgBuf.length) {
drbgBuf = drbg.nextBlock();
drbgPos = 0;
}
var n = Std.int(Math.min(drbgBuf.length - drbgPos, length - off));
bytes.blit(off, drbgBuf, drbgPos, n);
drbgPos += n;
off += n;
}
return bytes;
}
static function osEntropy(length:Int):Bytes {
var bytes = Bytes.alloc(length);
#if kha_krom
var kromBytes = leenkx.network.torrent.transport.RtcNative.randomBytes(length);
if (kromBytes != null) return kromBytes;
#end
#if sys
if (Sys.systemName() != "Windows") {
try {
var f = sys.io.File.read("/dev/urandom", true);
f.readBytes(bytes, 0, length);
f.close();
return bytes;
} catch(e:Dynamic) {}
} else {
try {
var p = new sys.io.Process("powershell", [
"-NoProfile", "-Command",
"$r = New-Object 'byte[]' " + length + "; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($r); -join ($r | ForEach-Object { $_.ToString('x2') })"
]);
var hex = StringTools.trim(p.stdout.readAll().toString());
p.close();
if (hex.length == length * 2) {
return Bytes.ofHex(hex);
}
} catch(e:Dynamic) {
trace("Windows RNG via PowerShell failed: " + e);
}
}
#end
throw "Crypto: OS entropy unavailable. Refusing to generate predictable keys.";
}
public static function hash(data:Bytes):Bytes {
return TweetNaCl.hash(data);
}
public static function signDetached(message:Bytes, secretKey:Bytes):Bytes {
return TweetNaCl.signDetached(message, secretKey);
}
public static function signDetachedVerify(message:Bytes, signature:Bytes, publicKey:Bytes):Bool {
return TweetNaCl.signDetachedVerify(message, signature, publicKey);
}
public static function box(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
return TweetNaCl.box(message, nonce, theirPublicKey, mySecretKey);
}
public static function boxOpen(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
return TweetNaCl.boxOpen(message, nonce, theirPublicKey, mySecretKey);
}
public static function boxKeyPair():{publicKey:Bytes, secretKey:Bytes} {
return TweetNaCl.boxKeyPair();
}
public static function signKeyPairFromSeed(seed:Bytes):{publicKey:Bytes, secretKey:Bytes} {
return TweetNaCl.signKeyPairFromSeed(seed);
}
public static var boxNonceLength(get, null):Int;
static function get_boxNonceLength():Int {
return TweetNaCl.crypto_box_NONCEBYTES;
}
public static function ripemd160(data:Bytes):Bytes {
return Ripemd160.make(data);
}
public static function encodeSeed(material:Bytes):String {
var prefix = Bytes.ofHex(SEEDPREFIX);
var payload = Bytes.alloc(prefix.length + material.length);
payload.blit(0, prefix, 0, prefix.length);
payload.blit(prefix.length, material, 0, material.length);
return Base58Check.encode(payload);
}
public static function encodeAddress(publicKey:Bytes):String {
var prefix = Bytes.ofHex(ADDRESSPREFIX);
var hash = hash(publicKey);
var ripemd = ripemd160(hash);
var payload = Bytes.alloc(prefix.length + ripemd.length);
payload.blit(0, prefix, 0, prefix.length);
payload.blit(prefix.length, ripemd, 0, ripemd.length);
return Base58Check.encode(payload);
}
public static function toHex(bytes:Bytes):String {
return bytes.toHex();
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Riccardo Mazzucco - All Rights reserved
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,580 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.Types;
import leenkx.network.torrent.Base58.Base58Check;
import leenkx.network.torrent.transport.ITransport;
import leenkx.network.torrent.transport.ITransport.TransportOpts;
import leenkx.network.torrent.TorrentInfo;
class LeenkxClient {
public static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var defaultHeartbeat:Int = 30000;
public var identifier:String;
public var seed:String;
public var pk:String;
public var ek:String;
public var timeout:Float;
public var announce:Array<String>;
public var infoHashBinary:String;
public var peerIdBinary:String;
public var keyPair:{publicKey:Bytes, secretKey:Bytes};
public var keyPairEncrypt:{publicKey:Bytes, secretKey:Bytes};
public var peers:Map<String, PeerInfo> = [];
public var seen:Map<String, Float> = [];
public var pkToAddress:Map<String, String> = [];
public var isRelay:Bool = false;
public var lastPeerCount:Null<Int> = null;
public var lastPeerId:String = null;
public var lastPeerIndex:Int = 0;
public var serverAddress:String = null;
public var heartbeatTimer:Null<haxe.Timer> = null;
public var onOpen:Void -> Void;
public var onTorrent:String -> Dynamic -> Void;
public var onMessage:String -> Dynamic -> Dynamic -> Void;
public var onSeen:String -> Void;
public var onLeft:String -> Void;
public var onServer:String -> Void;
public var onPing:String -> Void;
public var onTimeout:String -> Void;
public var onConnections:Int -> Void;
public var onWireSeen:Int -> Dynamic -> Void;
public var onWireLeft:Int -> Dynamic -> Void;
public var onTracker:String -> Dynamic -> Void;
public var onAnnounce:String -> Void;
public var onError:String -> Void;
public var transport:ITransport;
private var deferredOpts:TransportOpts = null;
private var heartbeatInterval:Int = 30000;
public var torrents(get, never):Map<String, Torrent>;
public var onTorrentAdded:Torrent -> Void;
public var onTorrentDone:Torrent -> Void;
var swarm:TorrentClient = null;
function get_torrents():Map<String, Torrent> {
return swarm != null ? swarm.torrents : [];
}
function getSwarm():TorrentClient {
if (swarm == null) {
swarm = new TorrentClient();
swarm.onError = function(e) {
if (onError != null) onError(e);
};
swarm.onTorrentAdded = function(t) {
if (onTorrentAdded != null) onTorrentAdded(t);
};
swarm.onTorrentDone = function(t) {
if (onTorrentDone != null) onTorrentDone(t);
};
}
return swarm;
}
public function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().add(torrentId, opts, cb);
}
public function seedMeta(meta:MetaInfo,
files:Array<{path:String, data:Bytes}>,
?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().seedMeta(meta, files, opts, cb);
}
public function get(id:Dynamic):Torrent {
if (Std.isOfType(id, Torrent)) return id;
return swarm != null ? swarm.get(id) : null;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
if (swarm != null) swarm.remove(infoHashHex, deleteData);
}
public function listen(port:Int):Void {
getSwarm().listen(port);
}
public function new(?identifier:String, ?opts:LeenkxOptions) {
if (identifier != null && !Std.isOfType(identifier, String)) {
opts = cast identifier;
identifier = null;
}
opts = opts != null ? opts : {};
this.announce = opts.announce != null ? opts.announce : TorrentClient.defaultTrackers;
this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT;
if (opts.seed != null) {
this.seed = opts.seed;
} else {
this.seed = Crypto.encodeSeed(Crypto.randomBytes(32));
}
if (opts.keyPair != null) {
this.keyPair = opts.keyPair;
} else {
var seedBytes = Base58Check.decode(this.seed);
var material = seedBytes.sub(2, seedBytes.length - 2);
this.keyPair = Crypto.signKeyPairFromSeed(material);
}
this.keyPairEncrypt = Crypto.boxKeyPair();
this.pk = Base58.encode(this.keyPair.publicKey);
this.ek = Base58.encode(this.keyPairEncrypt.publicKey);
this.identifier = identifier != null ? identifier : this.address();
var infoHash = TorrentInfo.identifierHash(this.identifier);
this.infoHashBinary = TorrentInfo.toBinaryString(infoHash);
this.peerIdBinary = TorrentInfo.toBinaryString(TorrentInfo.peerId());
createTransport(opts, opts.deferJoin == true);
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : defaultHeartbeat;
}
function createTransport(opts:LeenkxOptions, defer:Bool):Void {
var transportOpts:TransportOpts = {
announce: this.announce,
iceServers: opts.iceServers,
tracker: opts.tracker,
wtOpts: opts.wtOpts,
torrentOpts: opts.torrentOpts, torrent: opts.torrent,
pk: this.pk,
ek: this.ek,
infoHashBinary: this.infoHashBinary,
peerIdBinary: this.peerIdBinary
};
var announceUrl = this.announce[0];
if (opts.transport == "datachannel") {
transport = new leenkx.network.torrent.transport.DatachannelTransport(announceUrl, opts.iceServers);
} else {
transport = new leenkx.network.torrent.transport.WireTransport(announceUrl, opts.iceServers);
}
transport.onReady = onTransportReady;
transport.onPeerConnected = onTransportPeerConnected;
transport.onPeerDisconnected = onTransportPeerDisconnected;
transport.onPeerData = onTransportPeerData;
transport.onPeerHandshake = onTransportPeerHandshake;
transport.onTrackerUpdate = onTransportTrackerUpdate;
transport.onTrackerAnnounce = onTransportTrackerAnnounce;
transport.onError = onTransportError;
if (defer) {
deferredOpts = transportOpts;
} else {
transport.join(this.identifier, transportOpts);
}
}
public function connect():Void {
if (deferredOpts != null) {
var opts = deferredOpts;
deferredOpts = null;
transport.join(this.identifier, opts);
}
this.heartbeat(this.heartbeatInterval);
}
function onTransportReady():Void {
if (onTorrent != null) onTorrent(identifier, null);
}
function onTransportPeerConnected(peerId:String, count:Int):Void {
lastPeerId = peerId;
var prevCount = lastPeerCount;
lastPeerCount = count;
if (onWireSeen != null) onWireSeen(count, {peerId: peerId});
if (count != prevCount) {
if (onConnections != null) onConnections(count);
}
}
function onTransportPeerDisconnected(count:Int):Void {
if (onWireLeft != null) onWireLeft(count, {peerId: lastPeerId});
if (count != lastPeerCount) {
lastPeerCount = count;
if (onConnections != null) onConnections(count);
}
}
function onTransportPeerData(senderPk:String, data:Bytes):Void {
onMessageData(identifier, data, senderPk);
}
function onTransportPeerHandshake(peerPk:String, peerEk:String):Void {
if (peerPk != null && peerPk != "") {
sawPeer(peerPk, peerEk, identifier);
}
}
function onTransportTrackerUpdate(update:Dynamic):Void {
if (onTracker != null) onTracker(identifier, update);
}
function onTransportTrackerAnnounce():Void {
if (onAnnounce != null) onAnnounce(identifier);
}
function onTransportError(msg:String):Void {
trace("onTransportError: " + msg);
if (onError != null) onError(msg);
}
public function address(?pk:Dynamic):String {
var pkBytes:Bytes = null;
if (pk != null && Std.isOfType(pk, String)) {
pkBytes = Base58.decode(pk);
} else if (pk != null) {
pkBytes = pk;
} else {
pkBytes = this.keyPair.publicKey;
}
return Crypto.encodeAddress(pkBytes);
}
function addressFromPk(pkStr:String):String {
var cached = pkToAddress.get(pkStr);
if (cached != null) return cached;
var addr = address(Base58.decode(pkStr));
pkToAddress.set(pkStr, addr);
return addr;
}
public function encodeSeed(material:Bytes):String {
return Crypto.encodeSeed(material);
}
public function ping():Void {
var packet = makePacket({y: PacketType.Ping});
sendRaw(packet);
}
public function send(?address:String, message:Dynamic):Void {
if (message == null) {
message = address;
address = null;
}
var packet = makePacket({
y: PacketType.Message,
v: haxe.Json.stringify(message)
});
var peerPk:String = null;
if (address != null) {
if (peers.exists(address)) {
peerPk = peers[address].pk;
packet = encryptPacket(peerPk, packet);
} else {
throw address + " not seen - no public key.";
}
}
sendRaw(packet, peerPk);
}
public function sendRawMessage(data:Bytes, ?address:String):Void {
var marked = Bytes.alloc(data.length + 1);
marked.set(0, 0x00);
marked.blit(1, data, 0, data.length);
if (transport != null) {
if (address != null) {
if (!peers.exists(address)) {
throw address + " not seen - no public key.";
}
if (!transport.sendTo(peers[address].pk, marked)) {
trace("sendRawMessage: no open channel to " + address);
}
} else {
transport.broadcast(marked);
}
}
}
public function heartbeat(?interval:Int):Void {
interval = interval != null ? interval : 30000;
if (heartbeatTimer != null) heartbeatTimer.stop();
heartbeatTimer = new haxe.Timer(interval);
heartbeatTimer.run = () -> {
ping();
var t = now();
for (p in [for (k in peers.keys()) k]) {
var peerInfo = peers[p];
if (peerInfo.last + interval < t) {
peers.remove(p);
if (onTimeout != null) onTimeout(p);
if (onLeft != null) onLeft(p);
}
}
for (h in [for (k in seen.keys()) k]) {
if (seen.get(h) + interval < t) {
seen.remove(h);
}
}
};
}
public function destroy(?cb:Void -> Void):Void {
if (heartbeatTimer != null) {
heartbeatTimer.stop();
heartbeatTimer = null;
}
var packet = makePacket({y: PacketType.Leave});
sendRaw(packet);
if (swarm != null) {
swarm.destroy();
swarm = null;
}
if (transport != null) {
transport.destroy(cb);
} else if (cb != null) {
cb();
}
}
public function close(?cb:Void -> Void):Void {
destroy(cb);
}
public function connections():Int {
if (transport != null) {
var count = transport.getPeerCount();
if (count != lastPeerCount) {
lastPeerCount = count;
if (onConnections != null) onConnections(count);
}
return count;
}
return 0;
}
function makePacket(params:Dynamic):Bytes {
var p:Dynamic = {
t: now(),
i: identifier,
pk: pk,
ek: ek,
n: Crypto.randomBytes(8)
};
for (k in Reflect.fields(params)) {
Reflect.setField(p, k, Reflect.field(params, k));
}
var pe = Bencode.encode(p);
var sig = Crypto.signDetached(pe, keyPair.secretKey);
return Bencode.encode({s: sig, p: pe});
}
function encryptPacket(peerPk:String, packet:Bytes):Bytes {
var addr = addressFromPk(peerPk);
if (peers.exists(addr)) {
var nonce = Crypto.randomBytes(Crypto.boxNonceLength);
var encrypted = Crypto.box(
packet,
nonce,
Base58.decode(peers[addr].ek),
keyPairEncrypt.secretKey
);
return Bencode.encode({
n: nonce,
ek: Base58.encode(keyPairEncrypt.publicKey),
e: encrypted
});
} else {
throw addressFromPk(peerPk) + " not seen - no encryption key.";
}
}
function sendRaw(message:Bytes, ?peerPk:String):Void {
var hash = Crypto.hash(message).sub(0, 16).toHex();
seen.set(hash, now());
if (transport != null) {
if (peerPk != null) {
if (!transport.sendTo(peerPk, message)) {
transport.broadcast(message);
}
} else {
transport.broadcast(message);
}
}
}
function onMessageData(identifier:String, message:Bytes, ?senderPk:String):Void {
var hash = (message.length > 0 && message.get(0) == 0x00)
? null : Crypto.hash(message).sub(0, 16).toHex();
var t = now();
if (hash == null || !seen.exists(hash)) {
if (message.length > 0 && (message.get(0) == 0x00 || message.get(0) == 0x01)) {
var isRelayed = message.get(0) == 0x01;
var offset = 1;
var msgSenderPk:String = senderPk;
if (isRelayed) {
if (message.length < 2) {
seen.set(hash, t);
return;
}
var pkLen = message.get(1);
if (2 + pkLen > message.length) {
seen.set(hash, t);
return;
}
var claimedPk = message.sub(2, pkLen).toString();
if (pkToAddress.exists(claimedPk)) {
msgSenderPk = claimedPk;
}
offset = 2 + pkLen;
}
var rawPayload = message.sub(offset, message.length - offset);
var payload = new leenkx.network.Buffer();
payload.writeBytes(rawPayload);
if (isRelayed) seen.set(hash, t);
var senderAddr = msgSenderPk != null ? pkToAddress.get(msgSenderPk) : null;
if (senderAddr == null && msgSenderPk != null && msgSenderPk != "") {
senderAddr = addressFromPk(msgSenderPk);
}
if (onMessage != null) {
onMessage(senderAddr, payload, null);
}
if (isRelay && transport != null && transport.getPeerCount() > 1) {
if (isRelayed) {
sendRaw(message);
} else if (msgSenderPk != null && msgSenderPk != "") {
var pkBytes = Bytes.ofString(msgSenderPk);
var relayed = Bytes.alloc(1 + 1 + pkBytes.length + rawPayload.length);
relayed.set(0, 0x01);
relayed.set(1, pkBytes.length);
relayed.blit(2, pkBytes, 0, pkBytes.length);
relayed.blit(2 + pkBytes.length, rawPayload, 0, rawPayload.length);
sendRaw(relayed);
}
}
return;
}
var unpacked:Dynamic = null;
try {
unpacked = Bencode.decode(message);
} catch(e:Dynamic) {
trace("Bencode decode error: " + e);
seen.set(hash, t);
return;
}
try {
var isEncrypted = Reflect.field(unpacked, "e") != null
&& Reflect.field(unpacked, "n") != null
&& Reflect.field(unpacked, "ek") != null;
if (isEncrypted) {
var ekStr:String = unpacked.ek.toString();
var decrypted = Crypto.boxOpen(
unpacked.e,
unpacked.n,
Base58.decode(ekStr),
keyPairEncrypt.secretKey
);
if (decrypted != null) {
unpacked = Bencode.decode(decrypted);
} else {
unpacked = null;
}
}
if (unpacked != null && unpacked.p != null) {
var packet:Dynamic = Bencode.decode(unpacked.p);
var pkStr:String = packet.pk.toString();
var idStr:String = packet.i.toString();
var checkSig = Crypto.signDetachedVerify(unpacked.p, unpacked.s, Base58.decode(pkStr));
var checkId = idStr == identifier;
var checkTime = packet.t + timeout > t;
if (checkSig && checkId && checkTime) {
var ekStr:String = packet.ek.toString();
sawPeer(pkStr, ekStr, identifier);
var yStr:String = packet.y != null ? packet.y.toString() : null;
if (yStr == PacketType.Message) {
var msgStr:String = packet.v.toString();
var msgJson:Dynamic = null;
try {
msgJson = haxe.Json.parse(msgStr);
} catch(e) {
}
if (msgJson != null) {
if (onMessage != null) {
onMessage(addressFromPk(pkStr), msgJson, packet);
}
} else {
try {
var payload = new leenkx.network.Buffer();
payload.writeBytes(
haxe.crypto.Base64.decode(msgStr));
if (onMessage != null) {
onMessage(addressFromPk(pkStr), payload, packet);
}
} catch(e:Dynamic) {}
}
} else if (yStr == PacketType.Ping) {
if (onPing != null) onPing(addressFromPk(pkStr));
} else if (yStr == PacketType.Leave) {
var addr = addressFromPk(pkStr);
peers.remove(addr);
if (onLeft != null) onLeft(addr);
}
}
}
if (transport != null && transport.getPeerCount() > 1) {
sendRaw(message);
}
} catch(e:Dynamic) {
trace("onMessageData error processing packet: " + e);
}
}
seen.set(hash, t);
}
function sawPeer(pk:String, ek:String, identifier:String):Void {
var t = now();
var addr = addressFromPk(pk);
if (addr == address()) return;
if (!peers.exists(addr) || peers[addr].last + timeout < t) {
var index = 0;
var taken = true;
while (taken) {
taken = false;
index++;
for (k in peers.keys()) {
if (peers[k].index == index) { taken = true; break; }
}
}
peers.set(addr, {pk: pk, ek: ek, last: t, index: index});
lastPeerIndex = index;
lastPeerId = addr;
if (onOpen != null) onOpen();
if (onSeen != null) onSeen(addr);
if (addr == identifier) {
serverAddress = addr;
if (onServer != null) onServer(addr);
}
var packet = makePacket({y: PacketType.Ping});
sendRaw(packet);
} else {
var p = peers.get(addr);
p.ek = ek;
p.last = t;
}
}
static function now():Float {
return Date.now().getTime();
}
}

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