Lubos Lenco 4 年之前
父节点
当前提交
b8faf71c1b
共有 56 个文件被更改,包括 10875 次插入0 次删除
  1. 二进制
      Assets/bnoise_rank.png
  2. 二进制
      Assets/bnoise_scramble.png
  3. 二进制
      Assets/bnoise_sobol.png
  4. 7 0
      LICENSE.md
  5. 二进制
      Shaders/raytrace_bake_ao.cso
  6. 二进制
      Shaders/raytrace_bake_ao.spirv
  7. 二进制
      Shaders/raytrace_bake_bent.cso
  8. 二进制
      Shaders/raytrace_bake_bent.spirv
  9. 二进制
      Shaders/raytrace_bake_light.cso
  10. 二进制
      Shaders/raytrace_bake_light.spirv
  11. 二进制
      Shaders/raytrace_bake_thick.cso
  12. 二进制
      Shaders/raytrace_bake_thick.spirv
  13. 二进制
      Shaders/raytrace_brute_core.cso
  14. 二进制
      Shaders/raytrace_brute_core.spirv
  15. 二进制
      Shaders/raytrace_brute_full.cso
  16. 二进制
      Shaders/raytrace_brute_full.spirv
  17. 2 0
      Shaders/src/build.bat
  18. 6 0
      Shaders/src/build_dxr.bat
  19. 6 0
      Shaders/src/build_vkrt.bat
  20. 二进制
      Shaders/src/dxc.exe
  21. 二进制
      Shaders/src/dxcompiler.dll
  22. 二进制
      Shaders/src/dxil.dll
  23. 92 0
      Shaders/src/raytrace_bake_ao.hlsl
  24. 94 0
      Shaders/src/raytrace_bake_bent.hlsl
  25. 121 0
      Shaders/src/raytrace_bake_light.hlsl
  26. 94 0
      Shaders/src/raytrace_bake_thick.hlsl
  27. 257 0
      Shaders/src/raytrace_brute.hlsl
  28. 27 0
      Shaders/src/std/attrib.hlsl
  29. 34 0
      Shaders/src/std/dof.hlsl
  30. 65 0
      Shaders/src/std/math.hlsl
  31. 34 0
      Shaders/src/std/rand.hlsl
  32. 403 0
      Sources/arm/format/BlendParser.hx
  33. 314 0
      Sources/arm/format/ExrWriter.hx
  34. 212 0
      Sources/arm/format/FbxBinaryParser.hx
  35. 869 0
      Sources/arm/format/FbxLibrary.hx
  36. 101 0
      Sources/arm/format/FbxParser.hx
  37. 37 0
      Sources/arm/format/JpgData.hx
  38. 740 0
      Sources/arm/format/JpgWriter.hx
  39. 22 0
      Sources/arm/format/MeshParser.hx
  40. 500 0
      Sources/arm/format/ObjParser.hx
  41. 51 0
      Sources/arm/format/PngData.hx
  42. 206 0
      Sources/arm/format/PngTools.hx
  43. 82 0
      Sources/arm/format/PngWriter.hx
  44. 52 0
      Sources/arm/geom/Plane.hx
  45. 79 0
      Sources/arm/geom/Sphere.hx
  46. 283 0
      Sources/arm/render/RenderPathRaytrace.hx
  47. 1724 0
      Sources/arm/shader/MaterialParser.hx
  48. 630 0
      Sources/arm/shader/NodeShader.hx
  49. 119 0
      Sources/arm/shader/NodeShaderContext.hx
  50. 21 0
      Sources/arm/shader/NodeShaderData.hx
  51. 2738 0
      Sources/arm/shader/NodesMaterial.hx
  52. 524 0
      Sources/arm/shader/ShaderFunctions.hx
  53. 12 0
      Sources/arm/sys/BuildMacros.hx
  54. 177 0
      Sources/arm/sys/File.hx
  55. 132 0
      Sources/arm/sys/Path.hx
  56. 8 0
      Sources/import.hx

二进制
Assets/bnoise_rank.png


二进制
Assets/bnoise_scramble.png


二进制
Assets/bnoise_sobol.png


+ 7 - 0
LICENSE.md

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

二进制
Shaders/raytrace_bake_ao.cso


二进制
Shaders/raytrace_bake_ao.spirv


二进制
Shaders/raytrace_bake_bent.cso


二进制
Shaders/raytrace_bake_bent.spirv


二进制
Shaders/raytrace_bake_light.cso


二进制
Shaders/raytrace_bake_light.spirv


二进制
Shaders/raytrace_bake_thick.cso


二进制
Shaders/raytrace_bake_thick.spirv


二进制
Shaders/raytrace_brute_core.cso


二进制
Shaders/raytrace_brute_core.spirv


二进制
Shaders/raytrace_brute_full.cso


二进制
Shaders/raytrace_brute_full.spirv


+ 2 - 0
Shaders/src/build.bat

@@ -0,0 +1,2 @@
+call build_dxr.bat
+call build_vkrt.bat

+ 6 - 0
Shaders/src/build_dxr.bat

@@ -0,0 +1,6 @@
+.\dxc.exe -Zpr -Fo ..\raytrace_brute_core.cso -T lib_6_3 .\raytrace_brute.hlsl
+.\dxc.exe -Zpr -Fo ..\raytrace_brute_full.cso -T lib_6_3 .\raytrace_brute.hlsl -D _FULL
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_ao.cso -T lib_6_3 .\raytrace_bake_ao.hlsl
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_light.cso -T lib_6_3 .\raytrace_bake_light.hlsl
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_bent.cso -T lib_6_3 .\raytrace_bake_bent.hlsl
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_thick.cso -T lib_6_3 .\raytrace_bake_thick.hlsl

+ 6 - 0
Shaders/src/build_vkrt.bat

@@ -0,0 +1,6 @@
+.\dxc.exe -Zpr -Fo ..\raytrace_brute_core.spirv -T lib_6_4 .\raytrace_brute.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all
+.\dxc.exe -Zpr -Fo ..\raytrace_brute_full.spirv -T lib_6_4 .\raytrace_brute.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all -D _FULL
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_ao.spirv -T lib_6_4 .\raytrace_bake_ao.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_light.spirv -T lib_6_4 .\raytrace_bake_light.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_bent.spirv -T lib_6_4 .\raytrace_bake_bent.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all
+.\dxc.exe -Zpr -Fo ..\raytrace_bake_thick.spirv -T lib_6_4 .\raytrace_bake_thick.hlsl -spirv -fvk-use-scalar-layout -fspv-target-env="vulkan1.2" -fvk-u-shift 10 all -fvk-b-shift 11 all

二进制
Shaders/src/dxc.exe


二进制
Shaders/src/dxcompiler.dll


二进制
Shaders/src/dxil.dll


+ 92 - 0
Shaders/src/raytrace_bake_ao.hlsl

@@ -0,0 +1,92 @@
+
+#include "std/rand.hlsl"
+#include "std/math.hlsl"
+
+struct Vertex {
+	uint posxy;
+	uint poszw;
+	uint nor;
+	uint tex;
+};
+
+struct RayGenConstantBuffer {
+	float4 v0; // frame, strength, radius, offset
+	float4 v1;
+	float4 v2;
+	float4 v3;
+	float4 v4;
+};
+
+struct RayPayload {
+	float4 color;
+	float3 ray_origin;
+	float3 ray_dir;
+};
+
+RWTexture2D<float4> render_target : register(u0);
+RaytracingAccelerationStructure scene : register(t0);
+ByteAddressBuffer indices : register(t1);
+StructuredBuffer<Vertex> vertices : register(t2);
+ConstantBuffer<RayGenConstantBuffer> constant_buffer : register(b0);
+
+Texture2D<float4> mytexture0 : register(t3);
+Texture2D<float4> mytexture1 : register(t4);
+Texture2D<float4> mytexture2 : register(t5);
+Texture2D<float4> mytexture_env : register(t6);
+Texture2D<float4> mytexture_sobol : register(t7);
+Texture2D<float4> mytexture_scramble : register(t8);
+Texture2D<float4> mytexture_rank : register(t9);
+
+static const int SAMPLES = 64;
+static uint seed = 0;
+
+[shader("raygeneration")]
+void raygeneration() {
+	float2 xy = DispatchRaysIndex().xy + 0.5f;
+	float4 tex0 = mytexture0.Load(uint3(xy, 0));
+	if (tex0.a == 0.0) {
+		render_target[DispatchRaysIndex().xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
+		return;
+	}
+	float3 pos = tex0.rgb;
+	float3 nor = mytexture1.Load(uint3(xy, 0)).rgb;
+
+	RayPayload payload;
+
+	RayDesc ray;
+	ray.TMin = constant_buffer.v0.w * 0.01;
+	ray.TMax = constant_buffer.v0.z * 10.0;
+	ray.Origin = pos;
+
+	float3 accum = float3(0, 0, 0);
+
+	for (int i = 0; i < SAMPLES; ++i) {
+		ray.Direction = cos_weighted_hemisphere_direction(nor, i, seed, constant_buffer.v0.x, mytexture_sobol, mytexture_scramble, mytexture_rank);
+		seed += 1;
+		TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload);
+		accum += payload.color.rgb;
+	}
+
+	accum /= SAMPLES;
+
+	float3 color = float3(render_target[DispatchRaysIndex().xy].xyz);
+	if (constant_buffer.v0.x == 0) {
+		color = accum.xyz;
+	}
+	else {
+		float a = 1.0 / constant_buffer.v0.x;
+		float b = 1.0 - a;
+		color = color * b + accum.xyz * a;
+	}
+	render_target[DispatchRaysIndex().xy] = float4(color.xyz, 1.0f);
+}
+
+[shader("closesthit")]
+void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) {
+	payload.color = float4(0, 0, 0, 1);
+}
+
+[shader("miss")]
+void miss(inout RayPayload payload) {
+	payload.color = float4(1, 1, 1, 1);
+}

+ 94 - 0
Shaders/src/raytrace_bake_bent.hlsl

@@ -0,0 +1,94 @@
+
+#include "std/rand.hlsl"
+#include "std/math.hlsl"
+
+struct Vertex {
+	uint posxy;
+	uint poszw;
+	uint nor;
+	uint tex;
+};
+
+struct RayGenConstantBuffer {
+	float4 v0; // frame, strength, radius, offset
+	float4 v1; // envstr, upaxis
+	float4 v2;
+	float4 v3;
+	float4 v4;
+};
+
+struct RayPayload {
+	float4 color;
+	float3 ray_origin;
+	float3 ray_dir;
+};
+
+RWTexture2D<float4> render_target : register(u0);
+RaytracingAccelerationStructure scene : register(t0);
+ByteAddressBuffer indices : register(t1);
+StructuredBuffer<Vertex> vertices : register(t2);
+ConstantBuffer<RayGenConstantBuffer> constant_buffer : register(b0);
+
+Texture2D<float4> mytexture0 : register(t3);
+Texture2D<float4> mytexture1 : register(t4);
+Texture2D<float4> mytexture2 : register(t5);
+Texture2D<float4> mytexture_env : register(t6);
+Texture2D<float4> mytexture_sobol : register(t7);
+Texture2D<float4> mytexture_scramble : register(t8);
+Texture2D<float4> mytexture_rank : register(t9);
+
+static const int SAMPLES = 64;
+static uint seed = 0;
+
+[shader("raygeneration")]
+void raygeneration() {
+	float2 xy = DispatchRaysIndex().xy + 0.5f;
+	float4 tex0 = mytexture0.Load(uint3(xy, 0));
+	if (tex0.a == 0.0) {
+		render_target[DispatchRaysIndex().xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
+		return;
+	}
+	float3 pos = tex0.rgb;
+	float3 nor = mytexture1.Load(uint3(xy, 0)).rgb;
+
+	RayPayload payload;
+
+	RayDesc ray;
+	ray.TMin = constant_buffer.v0.w * 0.01;
+	ray.TMax = constant_buffer.v0.z * 10.0;
+	ray.Origin = pos;
+
+	float3 accum = float3(0, 0, 0);
+
+	for (int i = 0; i < SAMPLES; ++i) {
+		ray.Direction = cos_weighted_hemisphere_direction(nor, i, seed, constant_buffer.v0.x, mytexture_sobol, mytexture_scramble, mytexture_rank);
+		seed += 1;
+		TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload);
+		accum += payload.color.rgb;
+	}
+
+	accum = normalize(accum / SAMPLES) * 0.5 + 0.5;
+
+	if (constant_buffer.v1.y > 0) accum.xyz = float3(accum.x, accum.z, 1.0 - accum.y);
+
+	float3 color = float3(render_target[DispatchRaysIndex().xy].xyz);
+	if (constant_buffer.v0.x == 0) {
+		color = accum.xyz;
+	}
+	else {
+		float a = 1.0 / constant_buffer.v0.x;
+		float b = 1.0 - a;
+		color = color * b + accum.xyz * a;
+	}
+	render_target[DispatchRaysIndex().xy] = float4(color.xyz, 1.0f);
+}
+
+[shader("closesthit")]
+void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) {
+	payload.color = float4(0, 0, 0, 0);
+}
+
+[shader("miss")]
+void miss(inout RayPayload payload) {
+	payload.color = float4(WorldRayDirection(), 0);
+}

+ 121 - 0
Shaders/src/raytrace_bake_light.hlsl

@@ -0,0 +1,121 @@
+
+#include "std/rand.hlsl"
+#include "std/math.hlsl"
+#include "std/attrib.hlsl"
+
+struct Vertex {
+	uint posxy;
+	uint poszw;
+	uint nor;
+	uint tex;
+};
+
+struct RayGenConstantBuffer {
+	float4 v0; // frame, strength, radius, offset
+	float4 v1; // envstr, upaxis, envangle
+	float4 v2;
+	float4 v3;
+	float4 v4;
+};
+
+struct RayPayload {
+	float4 color;
+	float3 ray_origin;
+	float3 ray_dir;
+};
+
+RWTexture2D<float4> render_target : register(u0);
+RaytracingAccelerationStructure scene : register(t0);
+ByteAddressBuffer indices : register(t1);
+StructuredBuffer<Vertex> vertices : register(t2);
+ConstantBuffer<RayGenConstantBuffer> constant_buffer : register(b0);
+
+Texture2D<float4> mytexture0 : register(t3);
+Texture2D<float4> mytexture1 : register(t4);
+Texture2D<float4> mytexture2 : register(t5);
+Texture2D<float4> mytexture_env : register(t6);
+Texture2D<float4> mytexture_sobol : register(t7);
+Texture2D<float4> mytexture_scramble : register(t8);
+Texture2D<float4> mytexture_rank : register(t9);
+
+static const int SAMPLES = 64;
+static uint seed = 0;
+
+[shader("raygeneration")]
+void raygeneration() {
+	float2 xy = DispatchRaysIndex().xy + 0.5f;
+	float4 tex0 = mytexture0.Load(uint3(xy, 0));
+	if (tex0.a == 0.0) {
+		render_target[DispatchRaysIndex().xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
+		return;
+	}
+	float3 pos = tex0.rgb;
+	float3 nor = mytexture1.Load(uint3(xy, 0)).rgb;
+
+	RayPayload payload;
+
+	RayDesc ray;
+	ray.TMin = constant_buffer.v0.w * 0.01;
+	ray.TMax = constant_buffer.v0.z * 10.0;
+	ray.Origin = pos;
+
+	float3 accum = float3(0, 0, 0);
+
+	for (int i = 0; i < SAMPLES; ++i) {
+		ray.Direction = cos_weighted_hemisphere_direction(nor, i, seed, constant_buffer.v0.x, mytexture_sobol, mytexture_scramble, mytexture_rank);
+		seed += 1;
+		TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload);
+		accum += payload.color.rgb;
+	}
+
+	accum /= SAMPLES;
+
+	float3 texpaint2 = mytexture2.Load(uint3(xy, 0)).rgb; // layer base
+	accum *= texpaint2;
+
+	float3 color = float3(render_target[DispatchRaysIndex().xy].xyz);
+	if (constant_buffer.v0.x == 0) {
+		color = accum.xyz;
+	}
+	else {
+		float a = 1.0 / constant_buffer.v0.x;
+		float b = 1.0 - a;
+		color = color * b + accum.xyz * a;
+	}
+	render_target[DispatchRaysIndex().xy] = float4(color.xyz, 1.0f);
+}
+
+[shader("closesthit")]
+void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) {
+	const uint triangleIndexStride = 12; // 3 * 4
+	uint base_index = PrimitiveIndex() * triangleIndexStride;
+	uint3 indices_sample = indices.Load3(base_index);
+
+	float3 vertex_normals[3] = {
+		float3(S16toF32(vertices[indices_sample[0]].nor), S16toF32(vertices[indices_sample[0]].poszw).y),
+		float3(S16toF32(vertices[indices_sample[1]].nor), S16toF32(vertices[indices_sample[1]].poszw).y),
+		float3(S16toF32(vertices[indices_sample[2]].nor), S16toF32(vertices[indices_sample[2]].poszw).y)
+	};
+	float3 n = normalize(hit_attribute(vertex_normals, attr));
+
+	float2 vertex_uvs[3] = {
+		S16toF32(vertices[indices_sample[0]].tex),
+		S16toF32(vertices[indices_sample[1]].tex),
+		S16toF32(vertices[indices_sample[2]].tex)
+	};
+	float2 tex_coord = hit_attribute2d(vertex_uvs, attr);
+
+	uint2 size;
+	mytexture2.GetDimensions(size.x, size.y);
+	float3 texpaint2 = pow(mytexture2.Load(uint3(tex_coord * size, 0)).rgb, 2.2); // layer base
+	payload.color.rgb = texpaint2.rgb;
+}
+
+[shader("miss")]
+void miss(inout RayPayload payload) {
+	float2 tex_coord = equirect(WorldRayDirection(), constant_buffer.v1.z);
+	uint2 size;
+	mytexture_env.GetDimensions(size.x, size.y);
+	float3 texenv = mytexture_env.Load(uint3(tex_coord * size, 0)).rgb * constant_buffer.v1.x;
+	payload.color = float4(texenv.rgb, -1);
+}

+ 94 - 0
Shaders/src/raytrace_bake_thick.hlsl

@@ -0,0 +1,94 @@
+
+#include "std/rand.hlsl"
+#include "std/math.hlsl"
+
+struct Vertex {
+	uint posxy;
+	uint poszw;
+	uint nor;
+	uint tex;
+};
+
+struct RayGenConstantBuffer {
+	float4 v0; // frame, strength, radius, offset
+	float4 v1;
+	float4 v2;
+	float4 v3;
+	float4 v4;
+};
+
+struct RayPayload {
+	float4 color;
+	float3 ray_origin;
+	float3 ray_dir;
+};
+
+RWTexture2D<float4> render_target : register(u0);
+RaytracingAccelerationStructure scene : register(t0);
+ByteAddressBuffer indices : register(t1);
+StructuredBuffer<Vertex> vertices : register(t2);
+ConstantBuffer<RayGenConstantBuffer> constant_buffer : register(b0);
+
+Texture2D<float4> mytexture0 : register(t3);
+Texture2D<float4> mytexture1 : register(t4);
+Texture2D<float4> mytexture2 : register(t5);
+Texture2D<float4> mytexture_env : register(t6);
+Texture2D<float4> mytexture_sobol : register(t7);
+Texture2D<float4> mytexture_scramble : register(t8);
+Texture2D<float4> mytexture_rank : register(t9);
+
+static const int SAMPLES = 64;
+static uint seed = 0;
+
+[shader("raygeneration")]
+void raygeneration() {
+	float2 xy = DispatchRaysIndex().xy + 0.5f;
+	float4 tex0 = mytexture0.Load(uint3(xy, 0));
+	if (tex0.a == 0.0) {
+		render_target[DispatchRaysIndex().xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
+		return;
+	}
+	float3 pos = tex0.rgb;
+	float3 nor = mytexture1.Load(uint3(xy, 0)).rgb;
+
+	RayPayload payload;
+
+	RayDesc ray;
+	ray.TMin = constant_buffer.v0.w * 0.01;
+	ray.TMax = constant_buffer.v0.z * 10.0;
+	ray.Origin = pos;
+	payload.ray_origin = ray.Origin;
+
+	float3 accum = float3(0, 0, 0);
+
+	for (int i = 0; i < SAMPLES; ++i) {
+		ray.Direction = cos_weighted_hemisphere_direction(-nor, i, seed, constant_buffer.v0.x, mytexture_sobol, mytexture_scramble, mytexture_rank);
+		seed += 1;
+		TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload);
+		accum += payload.color.rgb;
+	}
+
+	accum /= SAMPLES;
+
+	float3 color = float3(render_target[DispatchRaysIndex().xy].xyz);
+	if (constant_buffer.v0.x == 0) {
+		color = accum.xyz;
+	}
+	else {
+		float a = 1.0 / constant_buffer.v0.x;
+		float b = 1.0 - a;
+		color = color * b + accum.xyz * a;
+	}
+	render_target[DispatchRaysIndex().xy] = float4(color.xyz, 1.0f);
+}
+
+[shader("closesthit")]
+void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) {
+	float dist = RayTCurrent() * 2.0;
+	payload.color = float4(dist, dist, dist, 1);
+}
+
+[shader("miss")]
+void miss(inout RayPayload payload) {
+	payload.color = float4(0, 0, 0, 1);
+}

+ 257 - 0
Shaders/src/raytrace_brute.hlsl

@@ -0,0 +1,257 @@
+
+#ifdef _FULL
+#define _EMISSION
+#define _SUBSURFACE
+#define _TRANSLUCENCY
+#endif
+// #define _RENDER
+// #define _ROULETTE
+// #define _TRANSPARENCY
+
+#include "std/rand.hlsl"
+#include "std/attrib.hlsl"
+#include "std/math.hlsl"
+
+struct Vertex {
+	uint posxy;
+	uint poszw;
+	uint nor;
+	uint tex;
+};
+
+struct RayGenConstantBuffer {
+	float4 eye; // xyz, frame
+	float4x4 inv_vp;
+	float4 params; // envstr, envangle
+};
+
+struct RayPayload {
+	float4 color; // rgb, frame
+	float3 ray_origin;
+	float3 ray_dir;
+};
+
+RWTexture2D<float4> render_target : register(u0);
+RaytracingAccelerationStructure scene : register(t0);
+ByteAddressBuffer indices : register(t1);
+StructuredBuffer<Vertex> vertices : register(t2);
+ConstantBuffer<RayGenConstantBuffer> constant_buffer : register(b0);
+
+Texture2D<float4> mytexture0 : register(t3);
+Texture2D<float4> mytexture1 : register(t4);
+Texture2D<float4> mytexture2 : register(t5);
+Texture2D<float4> mytexture_env : register(t6);
+Texture2D<float4> mytexture_sobol : register(t7);
+Texture2D<float4> mytexture_scramble : register(t8);
+Texture2D<float4> mytexture_rank : register(t9);
+
+static const int SAMPLES = 64;
+#ifdef _TRANSLUCENCY
+static const int DEPTH = 6;
+#else
+static const int DEPTH = 3; // Opaque hits
+#endif
+static uint seed = 0;
+#ifdef _TRANSPARENCY
+static const int DEPTH_TRANSPARENT = 16; // Transparent hits
+#endif
+#ifdef _ROULETTE
+static const int rrStart = 2;
+static const float rrProbability = 0.5; // Map to albedo
+#endif
+
+[shader("raygeneration")]
+void raygeneration() {
+	float3 accum = float3(0, 0, 0);
+	for (int j = 0; j < SAMPLES; ++j) {
+		// AA
+		float2 xy = DispatchRaysIndex().xy + 0.5f;
+		xy.x += rand(DispatchRaysIndex().x, DispatchRaysIndex().y, j, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+		seed += 1;
+		xy.y += rand(DispatchRaysIndex().x, DispatchRaysIndex().y, j, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+
+		float2 screen_pos = xy / DispatchRaysDimensions().xy * 2.0 - 1.0;
+		RayDesc ray;
+		ray.TMin = 0.0001;
+		ray.TMax = 10.0;
+		generate_camera_ray(screen_pos, ray.Origin, ray.Direction, constant_buffer.eye.xyz, constant_buffer.inv_vp);
+
+		RayPayload payload;
+		payload.color = float4(1, 1, 1, j);
+
+		#ifdef _TRANSPARENCY
+		int transparentHits = 0;
+		#endif
+
+		for (int i = 0; i < DEPTH; ++i) {
+
+			#ifdef _ROULETTE
+			float rrFactor = 1.0;
+			if (i >= rrStart) {
+				float f = rand(DispatchRaysIndex().x, DispatchRaysIndex().y, j, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+				if (f <= rrProbability) {
+					break;
+				}
+				rrFactor = 1.0 / (1.0 - rrProbability);
+			}
+			#endif
+
+			#ifdef _SUBSURFACE
+			TraceRay(scene, RAY_FLAG_FORCE_OPAQUE | RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 0, 1, 0, ray, payload);
+			#else
+			TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload);
+			#endif
+
+			#ifdef _EMISSION
+			if (payload.color.a  == -2) {
+				accum += payload.color.rgb;
+				break;
+			}
+			#endif
+
+			// Miss
+			if (payload.color.a < 0) {
+				#ifdef _TRANSPARENCY
+				if (payload.color.a == -2 && transparentHits < DEPTH_TRANSPARENT) {
+					payload.color.a = j;
+					transparentHits++;
+					i--;
+				}
+				#endif
+
+				if (i == 0 && constant_buffer.params.x < 0) { // No envmap
+					payload.color.rgb = float3(0.05, 0.05, 0.05);
+				}
+
+				accum += payload.color.rgb;
+				break;
+			}
+
+			#ifdef _ROULETTE
+			payload.color.rgb *= rrFactor;
+			#endif
+
+			ray.Origin = payload.ray_origin;
+			ray.Direction = payload.ray_dir;
+		}
+	}
+
+	float3 color = float3(render_target[DispatchRaysIndex().xy].xyz);
+
+	#ifdef _RENDER
+	float a = 1.0 / (constant_buffer.eye.w + 1);
+	float b = 1.0 - a;
+	color = color * b + (accum.xyz / SAMPLES) * a;
+	render_target[DispatchRaysIndex().xy] = float4(color.xyz, 0.0f);
+	#else
+	if (constant_buffer.eye.w == 0) {
+		color = accum.xyz / SAMPLES;
+	}
+	render_target[DispatchRaysIndex().xy] = float4(lerp(color.xyz, accum.xyz / SAMPLES, 1.0 / 4.0), 0.0f);
+	#endif
+}
+
+[shader("closesthit")]
+void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) {
+	const uint triangleIndexStride = 12; // 3 * 4
+	uint base_index = PrimitiveIndex() * triangleIndexStride;
+	uint3 indices_sample = indices.Load3(base_index);
+
+	float2 vertex_uvs[3] = {
+		S16toF32(vertices[indices_sample[0]].tex),
+		S16toF32(vertices[indices_sample[1]].tex),
+		S16toF32(vertices[indices_sample[2]].tex)
+	};
+	float2 tex_coord = hit_attribute2d(vertex_uvs, attr);
+
+	uint2 size;
+	mytexture0.GetDimensions(size.x, size.y);
+	float4 texpaint0 = mytexture0.Load(uint3(tex_coord * size, 0));
+
+	#ifdef _TRANSPARENCY
+	if (texpaint0.a <= 0.1) {
+		payload.ray_dir = WorldRayDirection();
+		payload.ray_origin = hit_world_position() + payload.ray_dir * 0.0001f;
+		payload.color.a = -2;
+		return;
+	}
+	#endif
+
+	float3 vertex_normals[3] = {
+		float3(S16toF32(vertices[indices_sample[0]].nor), S16toF32(vertices[indices_sample[0]].poszw).y),
+		float3(S16toF32(vertices[indices_sample[1]].nor), S16toF32(vertices[indices_sample[1]].poszw).y),
+		float3(S16toF32(vertices[indices_sample[2]].nor), S16toF32(vertices[indices_sample[2]].poszw).y)
+	};
+	float3 n = normalize(hit_attribute(vertex_normals, attr));
+
+	float4 texpaint1 = mytexture1.Load(uint3(tex_coord * size, 0));
+	float4 texpaint2 = mytexture2.Load(uint3(tex_coord * size, 0));
+	float3 texcolor = pow(texpaint0.rgb, float3(2.2, 2.2, 2.2));
+
+	float3 tangent = float3(0, 0, 0);
+	float3 binormal = float3(0, 0, 0);
+	create_basis(n, tangent, binormal);
+
+	texpaint1.rgb = normalize(texpaint1.rgb * 2.0 - 1.0);
+	texpaint1.g = -texpaint1.g;
+	n = mul(texpaint1.rgb, float3x3(tangent, binormal, n));
+
+	float f = rand(DispatchRaysIndex().x, DispatchRaysIndex().y, payload.color.a, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+
+	#ifdef _TRANSLUCENCY
+	float3 diffuseDir = texpaint0.a < f ?
+		cos_weighted_hemisphere_direction(WorldRayDirection(), payload.color.a, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank) :
+		cos_weighted_hemisphere_direction(n, payload.color.a, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+	#else
+	float3 diffuseDir = cos_weighted_hemisphere_direction(n, payload.color.a, seed, constant_buffer.eye.w, mytexture_sobol, mytexture_scramble, mytexture_rank);
+	#endif
+
+	if (f < 0.5) {
+		#ifdef _TRANSLUCENCY
+		float3 specularDir = texpaint0.a < f * 2 ? WorldRayDirection() : reflect(WorldRayDirection(), n);
+		#else
+		float3 specularDir = reflect(WorldRayDirection(), n);
+		#endif
+
+		payload.ray_dir = lerp(specularDir, diffuseDir, texpaint2.g * texpaint2.g);
+		float3 v = normalize(constant_buffer.eye.xyz - hit_world_position());
+		float dotNV = max(dot(n, v), 0.0);
+		float3 specular = surfaceSpecular(texcolor, texpaint2.b);
+		payload.color.xyz *= envBRDFApprox(specular, texpaint2.g, dotNV);
+	}
+	else {
+		payload.ray_dir = diffuseDir;
+		payload.color.xyz *= surfaceAlbedo(texcolor, texpaint2.b);
+	}
+
+	payload.ray_origin = hit_world_position() + payload.ray_dir * 0.0001f;
+
+	#ifdef _EMISSION
+	if (texpaint1.a == 1.0) { // matid
+		payload.color.xyz *= 100.0f;
+		payload.color.a = -2.0;
+	}
+	#endif
+
+	#ifdef _SUBSURFACE
+	if (texpaint1.a == (254.0f / 255.0f)) {
+		payload.ray_origin += WorldRayDirection() * f;
+	}
+	#endif
+}
+
+[shader("miss")]
+void miss(inout RayPayload payload) {
+
+	#ifdef _EMISSION
+	if (payload.color.a == -2.0) {
+		return;
+	}
+	#endif
+
+	float2 tex_coord = frac(equirect(WorldRayDirection(), constant_buffer.params.y));
+	uint2 size;
+	mytexture_env.GetDimensions(size.x, size.y);
+	float3 texenv = mytexture_env.Load(uint3(tex_coord * size, 0)).rgb * abs(constant_buffer.params.x);
+	payload.color = float4(payload.color.rgb * texenv.rgb, -1);
+}

+ 27 - 0
Shaders/src/std/attrib.hlsl

@@ -0,0 +1,27 @@
+
+#ifndef _ATTRIB_HLSL_
+#define _ATTRIB_HLSL_
+
+float2 S16toF32(uint val) {
+	int a = (int)(val << 16) >> 16;
+	int b = (int)(val & 0xffff0000) >> 16;
+	return float2(a, b) / 32767.0f;
+}
+
+float3 hit_world_position() {
+	return WorldRayOrigin() + RayTCurrent() * WorldRayDirection();
+}
+
+float3 hit_attribute(float3 vertexAttribute[3], BuiltInTriangleIntersectionAttributes attr) {
+	return vertexAttribute[0] +
+		attr.barycentrics.x * (vertexAttribute[1] - vertexAttribute[0]) +
+		attr.barycentrics.y * (vertexAttribute[2] - vertexAttribute[0]);
+}
+
+float2 hit_attribute2d(float2 vertexAttribute[3], BuiltInTriangleIntersectionAttributes attr) {
+	return vertexAttribute[0] +
+		attr.barycentrics.x * (vertexAttribute[1] - vertexAttribute[0]) +
+		attr.barycentrics.y * (vertexAttribute[2] - vertexAttribute[0]);
+}
+
+#endif

+ 34 - 0
Shaders/src/std/dof.hlsl

@@ -0,0 +1,34 @@
+
+#ifndef _DOF_HLSL_
+#define _DOF_HLSL_
+
+float2 calculate_concentric_sample_disk(float u, float v) {
+	const float PI = 3.1415926535;
+	// Maps a (u,v) in [0, 1)^2 to a 2D unit disk centered at (0,0). Based on PBRT.
+	float2 u_offset = 2.0f * float2(u, v) - float2(1, 1);
+	if (u_offset.x == 0 && u_offset.y == 0) {
+		return float2(0.0f, 0.0f);
+	}
+	float theta, r;
+	if (abs(u_offset.x) > abs(u_offset.y)) {
+		r = u_offset.x;
+		theta = PI / 4 * (u_offset.y / u_offset.x);
+	}
+	else {
+		r = u_offset.y;
+		theta = (PI / 2) - (PI / 4 * (u_offset.x / u_offset.y));
+	}
+	return r * float2(cos(theta), sin(theta));
+}
+
+void generate_camera_ray_dof(float2 screen_pos, inout float3 ray_origin, inout float3 ray_dir, float rand0, float rand1) {
+	float lens_rad = 0.005f;
+	float focal_dist = 0.4f;
+	float3 plens = float3(lens_rad * calculate_concentric_sample_disk(rand0, rand1), 0.0f);
+	float ft = focal_dist / abs(ray_dir.z);
+	float3 pfocus = ray_dir * ft;
+	ray_origin += plens;
+	ray_dir = normalize(pfocus - plens);
+}
+
+#endif

+ 65 - 0
Shaders/src/std/math.hlsl

@@ -0,0 +1,65 @@
+
+#ifndef _MATH_HLSL_
+#define _MATH_HLSL_
+
+#include "rand.hlsl"
+
+void create_basis(float3 normal, out float3 tangent, out float3 binormal) {
+	// tangent = abs(normal.x) > abs(normal.y) ?
+	// 	normalize(float3(0.0, normal.z, -normal.y)) :
+	// 	normalize(float3(-normal.z, 0.0, normal.x));
+	// binormal = cross(normal, tangent);
+	float3 v1 = cross(normal, float3(0.0, 0.0, 1.0));
+	float3 v2 = cross(normal, float3(0.0, 1.0, 0.0));
+	tangent = length(v1) > length(v2) ? v1 : v2;
+	binormal = cross(tangent, normal);
+}
+
+void generate_camera_ray(float2 screen_pos, out float3 ray_origin, out float3 ray_dir, float3 eye, float4x4 inv_vp) {
+	screen_pos.y = -screen_pos.y;
+	float4 world = mul(float4(screen_pos, 0, 1), inv_vp);
+	world.xyz /= world.w;
+	ray_origin = eye;
+	ray_dir = normalize(world.xyz - ray_origin);
+}
+
+float2 equirect(float3 normal, float angle) {
+	const float PI = 3.1415926535;
+	const float PI2 = PI * 2.0;
+	float phi = acos(normal.z);
+	float theta = atan2(-normal.y, normal.x) + PI + angle;
+	return float2(theta / PI2, phi / PI);
+}
+
+float3 cos_weighted_hemisphere_direction(float3 n, uint sample, uint seed, int frame, Texture2D<float4> sobol, Texture2D<float4> scramble, Texture2D<float4> rank) {
+	const float PI = 3.1415926535;
+	const float PI2 = PI * 2.0;
+	float f0 = rand(DispatchRaysIndex().x, DispatchRaysIndex().y, sample, seed, frame, sobol, scramble, rank);
+	float f1 = rand(DispatchRaysIndex().x, DispatchRaysIndex().y, sample, seed + 1, frame, sobol, scramble, rank);
+	float z = f0 * 2.0f - 1.0f;
+	float a = f1 * PI2;
+	float r = sqrt(1.0f - z * z);
+	float x = r * cos(a);
+	float y = r * sin(a);
+	return normalize(n + float3(x, y, z));
+}
+
+float3 surfaceAlbedo(const float3 baseColor, const float metalness) {
+	return lerp(baseColor, float3(0.0, 0.0, 0.0), metalness);
+}
+
+float3 surfaceSpecular(const float3 baseColor, const float metalness) {
+	return lerp(float3(0.04, 0.04, 0.04), baseColor, metalness);
+}
+
+// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
+float3 envBRDFApprox(float3 specular, float roughness, float dotNV) {
+	const float4 c0 = float4(-1, -0.0275, -0.572, 0.022);
+	const float4 c1 = float4(1, 0.0425, 1.04, -0.04);
+	float4 r = roughness * c0 + c1;
+	float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;
+	float2 ab = float2(-1.04, 1.04) * a004 + r.zw;
+	return specular * ab.x + ab.y;
+}
+
+#endif

+ 34 - 0
Shaders/src/std/rand.hlsl

@@ -0,0 +1,34 @@
+
+#ifndef _RAND_HLSL_
+#define _RAND_HLSL_
+
+// A Low-Discrepancy Sampler that Distributes Monte Carlo Errors as a Blue Noise in Screen Space
+// Eric Heitz, Laurent Belcour, Victor Ostromoukhov, David Coeurjolly and Jean-Claude Iehl
+// https://eheitzresearch.wordpress.com/762-2/
+float rand(int pixel_i, int pixel_j, int sampleIndex, int sampleDimension, int frame, Texture2D<float4> sobol, Texture2D<float4> scramble, Texture2D<float4> rank) {
+	// wrap arguments
+	pixel_i += frame * 9;
+	pixel_j += frame * 11;
+	pixel_i = pixel_i & 127;
+	pixel_j = pixel_j & 127;
+	sampleIndex = sampleIndex & 255;
+	sampleDimension = sampleDimension & 255;
+
+	// xor index based on optimized ranking
+	int i = sampleDimension + (pixel_i + pixel_j*128)*8;
+	int rankedSampleIndex = sampleIndex ^ int(rank.Load(uint3(i % 128, uint(i / 128), 0)).r * 255);
+
+	// fetch value in sequence
+	i = sampleDimension + rankedSampleIndex*256;
+	int value = int(sobol.Load(uint3(i % 256, uint(i / 256), 0)).r * 255);
+
+	// If the dimension is optimized, xor sequence value based on optimized scrambling
+	i = (sampleDimension%8) + (pixel_i + pixel_j*128)*8;
+	value = value ^ int(scramble.Load(uint3(i % 128, uint(i / 128), 0)).r * 255);
+
+	// convert to float and return
+	float v = (0.5f+value)/256.0f;
+	return v;
+}
+
+#endif

+ 403 - 0
Sources/arm/format/BlendParser.hx

@@ -0,0 +1,403 @@
+// .blend file parser
+// https://github.com/armory3d/blend
+// Reference:
+// https://github.com/fschutt/mystery-of-the-blend-backup
+// https://web.archive.org/web/20170630054951/http://www.atmind.nl/blender/mystery_ot_blend.html
+// Usage:
+// var bl = new BlendParser(blob:kha.Blob);
+// trace(bl.dir("Scene"));
+// var scenes = bl.get("Scene");
+// trace(scenes[0].get("id").get("name"));
+package arm.format;
+
+// https://github.com/Kode/Kha
+import kha.Blob;
+
+class BlendParser {
+
+	public var pos: Int;
+	var blob: Blob;
+
+	// Header
+	public var version: String;
+	public var pointerSize: Int;
+	public var littleEndian: Bool;
+	// Data
+	public var blocks: Array<Block> = [];
+	public var dna: Dna = null;
+	public var map = new Map<Int, Map<Int, Block>>(); // Map blocks by memory address
+
+	public function new(blob: Blob) {
+		this.blob = blob;
+		this.pos = 0;
+		if (readChars(7) != "BLENDER") {
+			this.blob = Blob.fromBytes(haxe.io.Bytes.ofData(Krom.inflate(blob.toBytes().getData(), false)));
+			this.pos = 0;
+			if (readChars(7) != "BLENDER") return;
+		}
+		parse();
+	}
+
+	public function dir(type: String): Array<String> {
+		// Return structure fields
+		var typeIndex = getTypeIndex(dna, type);
+		if (typeIndex == -1) return null;
+		var ds = getStruct(dna, typeIndex);
+		var fields: Array<String> = [];
+		for (i in 0...ds.fieldNames.length) {
+			var nameIndex = ds.fieldNames[i];
+			var typeIndex = ds.fieldTypes[i];
+			fields.push(dna.types[typeIndex] + " " + dna.names[nameIndex]);
+		}
+		return fields;
+	}
+
+	public function get(type: String): Array<Handle> {
+		if (dna == null) return null;
+		// Return all structures of type
+		var typeIndex = getTypeIndex(dna, type);
+		if (typeIndex == -1) return null;
+		var ds = getStruct(dna, typeIndex);
+		var handles: Array<Handle> = [];
+		for (b in blocks) {
+			if (dna.structs[b.sdnaIndex].type == typeIndex) {
+				var h = new Handle();
+				handles.push(h);
+				h.block = b;
+				h.ds = ds;
+			}
+		}
+		return handles;
+	}
+
+	public static function getStruct(dna: Dna, typeIndex: Int): DnaStruct {
+		for (ds in dna.structs) if (ds.type == typeIndex) return ds;
+		return null;
+	}
+
+	public static function getTypeIndex(dna: Dna, type: String): Int {
+		for (i in 0...dna.types.length) if (type == dna.types[i]) { return i; }
+		return -1;
+	}
+
+	function parse() {
+		// Pointer size: _ 32bit, - 64bit
+		pointerSize = readChar() == "_" ? 4 : 8;
+
+		// v - little endian, V - big endian
+		littleEndian = readChar() == "v";
+		if (littleEndian) {
+			read16 = read16LE;
+			read32 = read32LE;
+			read64 = read64LE;
+			readf32 = readf32LE;
+		}
+		else {
+			read16 = read16BE;
+			read32 = read32BE;
+			read64 = read64BE;
+			readf32 = readf32BE;
+		}
+
+		version = readChars(3);
+
+		// Reading file blocks
+		// Header - data
+		while (pos < blob.length) {
+			align();
+			var b = new Block();
+
+			// Block type
+			b.code = readChars(4);
+			if (b.code == "ENDB") break;
+
+			blocks.push(b);
+			b.blend = this;
+
+			// Total block length
+			b.size = read32();
+
+			// Memory address
+			var addr = readPointer();
+			if (!map.exists(addr.high)) map.set(addr.high, new Map<Int, Block>());
+			map.get(addr.high).set(addr.low, b);
+
+			// Index of dna struct contained in this block
+			b.sdnaIndex = read32();
+
+			// Number of dna structs in this block
+			b.count = read32();
+
+			b.pos = pos;
+
+			// This block stores dna structures
+			if (b.code == "DNA1") {
+				dna = new Dna();
+
+				var id = readChars(4); // SDNA
+				var nameId = readChars(4); // NAME
+				var namesCount = read32();
+				for (i in 0...namesCount) {
+					dna.names.push(readString());
+				}
+				align();
+
+				var typeId = readChars(4); // TYPE
+				var typesCount = read32();
+				for (i in 0...typesCount) {
+					dna.types.push(readString());
+				}
+				align();
+
+				var lenId = readChars(4); // TLEN
+				for (i in 0...typesCount) {
+					dna.typesLength.push(read16());
+				}
+				align();
+
+				var structId = readChars(4); // STRC
+				var structCount = read32();
+				for (i in 0...structCount) {
+					var ds = new DnaStruct();
+					dna.structs.push(ds);
+					ds.dna = dna;
+					ds.type = read16();
+					var fieldCount = read16();
+					if (fieldCount > 0) {
+						ds.fieldTypes = [];
+						ds.fieldNames = [];
+						for (j in 0...fieldCount) {
+							ds.fieldTypes.push(read16());
+							ds.fieldNames.push(read16());
+						}
+					}
+				}
+			}
+			else {
+				pos += b.size;
+			}
+		}
+	}
+
+	function align() {
+		// 4 bytes aligned
+		var mod = pos % 4;
+		if (mod > 0) pos += 4 - mod;
+	}
+
+	public function read8(): Int {
+		var i = blob.readU8(pos);
+		pos += 1;
+		return i;
+	}
+
+	public var read16: Void->Int;
+	public var read32: Void->Int;
+	public var read64: Void->haxe.Int64;
+	public var readf32: Void->Float;
+
+	function read16LE(): Int {
+		var i = blob.readS16LE(pos);
+		pos += 2;
+		return i;
+	}
+
+	function read32LE(): Int {
+		var i = blob.readS32LE(pos);
+		pos += 4;
+		return i;
+	}
+
+	function read64LE(): haxe.Int64 {
+		return haxe.Int64.make(read32(), read32());
+	}
+
+	function readf32LE(): Float {
+		var f = blob.readF32LE(pos);
+		pos += 4;
+		return f;
+	}
+
+	function read16BE(): Int {
+		var i = blob.readS16BE(pos);
+		pos += 2;
+		return i;
+	}
+
+	function read32BE(): Int {
+		var i = blob.readS32BE(pos);
+		pos += 4;
+		return i;
+	}
+
+	function read64BE(): haxe.Int64 {
+		return haxe.Int64.make(read32(), read32());
+	}
+
+	function readf32BE(): Float {
+		var f = blob.readF32BE(pos);
+		pos += 4;
+		return f;
+	}
+
+	public function read8array(len: Int): haxe.io.Int32Array {
+		var ar = new haxe.io.Int32Array(len);
+		for (i in 0...len) ar[i] = read8();
+		return ar;
+	}
+
+	public function read16array(len: Int): haxe.io.Int32Array {
+		var ar = new haxe.io.Int32Array(len);
+		for (i in 0...len) ar[i] = read16();
+		return ar;
+	}
+
+	public function read32array(len: Int): haxe.io.Int32Array {
+		var ar = new haxe.io.Int32Array(len);
+		for (i in 0...len) ar[i] = read32();
+		return ar;
+	}
+
+	public function readf32array(len: Int): kha.arrays.Float32Array {
+		var ar = new kha.arrays.Float32Array(len);
+		for (i in 0...len) ar[i] = readf32();
+		return ar;
+	}
+
+	public function readString(): String {
+		var s = "";
+		while (true) {
+			var ch = read8();
+			if (ch == 0) break;
+			s += String.fromCharCode(ch);
+		}
+		return s;
+	}
+
+	public function readChars(len: Int): String {
+		var s = "";
+		for (i in 0...len) s += readChar();
+		return s;
+	}
+
+	public function readChar(): String {
+		return String.fromCharCode(read8());
+	}
+
+	public function readPointer(): haxe.Int64 {
+		return pointerSize == 4 ? haxe.Int64.ofInt(read32()) : read64();
+	}
+}
+
+class Block {
+	public var blend: BlendParser;
+	public var code: String;
+	public var size: Int;
+	public var sdnaIndex: Int;
+	public var count: Int;
+	public var pos: Int; // Byte pos of data start in blob
+	public function new() {}
+}
+
+class Dna {
+	public var names: Array<String> = [];
+	public var types: Array<String> = [];
+	public var typesLength: Array<Int> = [];
+	public var structs: Array<DnaStruct> = [];
+	public function new() {}
+}
+
+class DnaStruct {
+	public var dna: Dna;
+	public var type: Int; // Index in dna.types
+	public var fieldTypes: Array<Int>; // Index in dna.types
+	public var fieldNames: Array<Int>; // Index in dna.names
+	public function new() {}
+}
+
+class Handle {
+	public var block: Block;
+	public var offset: Int = 0; // Block data bytes offset
+	public var ds: DnaStruct;
+	public function new() {}
+	function getSize(index: Int): Int {
+		var nameIndex = ds.fieldNames[index];
+		var typeIndex = ds.fieldTypes[index];
+		var dna = ds.dna;
+		var n = dna.names[nameIndex];
+		var size = 0;
+		if (n.indexOf("*") >= 0) size = block.blend.pointerSize;
+		else size = dna.typesLength[typeIndex];
+		if (n.indexOf("[") > 0) size *= getArrayLen(n);
+		return size;
+	}
+	function baseName(s: String): String {
+		while (s.charAt(0) == "*") s = s.substring(1, s.length);
+		if (s.charAt(s.length - 1) == "]") s = s.substring(0, s.indexOf("["));
+		return s;
+	}
+	function getArrayLen(s: String): Int {
+		return Std.parseInt(s.substring(s.indexOf("[") + 1, s.indexOf("]")));
+	}
+	public function get(name: String, index = 0, asType: String = null, arrayLen = 0): Dynamic {
+		// Return raw type or structure
+		var dna = ds.dna;
+		for (i in 0...ds.fieldNames.length) {
+			var nameIndex = ds.fieldNames[i];
+			var dnaName = dna.names[nameIndex];
+			if (name == baseName(dnaName)) {
+				var typeIndex = ds.fieldTypes[i];
+				var type = dna.types[typeIndex];
+				var newOffset = offset;
+				for (j in 0...i) newOffset += getSize(j);
+				// Cast void* to type
+				if (asType != null) {
+					for (i in 0...dna.types.length) {
+						if (dna.types[i] == asType) { typeIndex = i; break; }
+					}
+				}
+				// Raw type
+				if (typeIndex < 12) {
+					var blend = block.blend;
+					blend.pos = block.pos + newOffset;
+					var isArray = dnaName.charAt(dnaName.length - 1) == "]";
+					var len = isArray ? (arrayLen > 0 ? arrayLen : getArrayLen(dnaName)) : 1;
+					switch (type) {
+					case "int": return isArray ? blend.read32array(len) : blend.read32();
+					case "char": return isArray ? blend.readString() : blend.read8();
+					case "uchar": return isArray ? blend.read8array(len) : blend.read8();
+					case "short": return isArray ? blend.read16array(len) : blend.read16();
+					case "ushort": return isArray ? blend.read16array(len) : blend.read16();
+					case "float": return isArray ? blend.readf32array(len) : blend.readf32();
+					case "double": return 0; //blend.readf64();
+					case "long": return isArray ? blend.read32array(len) : blend.read32();
+					case "ulong": return isArray ? blend.read32array(len) : blend.read32();
+					case "int64_t": return blend.read64();
+					case "uint64_t": return blend.read64();
+					case "void": return 0;
+					}
+				}
+				// Structure
+				var h = new Handle();
+				h.ds = BlendParser.getStruct(dna, typeIndex);
+				var isPointer = dnaName.charAt(0) == "*";
+				if (isPointer) {
+					block.blend.pos = block.pos + newOffset;
+					var addr = block.blend.readPointer();
+					if (block.blend.map.exists(addr.high)) {
+						h.block = block.blend.map.get(addr.high).get(addr.low);
+					}
+					else h.block = block;
+					h.offset = 0;
+				}
+				else {
+					h.block = block;
+					h.offset = newOffset;
+				}
+				h.offset += dna.typesLength[typeIndex] * index;
+				return h;
+			}
+		}
+		return null;
+	}
+}

+ 314 - 0
Sources/arm/format/ExrWriter.hx

@@ -0,0 +1,314 @@
+// Based on miniexr.cpp - public domain - 2013 Aras Pranckevicius / Unity Technologies
+// https://github.com/aras-p/miniexr
+// https://www.openexr.com/documentation/openexrfilelayout.pdf
+package arm.format;
+
+class ExrWriter {
+
+	public function new(out: haxe.io.BytesOutput, width: Int, height: Int, src: haxe.io.Bytes, bits = 16, type = 1, off = 0) {
+		out.writeByte(0x76); // magic
+		out.writeByte(0x2f);
+		out.writeByte(0x31);
+		out.writeByte(0x01);
+		out.writeByte(2); // version, scanline
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeString("channels");
+		out.writeByte(0);
+		out.writeString("chlist");
+		out.writeByte(0);
+
+		out.writeByte(55);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		var attrib = bits == 16 ? 1 : 2; // half, float
+
+		out.writeByte("B".code); // B
+		out.writeByte(0);
+
+		out.writeByte(attrib);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte("G".code); // G
+		out.writeByte(0);
+
+		out.writeByte(attrib);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte("R".code); // R
+		out.writeByte(0);
+
+		out.writeByte(attrib);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+
+		out.writeString("compression");
+		out.writeByte(0);
+		out.writeString("compression");
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0); // no compression
+
+		out.writeString("dataWindow");
+		out.writeByte(0);
+		out.writeString("box2i");
+		out.writeByte(0);
+
+		out.writeByte(16);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		var ww = width - 1;
+		var hh = height - 1;
+
+		out.writeByte(ww & 0xff);
+		out.writeByte((ww >> 8) & 0xff);
+		out.writeByte((ww >> 16) & 0xff);
+		out.writeByte((ww >> 24) & 0xff);
+
+		out.writeByte(hh & 0xff);
+		out.writeByte((hh >> 8) & 0xff);
+		out.writeByte((hh >> 16) & 0xff);
+		out.writeByte((hh >> 24) & 0xff);
+
+		out.writeString("displayWindow");
+		out.writeByte(0);
+		out.writeString("box2i");
+		out.writeByte(0);
+
+		out.writeByte(16);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(ww & 0xff);
+		out.writeByte((ww >> 8) & 0xff);
+		out.writeByte((ww >> 16) & 0xff);
+		out.writeByte((ww >> 24) & 0xff);
+
+		out.writeByte(hh & 0xff);
+		out.writeByte((hh >> 8) & 0xff);
+		out.writeByte((hh >> 16) & 0xff);
+		out.writeByte((hh >> 24) & 0xff);
+
+		out.writeString("lineOrder");
+		out.writeByte(0);
+		out.writeString("lineOrder");
+		out.writeByte(0);
+
+		out.writeByte(1);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0); // increasing Y
+
+		out.writeString("pixelAspectRatio");
+		out.writeByte(0);
+		out.writeString("float");
+		out.writeByte(0);
+
+		out.writeByte(4);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0); // 1.0f
+		out.writeByte(0);
+		out.writeByte(0x80);
+		out.writeByte(0x3f);
+
+		out.writeString("screenWindowCenter");
+		out.writeByte(0);
+
+		out.writeString("v2f");
+		out.writeByte(0);
+
+		out.writeByte(8);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeString("screenWindowWidth");
+		out.writeByte(0);
+
+		out.writeString("float");
+		out.writeByte(0);
+
+		out.writeByte(4);
+		out.writeByte(0);
+		out.writeByte(0);
+		out.writeByte(0);
+
+		out.writeByte(0); // 1.0f
+		out.writeByte(0);
+		out.writeByte(0x80);
+		out.writeByte(0x3f);
+
+		out.writeByte(0); // end of header
+
+		var channels = 4;
+		var byteSize = bits == 16 ? 2 : 4;
+		var kHeaderSize = out.length;
+		var kScanlineTableSize = 8 * height;
+		var pixelRowSize = width * 3 * byteSize;
+		var fullRowSize = pixelRowSize + 8;
+
+		// line offset table
+		var ofs = kHeaderSize + kScanlineTableSize;
+		for (y in 0...height) {
+			out.writeByte(ofs & 0xff);
+			out.writeByte((ofs >> 8) & 0xff);
+			out.writeByte((ofs >> 16) & 0xff);
+			out.writeByte((ofs >> 24) & 0xff);
+			out.writeByte(0);
+			out.writeByte(0);
+			out.writeByte(0);
+			out.writeByte(0);
+			ofs += fullRowSize;
+		}
+
+		// scanline data
+		var stride = channels * byteSize;
+		var pos = 0;
+
+		function writeLine16(bytePos: Int) {
+			for (x in 0...width) {
+				out.writeByte(src.get(bytePos    ));
+				out.writeByte(src.get(bytePos + 1));
+				bytePos += stride;
+			}
+		}
+
+		function writeLine32(bytePos: Int) {
+			for (x in 0...width) {
+				out.writeByte(src.get(bytePos    ));
+				out.writeByte(src.get(bytePos + 1));
+				out.writeByte(src.get(bytePos + 2));
+				out.writeByte(src.get(bytePos + 3));
+				bytePos += stride;
+			}
+		}
+
+		var writeLine = bits == 16 ? writeLine16 : writeLine32;
+
+		function writeBGR(off: Int) {
+			writeLine(pos + byteSize * 2);
+			writeLine(pos + byteSize);
+			writeLine(pos    );
+		}
+
+		function writeSingle(off: Int) {
+			writeLine(pos + off * byteSize);
+			writeLine(pos + off * byteSize);
+			writeLine(pos + off * byteSize);
+		}
+
+		var writeData = type == 1 ? writeBGR : writeSingle;
+
+		for (y in 0...height) {
+			// coordinate
+			out.writeByte(y & 0xff);
+			out.writeByte((y >> 8) & 0xff);
+			out.writeByte((y >> 16) & 0xff);
+			out.writeByte((y >> 24) & 0xff);
+			// data size
+			out.writeByte(pixelRowSize & 0xff);
+			out.writeByte((pixelRowSize >> 8) & 0xff);
+			out.writeByte((pixelRowSize >> 16) & 0xff);
+			out.writeByte((pixelRowSize >> 24) & 0xff);
+			// data
+			writeData(off);
+			pos += width * stride;
+		}
+	}
+}

+ 212 - 0
Sources/arm/format/FbxBinaryParser.hx

@@ -0,0 +1,212 @@
+package arm.format;
+
+import arm.format.FbxLibrary;
+
+class FbxBinaryParser {
+
+	var pos: Int;
+	var blob: kha.Blob;
+
+	var version: Int;
+	var is64: Bool;
+	var root: FbxNode;
+
+	function new(blob: kha.Blob) {
+		this.blob = blob;
+		pos = 0;
+		var magic = "Kaydara FBX Binary\x20\x20\x00\x1a\x00";
+		var valid = readChars(magic.length) == magic;
+		if (!valid) return;
+		var version = read32();
+		is64 = version >= 7500;
+		root = {
+			name : "Root",
+			props : [PInt(0), PString("Root"), PString("Root")],
+			childs : parseNodes()
+		};
+	}
+
+	public static function parse(blob: kha.Blob): FbxNode {
+		return new FbxBinaryParser(blob).root;
+	}
+
+	function parseArray(readVal: Void->Dynamic, isFloat = false): FbxProp {
+		var len = read32();
+		var encoding = read32();
+		var compressedLen = read32();
+		var endPos = pos + compressedLen;
+		var _blob = blob;
+		if (encoding != 0) {
+			pos += 2;
+			var input = blob.sub(pos, compressedLen).toBytes().getData();
+			blob = kha.Blob.fromBytes(haxe.io.Bytes.ofData(Krom.inflate(input, true)));
+			pos = 0;
+		}
+		var res = isFloat ? parseArrayf(readVal, len) : parseArrayi(readVal, len);
+		if (encoding != 0) {
+			pos = endPos;
+			blob = _blob;
+		}
+		return res;
+	}
+
+	function parseArrayf(readVal: Void->Dynamic, len: Int): FbxProp {
+		var res: Array<Float> = [];
+		for (i in 0...len) res.push(readVal());
+		return PFloats(res);
+	}
+
+	function parseArrayi(readVal: Void->Dynamic, len: Int): FbxProp {
+		var res: Array<Int> = [];
+		for (i in 0...len) res.push(readVal());
+		return PInts(res);
+	}
+
+	function parseProp(): FbxProp {
+		switch (readChar()) {
+		case "C":
+			return PString(readChar());
+		case "Y":
+			return PInt(read16());
+		case "I":
+			return PInt(read32());
+		case "L":
+			return PInt(read64());
+		case "F":
+			return PFloat(readf32());
+		case "D":
+			return PFloat(readf64());
+		case "f":
+			return parseArray(readf32, true);
+		case "d":
+			return parseArray(readf64, true);
+		case "l":
+			return parseArray(read64);
+		case "i":
+			return parseArray(read32);
+		case "b":
+			return parseArray(readBool);
+		case "S":
+			var len = read32();
+			return PString(readChars(len));
+		case "R":
+			var b = readBytes(read32());
+			return null;
+		default:
+			return null;
+		}
+	}
+
+	function parseNode(): FbxNode {
+		var endPos = 0;
+		var numProps = 0;
+		var propListLen = 0;
+
+		if (is64) {
+			endPos = read64();
+			numProps = read64();
+			propListLen = read64();
+		}
+		else {
+			endPos = read32();
+			numProps = read32();
+			propListLen = read32();
+		}
+
+		var nameLen = read8();
+		var name = nameLen == 0 ? "" : readChars(nameLen);
+		if (endPos == 0) return null; // Null node
+
+		var props: Array<FbxProp> = null;
+		if (numProps > 0) props = [];
+		for (i in 0...numProps) props.push(parseProp());
+
+		var childs: Array<FbxNode> = null;
+		var listLen = endPos - pos;
+		if (listLen > 0) {
+			childs = [];
+			while (true) {
+				var nested = parseNode();
+				nested == null ? break : childs.push(nested);
+			}
+		}
+		return { name: name, props: props, childs: childs };
+	}
+
+	function parseNodes(): Array<FbxNode> {
+		var nodes = [];
+		while (true) {
+			var n = parseNode();
+			n == null ? break : nodes.push(n);
+		}
+		return nodes;
+	}
+
+	function read8(): Int {
+		var i = blob.readU8(pos);
+		pos += 1;
+		return i;
+	}
+
+	function read16(): Int {
+		var i = blob.readS16LE(pos);
+		pos += 2;
+		return i;
+	}
+
+	function read32(): Int {
+		var i = blob.bytes.getInt32(pos);
+		// var i = blob.readS32LE(pos); // Result sometimes off by 1?
+		pos += 4;
+		return i;
+	}
+
+	function read64(): Int {
+		var i1 = read32();
+		var i2 = read32();
+		// return cast haxe.Int64.make(i1, i2);
+		return i1;
+	}
+
+	function readf32(): Float {
+		var f = blob.readF32LE(pos);
+		pos += 4;
+		return f;
+	}
+
+	function readf64(): Float {
+		var i1 = read32();
+		var i2 = read32();
+		return haxe.io.FPHelper.i64ToDouble(i1, i2); // LE
+	}
+
+	function readString(): String {
+		var s = "";
+		while (true) {
+			var ch = read8();
+			if (ch == 0) break;
+			s += String.fromCharCode(ch);
+		}
+		return s;
+	}
+
+	function readChars(len: Int): String {
+		var s = "";
+		for (i in 0...len) s += readChar();
+		return s;
+	}
+
+	function readChar(): String {
+		return String.fromCharCode(read8());
+	}
+
+	function readBool(): Int {
+		return read8(); // return read8() == 1;
+	}
+
+	function readBytes(len: Int): kha.Blob {
+		var b = blob.sub(pos, len);
+		pos += len;
+		return b;
+	}
+}

+ 869 - 0
Sources/arm/format/FbxLibrary.hx

@@ -0,0 +1,869 @@
+// Adapted fbx parser originally developed by Nicolas Cannasse
+// https://github.com/HeapsIO/heaps/tree/master/hxd/fmt/fbx
+// The MIT License (MIT)
+
+// Copyright (c) 2013 Nicolas Cannasse
+
+// 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.
+package arm.format;
+
+enum FbxProp {
+	PInt( v : Int );
+	PFloat( v : Float );
+	PString( v : String );
+	PIdent( i : String );
+	PInts( v : Array<Int> );
+	PFloats( v : Array<Float> );
+}
+
+typedef FbxNode = {
+	var name : String;
+	var props : Array<FbxProp>;
+	var childs : Array<FbxNode>;
+}
+
+class FbxTools {
+
+	public static function get( n : FbxNode, path : String, opt = false ) {
+		var parts = path.split(".");
+		var cur = n;
+		for( p in parts ) {
+			var found = false;
+			for( c in cur.childs )
+				if( c.name == p ) {
+					cur = c;
+					found = true;
+					break;
+				}
+			if( !found ) {
+				if( opt )
+					return null;
+				throw n.name + " does not have " + path+" ("+p+" not found)";
+			}
+		}
+		return cur;
+	}
+
+	public static function getAll( n : FbxNode, path : String ) {
+		var parts = path.split(".");
+		var cur = [n];
+		for( p in parts ) {
+			var out = [];
+			for( n in cur )
+				for( c in n.childs )
+					if( c.name == p )
+						out.push(c);
+			cur = out;
+			if( cur.length == 0 )
+				return cur;
+		}
+		return cur;
+	}
+
+	public static function getInts( n : FbxNode ) {
+		if( n.props.length != 1 )
+			throw n.name + " has " + n.props + " props";
+		switch( n.props[0] ) {
+		case PInts(v):
+			return v;
+		default:
+			throw n.name + " has " + n.props + " props";
+		}
+	}
+
+	public static function getFloats( n : FbxNode ) {
+		if( n.props.length != 1 )
+			throw n.name + " has " + n.props + " props";
+		switch( n.props[0] ) {
+		case PFloats(v):
+			return v;
+		case PInts(i):
+			var fl = new Array<Float>();
+			for( x in i )
+				fl.push(x);
+			n.props[0] = PFloats(fl); // keep data synchronized
+			// this is necessary for merging geometries since we are pushing directly into the
+			// float buffer
+			return fl;
+		default:
+			throw n.name + " has " + n.props + " props";
+		}
+	}
+
+	public static function hasProp( n : FbxNode, p : FbxProp ) {
+		for( p2 in n.props )
+			if( Type.enumEq(p, p2) )
+				return true;
+		return false;
+	}
+
+	static inline function idToInt( f : Float ) {
+		#if (neko || hl)
+		// ids are unsigned
+		f %= 4294967296.;
+		if( f >= 2147483648. )
+			f -= 4294967296.;
+		else if( f < -2147483648. )
+			f += 4294967296.;
+		#end
+		return Std.int(f);
+	}
+
+	public static function toInt( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PInt(v): v;
+		case PFloat(f): idToInt(f);
+		default: throw "Invalid prop " + n;
+		}
+	}
+
+	public static function toFloat( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PInt(v): v * 1.0;
+		case PFloat(v): v;
+		default: throw "Invalid prop " + n;
+		}
+	}
+
+	public static function toString( n : FbxProp ) {
+		if( n == null ) throw "null prop";
+		return switch( n ) {
+		case PString(v): v;
+		default: throw "Invalid prop " + n;
+		}
+	}
+
+	public static function getId( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[0] ) {
+		case PInt(id): id;
+		case PFloat(id) : idToInt(id);
+		default: throw n.name + " is not an object " + n.props;
+		}
+	}
+
+	public static function getName( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[1] ) {
+		case PString(n): n.split("::").pop();
+		default: throw n.name + " is not an object";
+		}
+	}
+
+	public static function getType( n : FbxNode ) {
+		if( n.props.length != 3 )
+			throw n.name + " is not an object";
+		return switch( n.props[2] ) {
+		case PString(n): n;
+		default: throw n.name + " is not an object";
+		}
+	}
+
+}
+
+private enum Token {
+	TIdent( s : String );
+	TNode( s : String );
+	TInt( s : String );
+	TFloat( s : String );
+	TString( s : String );
+	TLength( v : Int );
+	TBraceOpen;
+	TBraceClose;
+	TColon;
+	TEof;
+}
+
+class Parser {
+
+	var line : Int;
+	var buf : String;
+	var pos : Int;
+	var token : Null<Token>;
+
+	function new() {
+	}
+
+	function parseText( str ) : FbxNode {
+		this.buf = str;
+		this.pos = 0;
+		this.line = 1;
+		token = null;
+		return {
+			name : "Root",
+			props : [PInt(0),PString("Root"),PString("Root")],
+			childs : parseNodes(),
+		};
+	}
+
+	function parseNodes() {
+		var nodes = [];
+		while( true ) {
+			switch( peek() ) {
+			case TEof, TBraceClose:
+				return nodes;
+			default:
+			}
+			nodes.push(parseNode());
+		}
+		return nodes;
+	}
+
+	function parseNode() : FbxNode {
+		var t = next();
+		var name = switch( t ) {
+		case TNode(n): n;
+		default: unexpected(t);
+		};
+		var props = [], childs = null;
+		while( true ) {
+			t = next();
+			switch( t ) {
+			case TFloat(s):
+				props.push(PFloat(Std.parseFloat(s)));
+			case TInt(s):
+				props.push(PInt(Std.parseInt(s)));
+			case TString(s):
+				props.push(PString(s));
+			case TIdent(s):
+				props.push(PIdent(s));
+			case TBraceOpen, TBraceClose, TNode(_):
+				token = t;
+			case TLength(v):
+				except(TBraceOpen);
+				except(TNode("a"));
+				var ints : Array<Int> = [];
+				var floats : Array<Float> = null;
+				var i = 0;
+				while( i < v ) {
+					t = next();
+					switch( t ) {
+					case TColon:
+						continue;
+					case TInt(s):
+						i++;
+						if( floats == null )
+							ints.push(Std.parseInt(s));
+						else
+							floats.push(Std.parseInt(s));
+					case TFloat(s):
+						i++;
+						if( floats == null ) {
+							floats = [];
+							for( i in ints )
+								floats.push(i);
+							ints = null;
+						}
+						floats.push(Std.parseFloat(s));
+					default:
+						unexpected(t);
+					}
+				}
+				props.push(floats == null ? PInts(ints) : PFloats(floats));
+				except(TBraceClose);
+				break;
+			default:
+				unexpected(t);
+			}
+			t = next();
+			switch( t ) {
+			case TNode(_), TBraceClose:
+				token = t; // next
+				break;
+			case TColon:
+				// next prop
+			case TBraceOpen:
+				childs = parseNodes();
+				except(TBraceClose);
+				break;
+			default:
+				unexpected(t);
+			}
+		}
+		if( childs == null ) childs = [];
+		return { name : name, props : props, childs : childs };
+	}
+
+	function except( except : Token ) {
+		var t = next();
+		if( !Type.enumEq(t, except) )
+			error("Unexpected '" + tokenStr(t) + "' (" + tokenStr(except) + " expected)");
+	}
+
+	function peek() {
+		if( token == null )
+			token = nextToken();
+		return token;
+	}
+
+	function next() {
+		if( token == null )
+			return nextToken();
+		var tmp = token;
+		token = null;
+		return tmp;
+	}
+
+	function error( msg : String ) : Dynamic {
+		throw msg + " (line " + line + ")";
+		return null;
+	}
+
+	function unexpected( t : Token ) : Dynamic {
+		return error("Unexpected "+tokenStr(t));
+	}
+
+	function tokenStr( t : Token ) {
+		return switch( t ) {
+		case TEof: "<eof>";
+		case TBraceOpen: '{';
+		case TBraceClose: '}';
+		case TIdent(i): i;
+		case TNode(i): i+":";
+		case TFloat(f): f;
+		case TInt(i): i;
+		case TString(s): '"' + s + '"';
+		case TColon: ',';
+		case TLength(l): '*' + l;
+		};
+	}
+
+	inline function nextChar() {
+		return StringTools.fastCodeAt(buf, pos++);
+	}
+
+	inline function getBuf( pos : Int, len : Int ) {
+		return buf.substr(pos, len);
+	}
+
+	inline function isIdentChar(c) {
+		return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code) || c == '_'.code || c == '-'.code;
+	}
+
+	@:noDebug
+	function nextToken() {
+		var start = pos;
+		while( true ) {
+			var c = nextChar();
+			switch( c ) {
+			case ' '.code, '\r'.code, '\t'.code:
+				start++;
+			case '\n'.code:
+				line++;
+				start++;
+			case ';'.code:
+				while( true ) {
+					var c = nextChar();
+					if( StringTools.isEof(c) || c == '\n'.code ) {
+						pos--;
+						break;
+					}
+				}
+				start = pos;
+			case ','.code:
+				return TColon;
+			case '{'.code:
+				return TBraceOpen;
+			case '}'.code:
+				return TBraceClose;
+			case '"'.code:
+				start = pos;
+				while( true ) {
+					c = nextChar();
+					if( c == '"'.code )
+						break;
+					if( StringTools.isEof(c) || c == '\n'.code )
+						error("Unclosed string");
+				}
+				return TString(getBuf(start, pos - start - 1));
+			case '*'.code:
+				start = pos;
+				do {
+					c = nextChar();
+				} while( c >= '0'.code && c <= '9'.code );
+				pos--;
+				return TLength(Std.parseInt(getBuf(start, pos - start)));
+			default:
+				if( (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || c == '_'.code ) {
+					do {
+						c = nextChar();
+					} while( isIdentChar(c) );
+					if( c == ':'.code )
+						return TNode(getBuf(start, pos - start - 1));
+					pos--;
+					return TIdent(getBuf(start, pos - start));
+				}
+				if( (c >= '0'.code && c <= '9'.code) || c == '-'.code ) {
+					do {
+						c = nextChar();
+					} while( c >= '0'.code && c <= '9'.code );
+					if( c != '.'.code && c != 'E'.code && c != 'e'.code && pos - start < 10 ) {
+						pos--;
+						return TInt(getBuf(start, pos - start));
+					}
+					if( c == '.'.code ) {
+						do {
+							c = nextChar();
+						} while( c >= '0'.code && c <= '9'.code );
+					}
+					if( c == 'e'.code || c == 'E'.code ) {
+						c = nextChar();
+						if( c != '-'.code && c != '+'.code )
+							pos--;
+						do {
+							c = nextChar();
+						} while( c >= '0'.code && c <= '9'.code );
+					}
+					pos--;
+					return TFloat(getBuf(start, pos - start));
+				}
+				if( StringTools.isEof(c) ) {
+					pos--;
+					return TEof;
+				}
+				error("Unexpected char '" + String.fromCharCode(c) + "'");
+			}
+		}
+	}
+
+	public static function parse( text : String ) {
+		return new Parser().parseText(text);
+	}
+}
+
+class FbxLibrary {
+
+	var root : FbxNode;
+	var ids : Map<Int,FbxNode>;
+	var connect : Map<Int,Array<Int>>;
+	var namedConnect : Map<Int,Map<String,Int>>;
+	var invConnect : Map<Int,Array<Int>>;
+	// var uvAnims : Map<String, Array<{ t : Float, u : Float, v : Float }>>;
+	// var animationEvents : Array<{ frame : Int, data : String }>;
+
+	/**
+		The FBX version that was decoded
+	**/
+	public var version : Float = 0.;
+
+	public function new() {
+		root = { name : "Root", props : [], childs : [] };
+		reset();
+	}
+
+	function reset() {
+		ids = new Map();
+		connect = new Map();
+		namedConnect = new Map();
+		invConnect = new Map();
+	}
+
+	public function load( root : FbxNode ) {
+		reset();
+		this.root = root;
+
+		version = FbxTools.toInt(FbxTools.get(root, "FBXHeaderExtension.FBXVersion").props[0]) / 1000;
+		if( Std.int(version) != 7 )
+			throw "FBX Version 7.x required : use FBX 2010 export";
+
+		for( c in root.childs )
+			init(c);
+
+		// init properties
+		// for( m in FbxTools.getAll(this.root, "Objects.Model") ) {
+		// 	for( p in FbxTools.getAll(m, "Properties70.P") )
+		// 		switch( FbxTools.toString(p.props[0]) ) {
+		// 		case "UDP3DSMAX":
+		// 			var userProps = FbxTools.toString(p.props[4]).split("&cr;&lf;");
+		// 			for( p in userProps ) {
+		// 				var pl = p.split("=");
+		// 				var pname = StringTools.trim(pl.shift());
+		// 				var pval = StringTools.trim(pl.join("="));
+		// 				switch( pname ) {
+		// 				case "UV" if( pval != "" ):
+		// 					var xml = try Xml.parse(pval) catch( e : Dynamic ) throw "Invalid UV data in " + FbxTools.getName(m);
+		// 					var frames = [for( f in new haxe.xml.Access(xml.firstElement()).elements ) { var f = f.innerData.split(" ");  { t : Std.parseFloat(f[0]) * 9622116.25, u : Std.parseFloat(f[1]), v : Std.parseFloat(f[2]) }} ];
+		// 					if( uvAnims == null ) uvAnims = new Map();
+		// 					uvAnims.set(FbxTools.getName(m), frames);
+		// 				case "Events":
+		// 					var xml = try Xml.parse(pval) catch( e : Dynamic ) throw "Invalid Events data in " + FbxTools.getName(m);
+		// 					animationEvents = [for( f in new haxe.xml.Access(xml.firstElement()).elements ) { var f = f.innerData.split(" ");  { frame : Std.parseInt(f.shift()), data : StringTools.trim(f.join(" ")) }} ];
+		// 				default:
+		// 				}
+		// 			}
+		// 		default:
+		// 		}
+		// }
+	}
+
+	function init( n : FbxNode ) {
+		switch( n.name ) {
+		case "Connections":
+			for( c in n.childs ) {
+				if( c.name != "C" )
+					continue;
+				var child = FbxTools.toInt(c.props[1]);
+				var parent = FbxTools.toInt(c.props[2]);
+
+				// Maya exports invalid references
+				if( ids.get(child) == null || ids.get(parent) == null ) continue;
+
+				var name = c.props[3];
+
+				if( name != null ) {
+					var name = FbxTools.toString(name);
+					var nc = namedConnect.get(parent);
+					if( nc == null ) {
+						nc = new Map();
+						namedConnect.set(parent, nc);
+					}
+					nc.set(name, child);
+					// don't register as a parent, since the target can also be the child of something else
+					if( name == "LookAtProperty" ) continue;
+				}
+
+				var c = connect.get(parent);
+				if( c == null ) {
+					c = [];
+					connect.set(parent, c);
+				}
+				c.push(child);
+
+				if( parent == 0 )
+					continue;
+
+				var c = invConnect.get(child);
+				if( c == null ) {
+					c = [];
+					invConnect.set(child, c);
+				}
+				c.push(parent);
+			}
+		case "Objects":
+			for( c in n.childs )
+				ids.set(FbxTools.getId(c), c);
+		default:
+		}
+	}
+
+	public function getGeometry( name : String = "" ) {
+		var geom = null;
+		for( g in FbxTools.getAll(root, "Objects.Geometry") )
+			if( FbxTools.hasProp(g, PString("Geometry::" + name)) ) {
+				geom = g;
+				break;
+			}
+		if( geom == null )
+			throw "Geometry " + name + " not found";
+		return new Geometry(this, geom);
+	}
+
+	public function getFirstGeometry() {
+		var geom = FbxTools.getAll(root, "Objects.Geometry")[0];
+		return new Geometry(this, geom);
+	}
+
+	public function getAllGeometries() {
+		var geoms = FbxTools.getAll(root, "Objects.Geometry");
+		var res:Array<Geometry> = [];
+		for (g in geoms) res.push(new Geometry(this, g));
+		return res;
+	}
+}
+
+class Geometry {
+
+	var lib : FbxLibrary;
+	var root : FbxNode;
+
+	public function new(l, root) {
+		this.lib = l;
+		this.root = root;
+	}
+
+	public function getRoot() {
+		return root;
+	}
+
+	public function getVertices() {
+		return FbxTools.getFloats(FbxTools.get(root, "Vertices"));
+	}
+
+	public function getPolygons() {
+		return FbxTools.getInts(FbxTools.get(root, "PolygonVertexIndex"));
+	}
+
+	/**
+		Decode polygon informations into triangle indexes and vertices indexes.
+		Returns vidx, which is the list of vertices indexes and iout which is the index buffer for the full vertex model
+	**/
+	public function getIndexes() {
+		var count = 0, pos = 0;
+		var index = getPolygons();
+		var vout = [], iout = [];
+		for( i in index ) {
+			count++;
+			if( i < 0 ) {
+				index[pos] = -i - 1;
+				var start = pos - count + 1;
+				for( n in 0...count )
+					vout.push(index[n + start]);
+				for( n in 0...count - 2 ) {
+					iout.push(start + n);
+					iout.push(start + count - 1);
+					iout.push(start + n + 1);
+				}
+				index[pos] = i; // restore
+				count = 0;
+			}
+			pos++;
+		}
+		return { vidx : vout, idx : iout };
+	}
+
+	public function getNormals() {
+		return processVectors("LayerElementNormal", "Normals");
+	}
+
+	public function getTangents( opt = false ) {
+		return processVectors("LayerElementTangent", "Tangents", opt);
+	}
+
+	function processVectors( layer, name, opt = false ) {
+		var vect = FbxTools.get(root, layer + "." + name, opt);
+		if( vect == null ) return null;
+		var nrm = FbxTools.getFloats(vect);
+		// if by-vertice (Maya in some cases, unless maybe "Split per-Vertex Normals" is checked)
+		// let's reindex based on polygon indexes
+		if( FbxTools.toString(FbxTools.get(root, layer+".MappingInformationType").props[0]) == "ByVertice" ) {
+			var nout = [];
+			for( i in getPolygons() ) {
+				var vid = i;
+				if( vid < 0 ) vid = -vid - 1;
+				nout.push(nrm[vid * 3]);
+				nout.push(nrm[vid * 3 + 1]);
+				nout.push(nrm[vid * 3 + 2]);
+			}
+			nrm = nout;
+		}
+		return nrm;
+	}
+
+	public function getColors() {
+		var color = FbxTools.get(root, "LayerElementColor",true);
+		return color == null ? null : { values : FbxTools.getFloats(FbxTools.get(color, "Colors")), index : FbxTools.getInts(FbxTools.get(color, "ColorIndex")) };
+	}
+
+	public function getUVs() {
+		var uvs = [];
+		for( v in FbxTools.getAll(root, "LayerElementUV") ) {
+			var index = FbxTools.get(v, "UVIndex", true);
+			var values = FbxTools.getFloats(FbxTools.get(v, "UV"));
+			var index = if( index == null ) {
+				// ByVertice/Direct (Maya sometimes...)
+				[for( i in getPolygons() ) if( i < 0 ) -i - 1 else i];
+			} else FbxTools.getInts(index);
+			uvs.push({ values : values, index : index });
+		}
+		return uvs;
+	}
+
+	public function getBuffers(binary:Bool, p:FbxParser) {
+		// triangulize indexes :
+		// format is  A,B,...,-X : negative values mark the end of the polygon
+		var pbuf = getVertices();
+		var nbuf = getNormals();
+		var tbuf = getUVs()[0];
+		var cbuf = FbxParser.parseVCols ? getColors() : null;
+		var polys = getPolygons();
+
+		if (FbxParser.parseTransform) {
+			var m = iron.math.Mat4.identity();
+			var v = new iron.math.Vec4(p.tx, p.ty, p.tz);
+			var q = new iron.math.Quat();
+			q.fromEuler(p.rx, p.ry, p.rz);
+			var sc = new iron.math.Vec4(p.sx, p.sy, p.sz);
+			m.compose(v, q, sc);
+
+			for (i in 0...Std.int(pbuf.length / 3)) {
+				v.set(pbuf[i * 3], pbuf[i * 3 + 1], pbuf[i * 3 + 2]);
+				v.applymat(m);
+				pbuf[i * 3    ] = v.x;
+				pbuf[i * 3 + 1] = v.y;
+				pbuf[i * 3 + 2] = v.z;
+			}
+			for (i in 0...Std.int(nbuf.length / 3)) {
+				v.set(nbuf[i * 3], nbuf[i * 3 + 1], nbuf[i * 3 + 2]);
+				v.applyQuat(q);
+				nbuf[i * 3    ] = v.x;
+				nbuf[i * 3 + 1] = v.y;
+				nbuf[i * 3 + 2] = v.z;
+			}
+		}
+
+		// Pack positions to (-1, 1) range
+		var scalePos = 0.0;
+		for (p in pbuf) {
+			var f = Math.abs(p);
+			if (scalePos < f) scalePos = f;
+		}
+		var inv = 32767 * (1 / scalePos);
+
+		var pos = 0;
+		var count = 0;
+		var vlen = 0;
+		var ilen = 0;
+		for (i in polys) {
+			count++;
+			if (i < 0) {
+				for (n in 0...count) vlen++;
+				for (n in 0...count - 2) ilen += 3;
+				count = 0;
+			}
+			pos++;
+		}
+
+		// Pack into 16bit
+		var posa = new kha.arrays.Int16Array(vlen * 4);
+		var nora = new kha.arrays.Int16Array(vlen * 2);
+		var texa = tbuf != null ? new kha.arrays.Int16Array(vlen * 2) : null;
+		var cola = cbuf != null ? new kha.arrays.Int16Array(vlen * 4) : null;
+		var inda = new kha.arrays.Uint32Array(ilen);
+
+		pos = 0;
+		count = 0;
+		vlen = 0;
+		ilen = 0;
+		for (i in polys) {
+			count++;
+			if (i < 0) {
+				polys[pos] = -i - 1;
+				var start = pos - count + 1;
+				for (n in 0...count) {
+					var k = n + start;
+					var vidx = polys[k];
+					posa[vlen * 4    ] = Std.int(pbuf[vidx * 3    ] * inv);
+					posa[vlen * 4 + 1] = Std.int(pbuf[vidx * 3 + 1] * inv);
+					posa[vlen * 4 + 2] = Std.int(pbuf[vidx * 3 + 2] * inv);
+					posa[vlen * 4 + 3] = Std.int(nbuf[k * 3 + 2] * 32767);
+					nora[vlen * 2    ] = Std.int(nbuf[k * 3    ] * 32767);
+					nora[vlen * 2 + 1] = Std.int(nbuf[k * 3 + 1] * 32767);
+					if (tbuf != null) {
+						var iuv = tbuf.index[k];
+						var uvx = tbuf.values[iuv * 2];
+						if (uvx > 1.0) uvx = uvx - Std.int(uvx);
+						var uvy = tbuf.values[iuv * 2 + 1];
+						if (uvy > 1.0) uvy = uvy - Std.int(uvy);
+						texa[vlen * 2    ] = Std.int(       uvx  * 32767);
+						texa[vlen * 2 + 1] = Std.int((1.0 - uvy) * 32767);
+					}
+					if (cbuf != null) {
+						var icol = cbuf.index[k];
+						cola[vlen * 3    ] = Std.int(cbuf.values[icol * 4    ] * 32767);
+						cola[vlen * 3 + 1] = Std.int(cbuf.values[icol * 4 + 1] * 32767);
+						cola[vlen * 3 + 2] = Std.int(cbuf.values[icol * 4 + 2] * 32767);
+						// cola[vlen * 4 + 3] = Std.int(cbuf.values[icol * 4 + 3] * 32767);
+					}
+					vlen++;
+				}
+				if (count <= 4) { // Convex, fan triangulation
+					for (n in 0...count - 2) {
+						inda[ilen + 2] = start + n;
+						inda[ilen + 1] = start + count - 1;
+						inda[ilen    ] = start + n + 1;
+						ilen += 3;
+					}
+				}
+				else { // Convex or concave, ear clipping
+					var va: Array<Int> = [];
+					for (i in 0...count) va.push(start + i);
+					var nx = nbuf[start * 3    ];
+					var ny = nbuf[start * 3 + 1];
+					var nz = nbuf[start * 3 + 2];
+					var nxabs = Math.abs(nx);
+					var nyabs = Math.abs(ny);
+					var nzabs = Math.abs(nz);
+					var flip = nx + ny + nz > 0;
+					var axis = nxabs > nyabs && nxabs > nzabs ? 0 : nyabs > nxabs && nyabs > nzabs ? 1 : 2;
+					var axis0 = axis == 0 ? (flip ? 2 : 1) : axis == 1 ? (flip ? 0 : 2) : (flip ? 1 : 0);
+					var axis1 = axis == 0 ? (flip ? 1 : 2) : axis == 1 ? (flip ? 2 : 0) : (flip ? 0 : 1);
+
+					var vi = count;
+					var loops = 0;
+					var i = -1;
+					while (vi > 3 && loops++ < vi) {
+						i = (i + 1) % vi;
+						var i1 = (i + 1) % vi;
+						var i2 = (i + 2) % vi;
+						var vi0 = polys[va[i ]] * 3;
+						var vi1 = polys[va[i1]] * 3;
+						var vi2 = polys[va[i2]] * 3;
+						var v0x = pbuf[vi0 + axis0];
+						var v0y = pbuf[vi0 + axis1];
+						var v1x = pbuf[vi1 + axis0];
+						var v1y = pbuf[vi1 + axis1];
+						var v2x = pbuf[vi2 + axis0];
+						var v2y = pbuf[vi2 + axis1];
+
+						var e0x = v0x - v1x; // Not an interior vertex
+						var e0y = v0y - v1y;
+						var e1x = v2x - v1x;
+						var e1y = v2y - v1y;
+						var cross = e0x * e1y - e0y * e1x;
+						if (cross <= 0) continue;
+
+						var overlap = false; // Other vertex found inside this triangle
+						for (j in 0...vi - 3) {
+							var j0 = polys[va[(i + 3 + j) % vi]] * 3;
+							var px = pbuf[j0 + axis0];
+							var py = pbuf[j0 + axis1];
+							if (MeshParser.pnpoly(v0x, v0y, v1x, v1y, v2x, v2y, px, py)) {
+								overlap = true;
+								break;
+							}
+						}
+						if (overlap) continue;
+
+						inda[ilen++] = va[i ]; // Found ear
+						inda[ilen++] = va[i1];
+						inda[ilen++] = va[i2];
+
+						for (j in ((i + 1) % vi)...vi - 1) { // Consume vertex
+							va[j] = va[j + 1];
+						}
+						vi--;
+						i--;
+						loops = 0;
+					}
+					inda[ilen++] = va[0]; // Last one
+					inda[ilen++] = va[1];
+					inda[ilen++] = va[2];
+				}
+				polys[pos] = i; // restore
+				count = 0;
+			}
+			pos++;
+		}
+
+		return { posa: posa, nora: nora, texa: texa, cola: cola, inda: inda, scalePos: scalePos };
+	}
+}

+ 101 - 0
Sources/arm/format/FbxParser.hx

@@ -0,0 +1,101 @@
+package arm.format;
+
+import arm.format.FbxLibrary;
+
+@:access(arm.format.FbxLibrary)
+@:access(arm.format.Geometry)
+class FbxParser {
+
+	public var posa: kha.arrays.Int16Array = null;
+	public var nora: kha.arrays.Int16Array = null;
+	public var texa: kha.arrays.Int16Array = null;
+	public var cola: kha.arrays.Int16Array = null;
+	public var inda: kha.arrays.Uint32Array = null;
+	public var scalePos = 1.0;
+	public var scaleTex = 1.0;
+	public var name = "";
+
+	public static var parseTransform = false;
+	public static var parseVCols = false;
+
+	// Transform
+	public var tx = 0.0;
+	public var ty = 0.0;
+	public var tz = 0.0;
+	public var rx = 0.0;
+	public var ry = 0.0;
+	public var rz = 0.0;
+	public var sx = 1.0;
+	public var sy = 1.0;
+	public var sz = 1.0;
+
+	var geoms: Array<Geometry>;
+	var current = 0;
+	var binary = true;
+
+	public function new(blob: kha.Blob) {
+		var magic = "Kaydara FBX Binary\x20\x20\x00\x1a\x00";
+		var s = "";
+		for (i in 0...magic.length) s += String.fromCharCode(blob.readU8(i));
+		binary = s == magic;
+
+		var fbx = binary ? FbxBinaryParser.parse(blob) : Parser.parse(blob.toString());
+		var lib = new FbxLibrary();
+		try { lib.load(fbx); }
+		catch (e: Dynamic) { trace(e); }
+
+		geoms = lib.getAllGeometries();
+		next();
+	}
+
+	public function next(): Bool {
+		if (current >= geoms.length) return false;
+		var geom = geoms[current];
+		var lib = geom.lib;
+
+		tx = ty = tz = 0;
+		rx = ry = rz = 0;
+		sx = sy = sz = 1;
+		if (parseTransform) {
+			var connects = lib.invConnect.get(FbxTools.getId(geom.getRoot()));
+			for (c in connects) {
+				var node = lib.ids.get(c);
+				for (p in FbxTools.getAll(node, "Properties70.P")) {
+					switch (FbxTools.toString(p.props[0])) {
+						case "Lcl Translation": {
+							tx = FbxTools.toFloat(p.props[4]);
+							ty = FbxTools.toFloat(p.props[5]);
+							tz = FbxTools.toFloat(p.props[6]);
+						}
+						case "Lcl Rotation": {
+							rx = FbxTools.toFloat(p.props[4]) * Math.PI / 180;
+							ry = FbxTools.toFloat(p.props[5]) * Math.PI / 180;
+							rz = FbxTools.toFloat(p.props[6]) * Math.PI / 180;
+						}
+						case "Lcl Scaling": {
+							sx = FbxTools.toFloat(p.props[4]);
+							sy = FbxTools.toFloat(p.props[5]);
+							sz = FbxTools.toFloat(p.props[6]);
+						}
+					}
+				}
+			}
+		}
+
+		var res = geom.getBuffers(binary, this);
+		scalePos = res.scalePos;
+		posa = res.posa;
+		nora = res.nora;
+		texa = res.texa;
+		cola = res.cola;
+		inda = res.inda;
+		name = FbxTools.getName(geom.getRoot());
+		if (name.charCodeAt(0) == 0) name = name.substring(1); // null
+		if (name.charCodeAt(0) == 1) name = name.substring(1); // start of heading
+		if (name == "Geometry") name = "Object -Geometry";
+		name = name.substring(0, name.length - 10); // -Geometry
+
+		current++;
+		return true;
+	}
+}

+ 37 - 0
Sources/arm/format/JpgData.hx

@@ -0,0 +1,37 @@
+/*
+ * format - haXe File Formats
+ *
+ *  JPG File Format
+ *  Copyright (C) 2007-2009 Trevor McCauley, Baluta Cristian (hx port) & Robert Sköld (format conversion)
+ *
+ * Copyright (c) 2009, The haXe Project Contributors
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+package arm.format;
+
+typedef JpgData = {
+	var width: Int;
+	var height: Int;
+	var quality: Float;
+	var pixels: haxe.io.Bytes;
+}

+ 740 - 0
Sources/arm/format/JpgWriter.hx

@@ -0,0 +1,740 @@
+/*
+ * format - haXe File Formats
+ *
+ *  JPG File Format
+ *  Copyright (C) 2007-2009 Thibault Imbert, AS3-to-haXe by Michel Oster
+ *
+ * Copyright (c) 2009, The haXe Project Contributors
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+package arm.format;
+
+class JpgWriter {
+	var ZigZag: Array<Int>;
+
+	// Static table initialization
+	function initZigZag() {
+		ZigZag = [
+			 0, 1, 5, 6,14,15,27,28,
+			 2, 4, 7,13,16,26,29,42,
+			 3, 8,12,17,25,30,41,43,
+			 9,11,18,24,31,40,44,53,
+			10,19,23,32,39,45,52,54,
+			20,22,33,38,46,51,55,60,
+			21,34,37,47,50,56,59,61,
+			35,36,48,49,57,58,62,63
+		];
+	}
+
+	var YTable: Array<Int>;     // = new Array(64);
+	var UVTable: Array<Int>;    // = new Array(64);
+	var fdtbl_Y: Array<Float>;    // = new Array(64);
+	var fdtbl_UV: Array<Float>;   // = new Array(64);
+
+	function initQuantTables(sf: Int) {
+		var YQT: Array<Int> = [
+			16, 11, 10, 16, 24, 40, 51, 61,
+			12, 12, 14, 19, 26, 58, 60, 55,
+			14, 13, 16, 24, 40, 57, 69, 56,
+			14, 17, 22, 29, 51, 87, 80, 62,
+			18, 22, 37, 56, 68,109,103, 77,
+			24, 35, 55, 64, 81,104,113, 92,
+			49, 64, 78, 87,103,121,120,101,
+			72, 92, 95, 98,112,100,103, 99
+		];
+		for (i in 0...64) {
+			var t: Int = Math.floor( (YQT[i] * sf + 50) / 100 );
+			if( t < 1 ) t = 1;
+			else if( t > 255 ) t = 255;
+			YTable[ ZigZag[i] ] = t;
+		}
+		var UVQT: Array<Int> = [
+			17, 18, 24, 47, 99, 99, 99, 99,
+			18, 21, 26, 66, 99, 99, 99, 99,
+			24, 26, 56, 99, 99, 99, 99, 99,
+			47, 66, 99, 99, 99, 99, 99, 99,
+			99, 99, 99, 99, 99, 99, 99, 99,
+			99, 99, 99, 99, 99, 99, 99, 99,
+			99, 99, 99, 99, 99, 99, 99, 99,
+			99, 99, 99, 99, 99, 99, 99, 99
+		];
+		for( j in 0...64 ) {
+			var u: Int = Math.floor( (UVQT[j] * sf + 50) / 100 );
+			if( u < 1 ) u = 1;
+			else if( u > 255 ) u = 255;
+			UVTable[ ZigZag[j] ] = u;
+		}
+		var aasf: Array<Float> = [
+			1.0, 1.387039845, 1.306562965, 1.175875602,
+			1.0, 0.785694958, 0.541196100, 0.275899379
+		];
+		var k = 0;
+		for( row in 0...8 ) {
+			for( col in 0...8 ) {
+				fdtbl_Y[k]  = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0));
+				fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0));
+				k++;
+			}
+		}
+	}
+
+	var std_dc_luminance_nrcodes: Array<Int>;
+	var std_dc_luminance_values: haxe.io.Bytes;
+	var std_ac_luminance_nrcodes: Array<Int>;
+	var std_ac_luminance_values: haxe.io.Bytes;
+
+	function initLuminance() {
+		std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0];
+		std_dc_luminance_values = strIntsToBytes( '0,1,2,3,4,5,6,7,8,9,10,11' );
+		std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d];
+		std_ac_luminance_values = strIntsToBytes(
+			'0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,' +
+			'0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,' +
+			'0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08,' +
+			'0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,' +
+			'0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,' +
+			'0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28,' +
+			'0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,' +
+			'0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,' +
+			'0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59,' +
+			'0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,' +
+			'0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,' +
+			'0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89,' +
+			'0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,' +
+			'0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,' +
+			'0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6,' +
+			'0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,' +
+			'0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,' +
+			'0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2,' +
+			'0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,' +
+			'0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,' +
+			'0xf9,0xfa'
+		);
+	}
+
+	function strIntsToBytes( s: String ) {
+		var len = s.length;
+		var b = new haxe.io.BytesBuffer();
+		var val = 0;
+		var i = 0;
+		for( j in 0...len ) {
+			if( s.charAt( j ) == ',' ) {
+				val = Std.parseInt( s.substr(i, j - i) );
+				b.addByte( val );
+				i = j + 1;
+			}
+		}
+		if( i < len ) {
+			val = Std.parseInt( s.substr(i) );
+			b.addByte( val );
+		}
+		return b.getBytes();
+	}
+
+	var std_dc_chrominance_nrcodes: Array<Int>;
+	var std_dc_chrominance_values: haxe.io.Bytes;
+	var std_ac_chrominance_nrcodes: Array<Int>;
+	var std_ac_chrominance_values: haxe.io.Bytes;
+
+	function initChrominance() {
+		std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0];
+		std_dc_chrominance_values = strIntsToBytes( '0,1,2,3,4,5,6,7,8,9,10,11' );
+		std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77];
+		std_ac_chrominance_values = strIntsToBytes(
+			'0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,' +
+			'0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,' +
+			'0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91,' +
+			'0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,' +
+			'0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,' +
+			'0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26,' +
+			'0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,' +
+			'0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,' +
+			'0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,' +
+			'0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,' +
+			'0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,' +
+			'0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87,' +
+			'0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,' +
+			'0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,' +
+			'0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,' +
+			'0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,' +
+			'0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,' +
+			'0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,' +
+			'0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,' +
+			'0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,' +
+			'0xf9,0xfa'
+		);
+	}
+
+	var YDC_HT: Map<Int,BitString>;
+	var UVDC_HT: Map<Int,BitString>;
+	var YAC_HT: Map<Int,BitString>;
+	var UVAC_HT: Map<Int,BitString>;
+
+	function initHuffmanTbl() {
+		YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes, std_dc_luminance_values);
+		UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes, std_dc_chrominance_values);
+		YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes, std_ac_luminance_values);
+		UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes, std_ac_chrominance_values);
+	}
+
+	function computeHuffmanTbl(nrcodes: Array<Int>, std_table: haxe.io.Bytes): Map<Int,BitString> {
+		var codevalue = 0;
+		var pos_in_table = 0;
+		var HT: Map<Int,BitString> = new Map();
+		for( k in 1...17 ) {
+			var end = nrcodes[k];
+			for( j in 0...end ) {
+				var idx: Int = std_table.get( pos_in_table );
+				HT.set( idx, new BitString( k, codevalue ) );
+				pos_in_table++;
+				codevalue++;
+			}
+			codevalue *= 2;
+		}
+		return HT;
+	}
+
+	var bitcode: Map<Int,BitString>;
+	var category: Map<Int,Int>;
+
+	function initCategoryNumber() {
+		var nrlower = 1;
+		var nrupper = 2;
+		var idx: Int;
+		for (cat in 1...16) {
+			//Positive numbers
+			for( nr in nrlower...nrupper ) {
+				idx = 32767 + nr;
+				category.set( idx, cat );
+				bitcode.set( idx, new BitString( cat, nr ) );
+			}
+			//Negative numbers
+			var nrneg: Int = -(nrupper - 1);
+			while( nrneg <= -nrlower ) {
+				idx = 32767 + nrneg;
+				category.set( idx, cat );
+				bitcode.set( idx, new BitString( cat, nrupper - 1 + nrneg ) );
+				nrneg++;
+			}
+			nrlower <<= 1;
+			nrupper <<= 1;
+		}
+	}
+
+	// IO functions
+	var byteout: haxe.io.Output;
+	var bytenew: Int;
+	var bytepos: Int;
+
+	function writeBits(bs: BitString) {
+		var value: Int = bs.val;
+		var posval: Int = bs.len - 1;
+		while( posval >= 0 ) {
+			//if (value & uint(1 << posval) ) {
+			if( (value & (1 << posval)) != 0 ) {  //<- CORRECT ?
+				//bytenew |= uint(1 << bytepos);
+				bytenew |= (1 << bytepos);
+			}
+			posval--;
+			bytepos--;
+			if( bytepos < 0 ) {
+				if( bytenew == 0xFF ) {
+					b(0xFF);
+					b(0);
+				}
+				else {
+					b(bytenew);
+				}
+				bytepos = 7;
+				bytenew = 0;
+			}
+		}
+	}
+
+	function writeWord( val: Int ) {
+		b( (val >> 8) & 0xFF );
+		b( val & 0xFF );
+	}
+
+	// DCT & quantization core
+
+	function fDCTQuant(data: Array<Float>, fdtbl: Array<Float>): Array<Float> {
+		/* Pass 1:  process rows. */
+		var dataOff = 0;
+		for (i in 0...8) {
+			var tmp0: Float = data[dataOff + 0] + data[dataOff + 7];
+			var tmp7: Float = data[dataOff + 0] - data[dataOff + 7];
+			var tmp1: Float = data[dataOff + 1] + data[dataOff + 6];
+			var tmp6: Float = data[dataOff + 1] - data[dataOff + 6];
+			var tmp2: Float = data[dataOff + 2] + data[dataOff + 5];
+			var tmp5: Float = data[dataOff + 2] - data[dataOff + 5];
+			var tmp3: Float = data[dataOff + 3] + data[dataOff + 4];
+			var tmp4: Float = data[dataOff + 3] - data[dataOff + 4];
+
+			/* Even part */
+			var tmp10: Float = tmp0 + tmp3;	/* phase 2 */
+			var tmp13: Float = tmp0 - tmp3;
+			var tmp11: Float = tmp1 + tmp2;
+			var tmp12: Float = tmp1 - tmp2;
+
+			data[dataOff + 0] = tmp10 + tmp11; /* phase 3 */
+			data[dataOff + 4] = tmp10 - tmp11;
+
+			var z1: Float = (tmp12 + tmp13) * 0.707106781; /* c4 */
+			data[dataOff + 2] = tmp13 + z1; /* phase 5 */
+			data[dataOff + 6] = tmp13 - z1;
+
+			/* Odd part */
+			tmp10 = tmp4 + tmp5; /* phase 2 */
+			tmp11 = tmp5 + tmp6;
+			tmp12 = tmp6 + tmp7;
+
+			/* The rotator is modified from fig 4-8 to avoid extra negations. */
+			var z5: Float = (tmp10 - tmp12) * 0.382683433; /* c6 */
+			var z2: Float = 0.541196100 * tmp10 + z5; /* c2-c6 */
+			var z4: Float = 1.306562965 * tmp12 + z5; /* c2+c6 */
+			var z3: Float = tmp11 * 0.707106781; /* c4 */
+
+			var z11: Float = tmp7 + z3;	/* phase 5 */
+			var z13: Float = tmp7 - z3;
+
+			data[dataOff + 5] = z13 + z2;	/* phase 6 */
+			data[dataOff + 3] = z13 - z2;
+			data[dataOff + 1] = z11 + z4;
+			data[dataOff + 7] = z11 - z4;
+
+			dataOff += 8; /* advance pointer to next row */
+		}
+
+		/* Pass 2:  process columns. */
+		dataOff = 0;
+		for (j in 0...8) {
+			var tmp0p2: Float = data[dataOff+ 0] + data[dataOff+56];
+			var tmp7p2: Float = data[dataOff+ 0] - data[dataOff+56];
+			var tmp1p2: Float = data[dataOff+ 8] + data[dataOff+48];
+			var tmp6p2: Float = data[dataOff+ 8] - data[dataOff+48];
+			var tmp2p2: Float = data[dataOff+16] + data[dataOff+40];
+			var tmp5p2: Float = data[dataOff+16] - data[dataOff+40];
+			var tmp3p2: Float = data[dataOff+24] + data[dataOff+32];
+			var tmp4p2: Float = data[dataOff+24] - data[dataOff+32];
+
+			/* Even part */
+			var tmp10p2: Float = tmp0p2 + tmp3p2;	/* phase 2 */
+			var tmp13p2: Float = tmp0p2 - tmp3p2;
+			var tmp11p2: Float = tmp1p2 + tmp2p2;
+			var tmp12p2: Float = tmp1p2 - tmp2p2;
+
+			data[dataOff+ 0] = tmp10p2 + tmp11p2; /* phase 3 */
+			data[dataOff+32] = tmp10p2 - tmp11p2;
+
+			var z1p2: Float = (tmp12p2 + tmp13p2) * 0.707106781; /* c4 */
+			data[dataOff+16] = tmp13p2 + z1p2; /* phase 5 */
+			data[dataOff+48] = tmp13p2 - z1p2;
+
+			/* Odd part */
+			tmp10p2 = tmp4p2 + tmp5p2; /* phase 2 */
+			tmp11p2 = tmp5p2 + tmp6p2;
+			tmp12p2 = tmp6p2 + tmp7p2;
+
+			/* The rotator is modified from fig 4-8 to avoid extra negations. */
+			var z5p2: Float = (tmp10p2 - tmp12p2) * 0.382683433; /* c6 */
+			var z2p2: Float = 0.541196100 * tmp10p2 + z5p2; /* c2-c6 */
+			var z4p2: Float = 1.306562965 * tmp12p2 + z5p2; /* c2+c6 */
+			var z3p2: Float= tmp11p2 * 0.707106781; /* c4 */
+
+			var z11p2: Float = tmp7p2 + z3p2;	/* phase 5 */
+			var z13p2: Float = tmp7p2 - z3p2;
+
+			data[dataOff+40] = z13p2 + z2p2; /* phase 6 */
+			data[dataOff+24] = z13p2 - z2p2;
+			data[dataOff+ 8] = z11p2 + z4p2;
+			data[dataOff+56] = z11p2 - z4p2;
+
+			dataOff++; /* advance pointer to next column */
+		}
+
+		// Quantize/descale the coefficients
+		for (k in 0...64) {
+			// Apply the quantization and scaling factor & Round to nearest integer
+			data[k] = Math.round(data[k] * fdtbl[k]);
+		}
+		return data;
+	}
+
+	// Chunk writing
+
+	inline function b(v) {
+		byteout.writeByte(v);
+	}
+
+	function writeAPP0() {
+		b(0xFF); b(0xE0);  //<- marker 0xFFE0
+		b(0); b(16);  //<- length
+		b("J".code);  // J
+		b("F".code);
+		b("I".code);
+		b("F".code);
+		b(0);
+		b(1);  // versionhi
+		b(1);  // versionlo
+		b(0);  // xyunits
+		b(0); b(1);  // xdensity
+		b(0); b(1);  // ydensity
+		b(0);  // thumbnwidth
+		b(0);  // thumbnheight
+	}
+	function writeDQT() {
+		b(0xFF); b(0xDB);  //<- marker 0xFFDB
+		b(0); b(132);   //<- length
+		b(0);
+		for( j in 0...64 )
+			b(YTable[j]);
+		b(1);
+		for( j in 0...64 )
+			b(UVTable[j]);
+	}
+	function writeSOF0(width: Int, height: Int) {
+		b(0xFF); b(0xC0);  //<- marker 0xFFC0
+		b(0);  b(17);    //<- length, truecolor YUV JPG
+		b(8);  // precision
+		b( (height>>8) & 0xFF );
+		b(  height & 0xFF );
+		b(  (width>>8) & 0xFF );
+		b(  width & 0xFF );
+		b(3);     // nrofcomponents
+		b(1);     // IdY
+		b(0x11);  // HVY
+		b(0);     // QTY
+		b(2);     // IdU
+		b(0x11);  // HVU
+		b(1);     // QTU
+		b(3);     // IdV
+		b(0x11);  // HVV
+		b(1);     // QTV
+	}
+
+	function writeDHT() {
+		b(0xFF); b(0xC4);  //<- marker 0xFFC4
+		b(0x01); b(0xA2);   //<- length
+		b(0);  // HTYDCinfo
+		for( j in 1...17 )
+			b(std_dc_luminance_nrcodes[j]);
+		byteout.write(std_dc_luminance_values);
+
+		b(0x10);  // HTYACinfo
+		for( j in 1...17 )
+			b(std_ac_luminance_nrcodes[j]);
+		byteout.write(std_ac_luminance_values);
+
+		b(1);  // HTUDCinfo
+		for( j in 1...17 )
+			b(std_dc_chrominance_nrcodes[j]);
+		byteout.write(std_dc_chrominance_values);
+
+		b(0x11);  // HTUACinfo
+		for( j in 1...17 )
+			b(std_ac_chrominance_nrcodes[j]);
+		byteout.write(std_ac_chrominance_values);
+	}
+
+	function writeSOS() {
+		b(0xFF); b(0xDA);  //<- marker 0xFFDA
+		b(0); b(12);   //<- length
+		b(3);     // nrofcomponents
+		b(1);     // IdY
+		b(0);     // HTY
+		b(2);     // IdU
+		b(0x11);  // HTU
+		b(3);     // IdV
+		b(0x11);  // HTV
+		b(0);     // Ss
+		b(0x3F);  // Se
+		b(0);     // Bf
+	}
+
+	// Core processing
+	var DU: Array<Float>;  //<- initialized in function new JPEGEncoder()
+
+	function processDU(CDU: Array<Float>, fdtbl: Array<Float>, DC: Float, HTDC: Map<Int,BitString>, HTAC: Map<Int,BitString>): Float {
+		var EOB: BitString = HTAC.get( 0x00 );
+		var M16zeroes: BitString = HTAC.get( 0xF0 );
+
+		var DU_DCT: Array<Float> = fDCTQuant(CDU, fdtbl);
+		//ZigZag reorder
+		for (i in 0...64) {
+			DU[ ZigZag[i] ] = DU_DCT[i];
+		}
+		var idx: Int;
+		var Diff = Std.int( DU[0] - DC );
+		DC = DU[0];
+		//Encode DC
+		if( Diff == 0 ) {
+			writeBits( HTDC.get(0) ); // Diff might be 0
+		} else {
+			idx = 32767 + Diff;
+			writeBits(HTDC.get( category.get( idx ) ));
+			writeBits( bitcode.get( idx ) );
+		}
+
+		//Encode ACs
+		var end0pos = 63;
+		//for (; (end0pos>0)&&(DU[end0pos]==0); end0pos--) {  };
+		while( (end0pos > 0) && ( DU[end0pos] == 0.0 ) ) end0pos--;
+
+		//end0pos = first element in reverse order !=0
+		if ( end0pos == 0 ) {
+			writeBits(EOB);
+			return DC;
+		}
+		var i = 1;
+		while ( i <= end0pos ) {
+			var startpos = i;
+			//for (; (DU[i]==0) && (i<=end0pos); i++) {  };  <- it's a 'while' loop
+			while( ( DU[i] == 0.0 ) && ( i <= end0pos ) ) i++;
+
+			var nrzeroes: Int = i - startpos;
+			if ( nrzeroes >= 16 ) {
+				//for (var nrmarker: Int=1; nrmarker <= nrzeroes/16; nrmarker++) {
+				for( nrmarker in 0...(nrzeroes >> 4) ) writeBits(M16zeroes);
+				nrzeroes &= 0xF;
+			}
+			idx = 32767 + Std.int( DU[i] );  //<- line added
+			writeBits( HTAC.get( nrzeroes * 16 + category.get( idx ) ) );
+			writeBits( bitcode.get( idx ) );
+			i++;
+		}
+		if( end0pos != 63 ) writeBits(EOB);
+		return DC;
+	}
+
+	var YDU: Array<Float>;
+	var UDU: Array<Float>;
+	var VDU: Array<Float>;
+
+	function ARGB2YUV(img: haxe.io.Bytes, width : Int, xpos: Int, ypos: Int) {
+		var pos = 0;
+		for( y in 0...8 ) {
+			var offset = ((y + ypos) * width + xpos) << 2;
+			for( x in 0...8 ) {
+				offset++; // skip alpha
+				var R = img.get(offset++);
+				var G = img.get(offset++);
+				var B = img.get(offset++);
+				YDU[pos] = ((( 0.29900) * R + ( 0.58700) * G + ( 0.11400) * B)) -128;
+				UDU[pos] = (((-0.16874) * R + (-0.33126) * G + ( 0.50000) * B));
+				VDU[pos] = ((( 0.50000) * R + (-0.41869) * G + (-0.08131) * B));
+				pos++;
+			}
+		}
+	}
+
+	public function new( out : haxe.io.Output ) {
+	//begin : lines added to initialize variables
+		YTable = new Array<Int>();
+		UVTable = new Array<Int>();
+		fdtbl_Y = new Array<Float>();
+		fdtbl_UV = new Array<Float>();
+		for (i in 0...64) {
+			YTable.push(0); UVTable.push(0);
+			fdtbl_Y.push(0.0); fdtbl_UV.push(0.0);
+		}
+
+		bitcode = new Map();  //<- 65535 elements <BitString>
+		category = new Map();  //<- 65535 elements <Int>
+		byteout = out;
+		bytenew = 0;
+		bytepos = 7;
+
+		YDC_HT = new Map();
+		UVDC_HT = new Map();
+		YAC_HT = new Map();
+		UVAC_HT = new Map();
+
+		YDU = new Array<Float>();  //<- 64 elements
+		UDU = new Array<Float>();
+		VDU = new Array<Float>();
+		DU = new Array<Float>();
+		for (i in 0...64) {
+			YDU.push(0.0); UDU.push(0.0); VDU.push(0.0); DU.push(0.0);
+		}
+		initZigZag();
+		initLuminance();
+		initChrominance();
+	//end : lines added to initialize variables
+
+		// Create tables
+		initHuffmanTbl();
+		initCategoryNumber();
+	}
+
+	public function write( image : JpgData, type = 0, off = 0, swapRG = false ) {
+		// init quality table
+		var quality = image.quality;
+		if( quality <= 0 ) quality = 1;
+		if( quality > 100 ) quality = 100;
+		var sf =
+			if( quality < 50 ) Std.int( 5000 / quality )
+			else Std.int( 200 - quality * 2 );
+		initQuantTables(sf);
+
+		// Initialize bit writer
+		bytenew = 0;
+		bytepos = 7;
+
+		var width = image.width;
+		var height = image.height;
+		// Add JPEG headers
+		writeWord(0xFFD8); // SOI
+		writeAPP0();
+		writeDQT();
+		writeSOF0( width, height );
+		writeDHT();
+		writeSOS();
+
+		// Encode 8x8 macroblocks
+		var DCY = 0.0;
+		var DCU = 0.0;
+		var DCV = 0.0;
+		bytenew = 0;
+		bytepos = 7;
+		var ypos = 0;
+		if (type == 0) {
+			while( ypos < height ) {
+				var xpos = 0;
+				while( xpos < width ) {
+					ARGB2YUV(image.pixels, width, xpos, ypos);
+					DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+					DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+					DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+					xpos += 8;
+				}
+				ypos += 8;
+			}
+		}
+		else if (type == 1 && !swapRG) {
+			while( ypos < height ) {
+				var xpos = 0;
+				while( xpos < width ) {
+					RGBA2YUV(image.pixels, width, xpos, ypos);
+					DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+					DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+					DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+					xpos += 8;
+				}
+				ypos += 8;
+			}
+		}
+		else if (type == 1 && swapRG) {
+			while( ypos < height ) {
+				var xpos = 0;
+				while( xpos < width ) {
+					BGRA2YUV(image.pixels, width, xpos, ypos);
+					DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+					DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+					DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+					xpos += 8;
+				}
+				ypos += 8;
+			}
+		}
+		else if (type == 2) {
+			while( ypos < height ) {
+				var xpos = 0;
+				while( xpos < width ) {
+					RRR2YUV(image.pixels, width, xpos, ypos, off);
+					DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+					DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+					DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+					xpos += 8;
+				}
+				ypos += 8;
+			}
+		}
+
+		// Do the bit alignment of the EOI marker
+		if( bytepos >= 0 ) {
+			var fillbits = new BitString( bytepos + 1, ( 1 << (bytepos + 1) ) - 1 );
+			writeBits(fillbits);
+		}
+
+		writeWord(0xFFD9); //EOI
+	}
+
+	// armory
+	function RGBA2YUV(img: haxe.io.Bytes, width : Int, xpos: Int, ypos: Int) {
+		var pos = 0;
+		for( y in 0...8 ) {
+			var offset = ((y + ypos) * width + xpos) << 2;
+			for( x in 0...8 ) {
+				var R = img.get(offset++);
+				var G = img.get(offset++);
+				var B = img.get(offset++);
+				offset++; // skip alpha
+				YDU[pos] = ((( 0.29900) * R + ( 0.58700) * G + ( 0.11400) * B)) -128;
+				UDU[pos] = (((-0.16874) * R + (-0.33126) * G + ( 0.50000) * B));
+				VDU[pos] = ((( 0.50000) * R + (-0.41869) * G + (-0.08131) * B));
+				pos++;
+			}
+		}
+	}
+	function BGRA2YUV(img: haxe.io.Bytes, width : Int, xpos: Int, ypos: Int) {
+		var pos = 0;
+		for( y in 0...8 ) {
+			var offset = ((y + ypos) * width + xpos) << 2;
+			for( x in 0...8 ) {
+				var B = img.get(offset++);
+				var G = img.get(offset++);
+				var R = img.get(offset++);
+				offset++; // skip alpha
+				YDU[pos] = ((( 0.29900) * R + ( 0.58700) * G + ( 0.11400) * B)) -128;
+				UDU[pos] = (((-0.16874) * R + (-0.33126) * G + ( 0.50000) * B));
+				VDU[pos] = ((( 0.50000) * R + (-0.41869) * G + (-0.08131) * B));
+				pos++;
+			}
+		}
+	}
+	function RRR2YUV(img: haxe.io.Bytes, width : Int, xpos: Int, ypos: Int, off: Int) {
+		var pos = 0;
+		for( y in 0...8 ) {
+			var offset = ((y + ypos) * width + xpos) << 2;
+			for( x in 0...8 ) {
+				var R = img.get(offset + off);
+				offset+=4;
+				YDU[pos] = ((( 0.29900) * R + ( 0.58700) * R + ( 0.11400) * R)) -128;
+				UDU[pos] = (((-0.16874) * R + (-0.33126) * R + ( 0.50000) * R));
+				VDU[pos] = ((( 0.50000) * R + (-0.41869) * R + (-0.08131) * R));
+				pos++;
+			}
+		}
+	}
+}
+
+class BitString {
+	public var len: Int;
+	public var val: Int;
+
+	public function new(l: Int, v: Int) {
+		len = l;
+		val = v;
+	}
+}

+ 22 - 0
Sources/arm/format/MeshParser.hx

@@ -0,0 +1,22 @@
+package arm.format;
+
+import iron.math.Vec4;
+
+class MeshParser {
+	public static function pnpoly(v0x: Float, v0y: Float, v1x: Float, v1y: Float, v2x: Float, v2y: Float, px: Float, py: Float): Bool {
+		// https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
+		var c = false;
+		if (((v0y > py) != (v2y > py)) && (px < (v2x - v0x) * (py - v0y) / (v2y - v0y) + v0x)) c = !c;
+		if (((v1y > py) != (v0y > py)) && (px < (v0x - v1x) * (py - v1y) / (v0y - v1y) + v1x)) c = !c;
+		if (((v2y > py) != (v1y > py)) && (px < (v1x - v2x) * (py - v2y) / (v1y - v2y) + v2x)) c = !c;
+		return c;
+	}
+
+	public static function calcNormal(p0: Vec4, p1: Vec4, p2: Vec4): Vec4 {
+		var cb = new iron.math.Vec4().subvecs(p2, p1);
+		var ab = new iron.math.Vec4().subvecs(p0, p1);
+		cb.cross(ab);
+		cb.normalize();
+		return cb;
+	}
+}

+ 500 - 0
Sources/arm/format/ObjParser.hx

@@ -0,0 +1,500 @@
+package arm.format;
+
+class ObjParser {
+
+	public static var splitCode = "o".code; // Object split, "g" for groups, "u"semtl for materials
+	public var posa: kha.arrays.Int16Array = null;
+	public var nora: kha.arrays.Int16Array = null;
+	public var texa: kha.arrays.Int16Array = null;
+	public var inda: kha.arrays.Uint32Array = null;
+	public var udims: Array<kha.arrays.Uint32Array> = null; // Indices split per udim tile
+	public var udimsU = 1; // Number of horizontal udim tiles
+	public var scalePos = 1.0;
+	public var scaleTex = 1.0;
+	public var name = "";
+	public var hasNext = false; // File contains multiple objects
+	public var pos = 0;
+	var posTemp: Array<Float>;
+	var uvTemp: Array<Float>;
+	var norTemp: Array<Float>;
+	var va: kha.arrays.Uint32Array;
+	var ua: kha.arrays.Uint32Array;
+	var na: kha.arrays.Uint32Array;
+	var vi = 0;
+	var ui = 0;
+	var ni = 0;
+	var buf: haxe.io.UInt8Array = null;
+
+	static var vindOff = 0;
+	static var tindOff = 0;
+	static var nindOff = 0;
+	static var bytes: haxe.io.Bytes = null;
+	static var posFirst: Array<Float>;
+	static var uvFirst: Array<Float>;
+	static var norFirst: Array<Float>;
+
+	public function new(blob: kha.Blob, startPos = 0, udim = false) {
+		pos = startPos;
+		var posIndices: Array<Int> = [];
+		var uvIndices: Array<Int> = [];
+		var norIndices: Array<Int> = [];
+		var readingFaces = false;
+		var readingObject = false;
+		var fullAttrib = false;
+		bytes = blob.bytes;
+
+		posTemp = [];
+		uvTemp = [];
+		norTemp = [];
+		va = new kha.arrays.Uint32Array(60);
+		ua = new kha.arrays.Uint32Array(60);
+		na = new kha.arrays.Uint32Array(60);
+		buf = new haxe.io.UInt8Array(64);
+		if (startPos == 0) vindOff = tindOff = nindOff = 0;
+
+		if (splitCode == "u".code && startPos > 0) {
+			posTemp = posFirst;
+			norTemp = norFirst;
+			uvTemp = uvFirst;
+		}
+
+		while (true) {
+			if (pos >= bytes.length) break;
+
+			var c0 = bytes.get(pos++);
+			if (readingObject && readingFaces && (c0 == "v".code || c0 == splitCode)) {
+				pos--;
+				hasNext = true;
+				break;
+			}
+
+			if (c0 == "v".code) {
+				var c1 = bytes.get(pos++);
+				if (c1 == " ".code) {
+					if (bytes.get(pos) == " ".code) pos++; // Some exporters put additional space directly after "v"
+					posTemp.push(readFloat());
+					pos++; // Space
+					posTemp.push(readFloat());
+					pos++; // Space
+					posTemp.push(readFloat());
+				}
+				else if (c1 == "t".code) {
+					pos++; // Space
+					uvTemp.push(readFloat());
+					pos++; // Space
+					uvTemp.push(readFloat());
+					if (norTemp.length > 0) fullAttrib = true;
+				}
+				else if (c1 == "n".code) {
+					pos++; // Space
+					norTemp.push(readFloat());
+					pos++; // Space
+					norTemp.push(readFloat());
+					pos++; // Space
+					norTemp.push(readFloat());
+					if (uvTemp.length > 0) fullAttrib = true;
+				}
+			}
+			else if (c0 == "f".code) {
+				pos++; // Space
+				if (bytes.get(pos) == " ".code) pos++; // Some exporters put additional space directly after "f"
+				readingFaces = true;
+				vi = ui = ni = 0;
+				fullAttrib ? readFaceFast() : readFace();
+
+				if (vi <= 4) { // Convex, fan triangulation
+					posIndices.push(va[0]);
+					posIndices.push(va[1]);
+					posIndices.push(va[2]);
+					for (i in 3...vi) {
+						posIndices.push(va[0]);
+						posIndices.push(va[i - 1]);
+						posIndices.push(va[i]);
+					}
+					if (uvTemp.length > 0) {
+						uvIndices.push(ua[0]);
+						uvIndices.push(ua[1]);
+						uvIndices.push(ua[2]);
+						for (i in 3...ui) {
+							uvIndices.push(ua[0]);
+							uvIndices.push(ua[i - 1]);
+							uvIndices.push(ua[i]);
+						}
+					}
+					if (norTemp.length > 0) {
+						norIndices.push(na[0]);
+						norIndices.push(na[1]);
+						norIndices.push(na[2]);
+						for (i in 3...ni) {
+							norIndices.push(na[0]);
+							norIndices.push(na[i - 1]);
+							norIndices.push(na[i]);
+						}
+					}
+				}
+				else { // Convex or concave, ear clipping
+					var vindOff = splitCode == "u".code ? 0 : vindOff;
+					var nindOff = splitCode == "u".code ? 0 : nindOff;
+					var nx = 0.0;
+					var ny = 0.0;
+					var nz = 0.0;
+					if (norTemp.length > 0) {
+						nx = norTemp[(na[0] - nindOff) * 3    ];
+						ny = norTemp[(na[0] - nindOff) * 3 + 1];
+						nz = norTemp[(na[0] - nindOff) * 3 + 2];
+					}
+					else {
+						var n = MeshParser.calcNormal(
+							new iron.math.Vec4(posTemp[(va[0] - vindOff) * 3], posTemp[(va[0] - vindOff) * 3 + 1], posTemp[(va[0] - vindOff) * 3 + 2]),
+							new iron.math.Vec4(posTemp[(va[1] - vindOff) * 3], posTemp[(va[1] - vindOff) * 3 + 1], posTemp[(va[1] - vindOff) * 3 + 2]),
+							new iron.math.Vec4(posTemp[(va[2] - vindOff) * 3], posTemp[(va[2] - vindOff) * 3 + 1], posTemp[(va[2] - vindOff) * 3 + 2])
+						);
+						nx = n.x;
+						ny = n.y;
+						nz = n.z;
+					}
+					var nxabs = Math.abs(nx);
+					var nyabs = Math.abs(ny);
+					var nzabs = Math.abs(nz);
+					var flip = nx + ny + nz > 0;
+					var axis = nxabs > nyabs && nxabs > nzabs ? 0 : nyabs > nxabs && nyabs > nzabs ? 1 : 2;
+					var axis0 = axis == 0 ? (flip ? 2 : 1) : axis == 1 ? (flip ? 0 : 2) : (flip ? 1 : 0);
+					var axis1 = axis == 0 ? (flip ? 1 : 2) : axis == 1 ? (flip ? 2 : 0) : (flip ? 0 : 1);
+
+					var loops = 0;
+					var i = -1;
+					while (vi > 3 && loops++ < vi) {
+						i = (i + 1) % vi;
+						var i1 = (i + 1) % vi;
+						var i2 = (i + 2) % vi;
+						var vi0 = (va[i ] - vindOff) * 3;
+						var vi1 = (va[i1] - vindOff) * 3;
+						var vi2 = (va[i2] - vindOff) * 3;
+						var v0x = posTemp[vi0 + axis0];
+						var v0y = posTemp[vi0 + axis1];
+						var v1x = posTemp[vi1 + axis0];
+						var v1y = posTemp[vi1 + axis1];
+						var v2x = posTemp[vi2 + axis0];
+						var v2y = posTemp[vi2 + axis1];
+
+						var e0x = v0x - v1x; // Not an interior vertex
+						var e0y = v0y - v1y;
+						var e1x = v2x - v1x;
+						var e1y = v2y - v1y;
+						var cross = e0x * e1y - e0y * e1x;
+						if (cross <= 0) continue;
+
+						var overlap = false; // Other vertex found inside this triangle
+						for (j in 0...vi - 3) {
+							var j0 = (va[(i + 3 + j) % vi] - vindOff) * 3;
+							var px = posTemp[j0 + axis0];
+							var py = posTemp[j0 + axis1];
+							if (MeshParser.pnpoly(v0x, v0y, v1x, v1y, v2x, v2y, px, py)) {
+								overlap = true;
+								break;
+							}
+						}
+						if (overlap) continue;
+
+						posIndices.push(va[i ]); // Found ear
+						posIndices.push(va[i1]);
+						posIndices.push(va[i2]);
+						if (uvTemp.length > 0) {
+							uvIndices.push(ua[i ]);
+							uvIndices.push(ua[i1]);
+							uvIndices.push(ua[i2]);
+						}
+						if (norTemp.length > 0) {
+							norIndices.push(na[i ]);
+							norIndices.push(na[i1]);
+							norIndices.push(na[i2]);
+						}
+
+						for (j in ((i + 1) % vi)...vi - 1) { // Consume vertex
+							va[j] = va[j + 1];
+							ua[j] = ua[j + 1];
+							na[j] = na[j + 1];
+						}
+						vi--;
+						i--;
+						loops = 0;
+					}
+					posIndices.push(va[0]); // Last one
+					posIndices.push(va[1]);
+					posIndices.push(va[2]);
+					if (uvTemp.length > 0) {
+						uvIndices.push(ua[0]);
+						uvIndices.push(ua[1]);
+						uvIndices.push(ua[2]);
+					}
+					if (norTemp.length > 0) {
+						norIndices.push(na[0]);
+						norIndices.push(na[1]);
+						norIndices.push(na[2]);
+					}
+				}
+			}
+			else if (c0 == splitCode) {
+				if (splitCode == "u".code) pos += 5; // "u"semtl
+				pos++; // Space
+				if (!udim) readingObject = true;
+				name = readString();
+			}
+			nextLine();
+		}
+
+		if (startPos > 0) {
+			if (splitCode != "u".code) {
+				for (i in 0...posIndices.length) posIndices[i] -= vindOff;
+				for (i in 0...uvIndices.length) uvIndices[i] -= tindOff;
+				for (i in 0...norIndices.length) norIndices[i] -= nindOff;
+			}
+		}
+		else {
+			if (splitCode == "u".code) {
+				posFirst = posTemp;
+				norFirst = norTemp;
+				uvFirst = uvTemp;
+			}
+		}
+		vindOff += Std.int(posTemp.length / 3); // Assumes separate vertex data per object
+		tindOff += Std.int(uvTemp.length / 2);
+		nindOff += Std.int(norTemp.length / 3);
+
+		// Pack positions to (-1, 1) range
+		scalePos = 0.0;
+		for (i in 0...posTemp.length) {
+			var f = Math.abs(posTemp[i]);
+			if (scalePos < f) scalePos = f;
+		}
+		var inv = 32767 * (1 / scalePos);
+
+		posa = new kha.arrays.Int16Array(posIndices.length * 4);
+		inda = new kha.arrays.Uint32Array(posIndices.length);
+		for (i in 0...posIndices.length) {
+			posa[i * 4    ] = Std.int( posTemp[posIndices[i] * 3    ] * inv);
+			posa[i * 4 + 1] = Std.int(-posTemp[posIndices[i] * 3 + 2] * inv);
+			posa[i * 4 + 2] = Std.int( posTemp[posIndices[i] * 3 + 1] * inv);
+			inda[i] = i;
+		}
+
+		if (norIndices.length > 0) {
+			nora = new kha.arrays.Int16Array(norIndices.length * 2);
+			for (i in 0...posIndices.length) {
+				nora[i * 2    ] = Std.int( norTemp[norIndices[i] * 3    ] * 32767);
+				nora[i * 2 + 1] = Std.int(-norTemp[norIndices[i] * 3 + 2] * 32767);
+				posa[i * 4 + 3] = Std.int( norTemp[norIndices[i] * 3 + 1] * 32767);
+			}
+		}
+		else {
+			// Calc normals
+			nora = new kha.arrays.Int16Array(inda.length * 2);
+			var va = new iron.math.Vec4();
+			var vb = new iron.math.Vec4();
+			var vc = new iron.math.Vec4();
+			var cb = new iron.math.Vec4();
+			var ab = new iron.math.Vec4();
+			for (i in 0...Std.int(inda.length / 3)) {
+				var i1 = inda[i * 3    ];
+				var i2 = inda[i * 3 + 1];
+				var i3 = inda[i * 3 + 2];
+				va.set(posa[i1 * 4], posa[i1 * 4 + 1], posa[i1 * 4 + 2]);
+				vb.set(posa[i2 * 4], posa[i2 * 4 + 1], posa[i2 * 4 + 2]);
+				vc.set(posa[i3 * 4], posa[i3 * 4 + 1], posa[i3 * 4 + 2]);
+				cb.subvecs(vc, vb);
+				ab.subvecs(va, vb);
+				cb.cross(ab);
+				cb.normalize();
+				nora[i1 * 2    ] = Std.int(cb.x * 32767);
+				nora[i1 * 2 + 1] = Std.int(cb.y * 32767);
+				posa[i1 * 4 + 3] = Std.int(cb.z * 32767);
+				nora[i2 * 2    ] = Std.int(cb.x * 32767);
+				nora[i2 * 2 + 1] = Std.int(cb.y * 32767);
+				posa[i2 * 4 + 3] = Std.int(cb.z * 32767);
+				nora[i3 * 2    ] = Std.int(cb.x * 32767);
+				nora[i3 * 2 + 1] = Std.int(cb.y * 32767);
+				posa[i3 * 4 + 3] = Std.int(cb.z * 32767);
+			}
+		}
+
+		if (uvIndices.length > 0) {
+			if (udim) {
+				// Find number of tiles
+				var tilesU = 1;
+				var tilesV = 1;
+				for (i in 0...Std.int(uvTemp.length / 2)) {
+					while (uvTemp[i * 2    ] > tilesU) tilesU++;
+					while (uvTemp[i * 2 + 1] > tilesV) tilesV++;
+				}
+
+				function getTile(i1: Int, i2: Int, i3: Int): Int {
+					var u1 = uvTemp[uvIndices[i1] * 2    ];
+					var v1 = uvTemp[uvIndices[i1] * 2 + 1];
+					var u2 = uvTemp[uvIndices[i2] * 2    ];
+					var v2 = uvTemp[uvIndices[i2] * 2 + 1];
+					var u3 = uvTemp[uvIndices[i3] * 2    ];
+					var v3 = uvTemp[uvIndices[i3] * 2 + 1];
+					var tileU = Std.int((u1 + u2 + u3) / 3);
+					var tileV = Std.int((v1 + v2 + v3) / 3);
+					return tileU + tileV * tilesU;
+				}
+
+				// Amount of indices pre tile
+				var num = new kha.arrays.Uint32Array(tilesU * tilesV);
+				for (i in 0...Std.int(inda.length / 3)) {
+					var tile = getTile(inda[i * 3], inda[i * 3 + 1], inda[i * 3 + 2]);
+					num[tile] += 3;
+				}
+
+				// Split indices per tile
+				udims = [];
+				udimsU = tilesU;
+				for (i in 0...tilesU * tilesV) { udims.push(new kha.arrays.Uint32Array(num[i])); num[i] = 0; }
+
+				for (i in 0...Std.int(inda.length / 3)) {
+					var i1 = inda[i * 3    ];
+					var i2 = inda[i * 3 + 1];
+					var i3 = inda[i * 3 + 2];
+					var tile = getTile(i1, i2, i3);
+					udims[tile][num[tile]++] = i1;
+					udims[tile][num[tile]++] = i2;
+					udims[tile][num[tile]++] = i3;
+				}
+
+				// Normalize uvs to 0-1 range
+				var uvtiles = new kha.arrays.Int16Array(uvTemp.length);
+				for (i in 0...Std.int(inda.length / 3)) { // TODO: merge loops
+					var i1 = inda[i * 3    ];
+					var i2 = inda[i * 3 + 1];
+					var i3 = inda[i * 3 + 2];
+					var tile = getTile(i1, i2, i3);
+					var tileU = tile % tilesU;
+					var tileV = Std.int(tile / tilesU);
+					uvtiles[uvIndices[i1] * 2    ] = tileU;
+					uvtiles[uvIndices[i1] * 2 + 1] = tileV;
+					uvtiles[uvIndices[i2] * 2    ] = tileU;
+					uvtiles[uvIndices[i2] * 2 + 1] = tileV;
+					uvtiles[uvIndices[i3] * 2    ] = tileU;
+					uvtiles[uvIndices[i3] * 2 + 1] = tileV;
+				}
+				for (i in 0...uvtiles.length) uvTemp[i] -= uvtiles[i];
+			}
+
+			texa = new kha.arrays.Int16Array(uvIndices.length * 2);
+			for (i in 0...posIndices.length) {
+				var uvx = uvTemp[uvIndices[i] * 2];
+				if (uvx > 1.0) uvx = uvx - Std.int(uvx);
+				var uvy = uvTemp[uvIndices[i] * 2 + 1];
+				if (uvy > 1.0) uvy = uvy - Std.int(uvy);
+				texa[i * 2    ] = Std.int(       uvx  * 32767);
+				texa[i * 2 + 1] = Std.int((1.0 - uvy) * 32767);
+			}
+		}
+		bytes = null;
+		if (!hasNext) { posFirst = norFirst = uvFirst = null; }
+	}
+
+	function readFaceFast() {
+		while (true) {
+			va[vi++] = readInt() - 1;
+			pos++; // "/"
+			ua[ui++] = readInt() - 1;
+			pos++; // "/"
+			na[ni++] = readInt() - 1;
+			if (bytes.get(pos) == "\n".code || bytes.get(pos) == "\r".code) break;
+			pos++; // " "
+			// Some exporters put space at the end of "f" line
+			if (vi >= 3 && (bytes.get(pos) == "\n".code || bytes.get(pos) == "\r".code)) break;
+		}
+	}
+
+	function readFace() {
+		while (true) {
+			va[vi++] = readInt() - 1;
+			if (uvTemp.length > 0 || norTemp.length > 0) {
+				pos++; // "/"
+			}
+			if (uvTemp.length > 0) {
+				ua[ui++] = readInt() - 1;
+			}
+			if (norTemp.length > 0) {
+				pos++; // "/"
+				na[ni++] = readInt() - 1;
+			}
+			if (bytes.get(pos) == "\n".code || bytes.get(pos) == "\r".code) break;
+			pos++; // " "
+			// Some exporters put space at the end of "f" line
+			if (vi >= 3 && (bytes.get(pos) == "\n".code || bytes.get(pos) == "\r".code)) break;
+		}
+	}
+
+	function readFloat(): Float {
+		var bi = 0;
+		while (true) { // Read into buffer
+			var c = bytes.get(pos);
+			if (c == " ".code || c == "\n".code || c == "\r".code) break;
+			if (c == "E".code || c == "e".code) {
+				pos++;
+				var first = buf[0] == "-".code ? -(buf[1] - 48) : buf[0] - 48;
+				var exp = readInt();
+				var dec = 1;
+				var loop = exp > 0 ? exp : -exp;
+				for (i in 0...loop) dec *= 10;
+				return exp > 0 ? first * dec : first / dec;
+			}
+			pos++;
+			buf[bi++] = c;
+		}
+		var res = 0.0; // Parse buffer into float
+		var dot = 1;
+		var dec = 1;
+		var off = buf[0] == "-".code ? 1 : 0;
+		var len = bi - 1;
+		for (i in 0...bi - off) {
+			var c = buf[len - i];
+			if (c == ".".code) { dot = dec; continue; }
+			res += (c - 48) * dec;
+			dec *= 10;
+		}
+		off > 0 ? res /= -dot : res /= dot;
+		return res;
+	}
+
+	function readInt(): Int {
+		var bi = 0;
+		while (true) { // Read into buffer
+			var c = bytes.get(pos);
+			if (c == "/".code || c == "\n".code || c == "\r".code || c == " ".code) break;
+			pos++;
+			buf[bi++] = c;
+		}
+		var res = 0; // Parse buffer into int
+		var dec = 1;
+		var off = buf[0] == "-".code ? 1 : 0;
+		var len = bi - 1;
+		for (i in 0...bi - off) {
+			res += (buf[len - i] - 48) * dec;
+			dec *= 10;
+		}
+		if (off > 0) res *= -1;
+		return res;
+	}
+
+	function readString(): String {
+		var s = "";
+		while (true) {
+			var c = bytes.get(pos);
+			if (c == "\n".code || c == "\r".code || c == " ".code) break;
+			pos++;
+			s += String.fromCharCode(c);
+		}
+		return s;
+	}
+
+	function nextLine() {
+		while (true) {
+			var c = bytes.get(pos++);
+			if (c == "\n".code || pos >= bytes.length) break; // \n, \r\n
+		}
+	}
+}

+ 51 - 0
Sources/arm/format/PngData.hx

@@ -0,0 +1,51 @@
+/*
+ * format - haXe File Formats
+ *
+ * Copyright (c) 2008-2009, The haXe Project Contributors
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+package arm.format;
+
+enum Color {
+	ColGrey(alpha: Bool); // 1|2|4|8|16 without alpha , 8|16 with alpha
+	ColTrue(alpha: Bool); // 8|16
+	ColIndexed; // 1|2|4|8
+}
+
+typedef Header = {
+	var width: Int;
+	var height: Int;
+	var colbits: Int;
+	var color: Color;
+	var interlaced: Bool;
+}
+
+enum Chunk {
+	CEnd;
+	CHeader(h: Header);
+	CData(b: haxe.io.Bytes);
+	CPalette(b: haxe.io.Bytes);
+	CUnknown(id: String, data: haxe.io.Bytes);
+}
+
+typedef PngData = List<Chunk>;

+ 206 - 0
Sources/arm/format/PngTools.hx

@@ -0,0 +1,206 @@
+/*
+ * format - haXe File Formats
+ *
+ * Copyright (c) 2008-2009, The haXe Project Contributors
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+package arm.format;
+import arm.format.PngData;
+
+class PngTools {
+
+	/**
+		Creates PNG data from bytes that contains one bytes (grey values) for each pixel.
+	**/
+	public static function buildGrey( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgb = haxe.io.Bytes.alloc(width * height + height);
+		// translate RGB to BGR and add filter byte
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgb.set(w++,0); // no filter for this scanline
+			for( x in 0...width )
+				rgb.set(w++,data.get(r++));
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColGrey(false), interlaced : false }));
+		l.add(CData(deflate(rgb,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	/**
+		Creates PNG data from bytes that contains three bytes (R,G and B values) for each pixel.
+	**/
+	public static function buildRGB( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgb = haxe.io.Bytes.alloc(width * height * 3 + height);
+		// translate RGB to BGR and add filter byte
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgb.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgb.set(w++,data.get(r+2));
+				rgb.set(w++,data.get(r+1));
+				rgb.set(w++,data.get(r));
+				r += 3;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(false), interlaced : false }));
+		l.add(CData(deflate(rgb,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	/**
+		Creates PNG data from bytes that contains four bytes in ARGB format for each pixel.
+	**/
+	public static function build32ARGB( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		// translate ARGB to RGBA and add filter byte
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r+1)); // r
+				rgba.set(w++,data.get(r+2)); // g
+				rgba.set(w++,data.get(r+3)); // b
+				rgba.set(w++,data.get(r)); // a
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	/**
+		Creates PNG data from bytes that contains four bytes in BGRA format for each pixel.
+	**/
+	public static function build32BGRA( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		// translate ARGB to RGBA and add filter byte
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r+2)); // r
+				rgba.set(w++,data.get(r+1)); // g
+				rgba.set(w++,data.get(r)); // b
+				rgba.set(w++,data.get(r+3)); // a
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	public static function build32RGBA( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r)); // r
+				rgba.set(w++,data.get(r+1)); // g
+				rgba.set(w++,data.get(r+2)); // b
+				rgba.set(w++,data.get(r+3)); // a
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	// armory
+	public static function build32BGR1( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r+2)); // r
+				rgba.set(w++,data.get(r+1)); // g
+				rgba.set(w++,data.get(r)); // b
+				rgba.set(w++,255); // 1
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	public static function build32RGB1( width : Int, height : Int, data : haxe.io.Bytes, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r)); // r
+				rgba.set(w++,data.get(r+1)); // g
+				rgba.set(w++,data.get(r+2)); // b
+				rgba.set(w++,255); // 1
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	public static function build32RRR1( width : Int, height : Int, data : haxe.io.Bytes, off : Int, ?level = 9 ) : PngData {
+		var rgba = haxe.io.Bytes.alloc(width * height * 4 + height);
+		var w = 0, r = 0;
+		for( y in 0...height ) {
+			rgba.set(w++,0); // no filter for this scanline
+			for( x in 0...width ) {
+				rgba.set(w++,data.get(r+off)); // r
+				rgba.set(w++,data.get(r+off)); // r
+				rgba.set(w++,data.get(r+off)); // r
+				rgba.set(w++,255); // 1
+				r += 4;
+			}
+		}
+		var l = new List();
+		l.add(CHeader({ width : width, height : height, colbits : 8, color : ColTrue(true), interlaced : false }));
+		l.add(CData(deflate(rgba,level)));
+		l.add(CEnd);
+		return l;
+	}
+
+	public static function deflate(b:haxe.io.Bytes, ?level = 9):haxe.io.Bytes {
+		return haxe.io.Bytes.ofData(Krom.deflate(b.getData(), false));
+	}
+}

+ 82 - 0
Sources/arm/format/PngWriter.hx

@@ -0,0 +1,82 @@
+/*
+ * format - haXe File Formats
+ *
+ * Copyright (c) 2008-2009, The haXe Project Contributors
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+package arm.format;
+import arm.format.PngData;
+
+class PngWriter {
+
+	var o : haxe.io.Output;
+
+	public function new(o) {
+		this.o = o;
+		o.bigEndian = true;
+	}
+
+	public function write( png : PngData ) {
+		for( b in [137,80,78,71,13,10,26,10] )
+			o.writeByte(b);
+		for( c in png )
+			switch( c ) {
+			case CHeader(h):
+				var b = new haxe.io.BytesOutput();
+				b.bigEndian = true;
+				b.writeInt32(h.width);
+				b.writeInt32(h.height);
+				b.writeByte(h.colbits);
+				b.writeByte(switch( h.color ) {
+					case ColGrey(alpha): alpha ? 4 : 0;
+					case ColTrue(alpha): alpha ? 6 : 2;
+					case ColIndexed: 3;
+				});
+				b.writeByte(0);
+				b.writeByte(0);
+				b.writeByte(h.interlaced ? 1 : 0);
+				writeChunk("IHDR",b.getBytes());
+			case CEnd:
+				writeChunk("IEND",haxe.io.Bytes.alloc(0));
+			case CData(d):
+				writeChunk("IDAT",d);
+			case CPalette(b):
+				writeChunk("PLTE",b);
+			case CUnknown(id,data):
+				writeChunk(id,data);
+			}
+	}
+
+	function writeChunk( id : String, data : haxe.io.Bytes ) {
+		o.writeInt32(data.length);
+		o.writeString(id);
+		o.write(data);
+		// compute CRC
+		var crc = new haxe.crypto.Crc32();
+		for( i in 0...4 )
+			crc.byte(id.charCodeAt(i));
+		crc.update(data, 0, data.length);
+		o.writeInt32(crc.get());
+	}
+
+}

+ 52 - 0
Sources/arm/geom/Plane.hx

@@ -0,0 +1,52 @@
+package arm.geom;
+
+class Plane {
+
+	public var posa: kha.arrays.Int16Array = null;
+	public var nora: kha.arrays.Int16Array = null;
+	public var texa: kha.arrays.Int16Array = null;
+	public var inda: kha.arrays.Uint32Array = null;
+	public var scalePos = 1.0;
+	public var scaleTex = 1.0;
+	public var name = "";
+	public var hasNext = false;
+
+	public function new(sizeX = 1.0, sizeY = 1.0, vertsX = 2, vertsY = 2) {
+		// Pack positions to (-1, 1) range
+		var halfX = sizeX / 2;
+		var halfY = sizeY / 2;
+		scalePos = Math.max(halfX, halfY);
+		var inv = (1 / scalePos) * 32767;
+
+		posa = new kha.arrays.Int16Array(vertsX * vertsY * 4);
+		nora = new kha.arrays.Int16Array(vertsX * vertsY * 2);
+		texa = new kha.arrays.Int16Array(vertsX * vertsY * 2);
+		inda = new kha.arrays.Uint32Array((vertsX - 1) * (vertsY - 1) * 6);
+		var stepX = sizeX / (vertsX - 1);
+		var stepY = sizeY / (vertsY - 1);
+		for (i in 0...vertsX * vertsY) {
+			var x = (i % vertsX) * stepX - halfX;
+			var y = Std.int(i / vertsX) * stepY - halfY;
+			posa[i * 4    ] = Std.int(x * inv);
+			posa[i * 4 + 1] = Std.int(y * inv);
+			posa[i * 4 + 2] = 0;
+			nora[i * 2    ] = 0;
+			nora[i * 2 + 1] = 0;
+			posa[i * 4 + 3] = 32767;
+			x = (i % vertsX) / (vertsX - 1);
+			y = Std.int(i / vertsX) / (vertsY - 1);
+			texa[i * 2    ] = Std.int(x * 32767);
+			texa[i * 2 + 1] = Std.int(y * 32767);
+		}
+		for (i in 0...(vertsX - 1) * (vertsY - 1)) {
+			var x = i % (vertsX - 1);
+			var y = Std.int(i / (vertsY - 1));
+			inda[i * 6    ] = y * vertsX + x;
+			inda[i * 6 + 1] = y * vertsX + x + 1;
+			inda[i * 6 + 2] = (y + 1) * vertsX + x;
+			inda[i * 6 + 3] = y * vertsX + x + 1;
+			inda[i * 6 + 4] = (y + 1) * vertsX + x + 1;
+			inda[i * 6 + 5] = (y + 1) * vertsX + x;
+		}
+	}
+}

+ 79 - 0
Sources/arm/geom/Sphere.hx

@@ -0,0 +1,79 @@
+package arm.geom;
+
+class Sphere {
+
+	public var posa: kha.arrays.Int16Array = null;
+	public var nora: kha.arrays.Int16Array = null;
+	public var texa: kha.arrays.Int16Array = null;
+	public var inda: kha.arrays.Uint32Array = null;
+	public var scalePos = 1.0;
+	public var scaleTex = 1.0;
+	public var name = "";
+	public var hasNext = false;
+
+	public function new(radius = 1.0, widthSegments = 32, heightSegments = 16) {
+		// Pack positions to (-1, 1) range
+		scalePos = radius;
+		var inv = (1 / scalePos) * 32767;
+		var pi2 = Math.PI * 2;
+
+		var widthVerts = widthSegments + 1;
+		var heightVerts = heightSegments + 1;
+		posa = new kha.arrays.Int16Array(widthVerts * heightVerts * 4);
+		nora = new kha.arrays.Int16Array(widthVerts * heightVerts * 2);
+		texa = new kha.arrays.Int16Array(widthVerts * heightVerts * 2);
+		inda = new kha.arrays.Uint32Array(widthSegments * heightSegments * 6 - widthSegments * 6);
+
+		var nor = new iron.math.Vec4();
+		var pos = 0;
+		for (y in 0...heightVerts) {
+			var v = y / heightSegments;
+			var vFlip = 1.0 - v;
+			var uOff = y == 0 ? 0.5 / widthSegments : y == heightSegments ? -0.5 / widthSegments : 0.0;
+			for (x in 0...widthVerts) {
+				var u = x / widthSegments;
+				var uPI2 = u * pi2;
+				var vPI  = v * Math.PI;
+				var vPIsin = Math.sin(vPI);
+				var vx = -radius * Math.cos(uPI2) * vPIsin;
+				var vy =  radius * Math.cos(vPI);
+				var vz =  radius * Math.sin(uPI2) * vPIsin;
+				var i4 = pos * 4;
+				var i2 = pos * 2;
+				posa[i4    ] = Std.int(vx * inv);
+				posa[i4 + 1] = Std.int(vy * inv);
+				posa[i4 + 2] = Std.int(vz * inv);
+				nor.set(vx, vy, vz).normalize();
+				posa[i4 + 3] = Std.int(nor.z * 32767);
+				nora[i2    ] = Std.int(nor.x * 32767);
+				nora[i2 + 1] = Std.int(nor.y * 32767);
+				texa[i2    ] = Std.int((u + uOff) * 32767);
+				texa[i2 + 1] = Std.int(vFlip      * 32767);
+				pos++;
+			}
+		}
+
+		pos = 0;
+		var heightSegments1 = heightSegments - 1;
+		for (y in 0...heightSegments) {
+			for (x in 0...widthSegments) {
+				var x1 = x + 1;
+				var y1 = y + 1;
+				var a = y  * widthVerts + x1;
+				var b = y  * widthVerts + x;
+				var c = y1 * widthVerts + x;
+				var d = y1 * widthVerts + x1;
+				if (y > 0) {
+					inda[pos++] = a;
+					inda[pos++] = b;
+					inda[pos++] = d;
+				}
+				if (y < heightSegments1) {
+					inda[pos++] = b;
+					inda[pos++] = c;
+					inda[pos++] = d;
+				}
+			}
+		}
+	}
+}

+ 283 - 0
Sources/arm/render/RenderPathRaytrace.hx

@@ -0,0 +1,283 @@
+package arm.render;
+
+import iron.RenderPath;
+import iron.Scene;
+
+#if (kha_direct3d12 || kha_vulkan)
+
+class RenderPathRaytrace {
+
+	public static var frame = 0;
+	public static var raysPix = 0;
+	public static var raysSec = 0;
+	public static var ready = false;
+	public static var dirty = 0;
+	static var path: RenderPath;
+	static var first = true;
+	static var f32 = new kha.arrays.Float32Array(24);
+	static var helpMat = iron.math.Mat4.identity();
+	static var vb_scale = 1.0;
+	static var vb: kha.graphics4.VertexBuffer;
+	static var ib: kha.graphics4.IndexBuffer;
+	static var raysTimer = 0.0;
+	static var raysCounter = 0;
+	static var lastLayer: kha.Image = null;
+	static var lastEnvmap: kha.Image = null;
+	static var isBake = false;
+	static var lastBake = 0;
+
+	#if kha_direct3d12
+	static inline var ext = ".cso";
+	#else
+	static inline var ext = ".spirv";
+	#end
+
+	public static function init(_path: RenderPath) {
+		path = _path;
+	}
+
+	static function commands(useLiveLayer: Bool) {
+		if (!ready || isBake) {
+			ready = true;
+			isBake = false;
+			var mode = Context.pathTraceMode == TraceCore ? "core" : "full";
+			raytraceInit("raytrace_brute_" + mode + ext);
+			lastEnvmap = null;
+		}
+
+		if (!Context.envmapLoaded) Context.loadEnvmap();
+		var probe = Scene.active.world.probe;
+		var savedEnvmap = Context.showEnvmapBlur ? probe.radianceMipmaps[0] : Context.savedEnvmap;
+		var isLive = Config.raw.brush_live && RenderPathPaint.liveLayerDrawn > 0;
+		var layer = (isLive || useLiveLayer) ? RenderPathPaint.liveLayer : Context.layer;
+		if (lastEnvmap != savedEnvmap) {
+			lastEnvmap = savedEnvmap;
+			var bnoise_sobol = Scene.active.embedded.get("bnoise_sobol.k");
+			var bnoise_scramble = Scene.active.embedded.get("bnoise_scramble.k");
+			var bnoise_rank = Scene.active.embedded.get("bnoise_rank.k");
+			Layers.flatten(true);
+			Krom.raytraceSetTextures(Layers.expa.renderTarget_, Layers.expb.renderTarget_, Layers.expc.renderTarget_, savedEnvmap.texture_, bnoise_sobol.texture_, bnoise_scramble.texture_, bnoise_rank.texture_);
+		}
+
+		if (Context.pdirty > 0 || dirty > 0) {
+			Layers.flatten(true);
+		}
+
+		var cam = Scene.active.camera;
+		var ct = cam.transform;
+		helpMat.setFrom(cam.V);
+		helpMat.multmat(cam.P);
+		helpMat.getInverse(helpMat);
+		f32[0] = ct.worldx();
+		f32[1] = ct.worldy();
+		f32[2] = ct.worldz();
+		f32[3] = frame;
+		frame = (frame % 4) + 1; // _PAINT
+		// frame = frame + 1; // _RENDER
+		f32[4] = helpMat._00;
+		f32[5] = helpMat._01;
+		f32[6] = helpMat._02;
+		f32[7] = helpMat._03;
+		f32[8] = helpMat._10;
+		f32[9] = helpMat._11;
+		f32[10] = helpMat._12;
+		f32[11] = helpMat._13;
+		f32[12] = helpMat._20;
+		f32[13] = helpMat._21;
+		f32[14] = helpMat._22;
+		f32[15] = helpMat._23;
+		f32[16] = helpMat._30;
+		f32[17] = helpMat._31;
+		f32[18] = helpMat._32;
+		f32[19] = helpMat._33;
+		f32[20] = Scene.active.world.probe.raw.strength * 1.5;
+		if (!Context.showEnvmap) f32[20] = -f32[20];
+		f32[21] = Context.envmapAngle;
+		f32[22] = Project.layers.length;
+
+		var framebuffer = path.renderTargets.get("buf").image;
+		Krom.raytraceDispatchRays(framebuffer.renderTarget_, f32.buffer);
+
+		if (Context.ddirty == 1 || Context.pdirty == 1) Context.rdirty = 4;
+		Context.ddirty--;
+		Context.pdirty--;
+		Context.rdirty--;
+
+		// Context.ddirty = 1; // _RENDER
+	}
+
+	public static function commandsBake(parsePaintMaterial: ?Bool->Void): Bool {
+		if (!ready || !isBake || lastBake != Context.bakeType) {
+			var rebuild = !(ready && isBake && lastBake != Context.bakeType);
+			lastBake = Context.bakeType;
+			ready = true;
+			isBake = true;
+			lastEnvmap = null;
+			lastLayer = null;
+
+			if (path.renderTargets.get("baketex0") != null) {
+				path.renderTargets.get("baketex0").image.unload();
+				path.renderTargets.get("baketex1").image.unload();
+				path.renderTargets.get("baketex2").image.unload();
+			}
+
+			{
+				var t = new RenderTargetRaw();
+				t.name = "baketex0";
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = "RGBA64";
+				path.createRenderTarget(t);
+			}
+			{
+				var t = new RenderTargetRaw();
+				t.name = "baketex1";
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = "RGBA64";
+				path.createRenderTarget(t);
+			}
+			{
+				var t = new RenderTargetRaw();
+				t.name = "baketex2";
+				t.width = Config.getTextureResX();
+				t.height = Config.getTextureResY();
+				t.format = "RGBA64"; // Match raytrace_target format
+				path.createRenderTarget(t);
+			}
+
+			var _bakeType = Context.bakeType;
+			Context.bakeType = BakeInit;
+			parsePaintMaterial();
+			path.setTarget("baketex0");
+			path.clearTarget(0x00000000); // Pixels with alpha of 0.0 are skipped during raytracing
+			path.setTarget("baketex0", ["baketex1"]);
+			path.drawMeshes("paint");
+			Context.bakeType = _bakeType;
+			function _next() {
+				parsePaintMaterial();
+			}
+			App.notifyOnNextFrame(_next);
+
+			raytraceInit(getBakeShaderName(), rebuild);
+
+			return false;
+		}
+
+		if (!Context.envmapLoaded) Context.loadEnvmap();
+		var probe = Scene.active.world.probe;
+		var savedEnvmap = Context.showEnvmapBlur ? probe.radianceMipmaps[0] : Context.savedEnvmap;
+		if (lastEnvmap != savedEnvmap || lastLayer != Context.layer.texpaint) {
+			lastEnvmap = savedEnvmap;
+			lastLayer = Context.layer.texpaint;
+
+			var baketex0 = path.renderTargets.get("baketex0").image;
+			var baketex1 = path.renderTargets.get("baketex1").image;
+			var bnoise_sobol = Scene.active.embedded.get("bnoise_sobol.k");
+			var bnoise_scramble = Scene.active.embedded.get("bnoise_scramble.k");
+			var bnoise_rank = Scene.active.embedded.get("bnoise_rank.k");
+			var texpaint_undo = RenderPath.active.renderTargets.get("texpaint_undo" + History.undoI).image;
+			Krom.raytraceSetTextures(baketex0.renderTarget_, baketex1.renderTarget_, texpaint_undo.renderTarget_, savedEnvmap.texture_, bnoise_sobol.texture_, bnoise_scramble.texture_, bnoise_rank.texture_);
+		}
+
+		if (Context.brushTime > 0) {
+			Context.pdirty = 2;
+			Context.rdirty = 2;
+		}
+
+		if (Context.pdirty > 0) {
+			f32[0] = frame++;
+			f32[1] = Context.bakeAoStrength;
+			f32[2] = Context.bakeAoRadius;
+			f32[3] = Context.bakeAoOffset;
+			f32[4] = Scene.active.world.probe.raw.strength;
+			f32[5] = Context.bakeUpAxis;
+			f32[6] = Context.envmapAngle;
+
+			var framebuffer = path.renderTargets.get("baketex2").image;
+			Krom.raytraceDispatchRays(framebuffer.renderTarget_, f32.buffer);
+
+			path.setTarget("texpaint" + Context.layer.id);
+			path.bindTarget("baketex2", "tex");
+			path.drawShader("shader_datas/copy_pass/copy_pass");
+
+			raysPix = frame * 64;
+			raysCounter += 64;
+			raysTimer += iron.system.Time.realDelta;
+			if (raysTimer >= 1) {
+				raysSec = raysCounter;
+				raysTimer = 0;
+				raysCounter = 0;
+			}
+			return true;
+		}
+		else {
+			frame = 0;
+			raysTimer = 0;
+			raysCounter = 0;
+			return false;
+		}
+	}
+
+	static function raytraceInit(shaderName: String, build = true) {
+		if (first) {
+			first = false;
+			Scene.active.embedData("bnoise_sobol.k", function() {});
+			Scene.active.embedData("bnoise_scramble.k", function() {});
+			Scene.active.embedData("bnoise_rank.k", function() {});
+		}
+
+		iron.data.Data.getBlob(shaderName, function(shader: kha.Blob) {
+			if (build) buildData();
+			var bnoise_sobol = Scene.active.embedded.get("bnoise_sobol.k");
+			var bnoise_scramble = Scene.active.embedded.get("bnoise_scramble.k");
+			var bnoise_rank = Scene.active.embedded.get("bnoise_rank.k");
+			Krom.raytraceInit(shader.bytes.getData(), untyped vb.buffer, untyped ib.buffer, vb_scale);
+		});
+	}
+
+	static function buildData() {
+		if (Context.mergedObject == null) arm.util.MeshUtil.mergeMesh();
+		var mo = !Context.layerFilterUsed() ? Context.mergedObject : Context.paintObject;
+		var md = mo.data;
+		var geom = md.geom;
+		var mo_scale = mo.transform.scale.x; // Uniform scale only
+		vb_scale = mo.parent.transform.scale.x * md.scalePos * mo_scale;
+		vb = geom.vertexBuffer;
+		ib = geom.indexBuffers[0];
+	}
+
+	static function getBakeShaderName(): String {
+		return
+			Context.bakeType == BakeAO  		? "raytrace_bake_ao" + ext :
+			Context.bakeType == BakeLightmap 	? "raytrace_bake_light" + ext :
+			Context.bakeType == BakeBentNormal  ? "raytrace_bake_bent" + ext :
+												  "raytrace_bake_thick" + ext;
+	}
+
+	public static function draw(useLiveLayer: Bool) {
+		var isLive = Config.raw.brush_live && RenderPathPaint.liveLayerDrawn > 0;
+		if (Context.ddirty > 1 || Context.pdirty > 0 || isLive) frame = 0;
+
+		commands(useLiveLayer);
+
+		if (Config.raw.rp_bloom != false) {
+			RenderPathDeferred.commandsBloom("buf");
+		}
+		path.setTarget("buf");
+		path.drawMeshes("overlay");
+		path.setTarget("buf");
+		Inc.drawCompass(path.currentG);
+		path.setTarget("taa");
+		path.bindTarget("buf", "tex");
+		path.drawShader("shader_datas/compositor_pass/compositor_pass");
+		path.setTarget("");
+		path.bindTarget("taa", "tex");
+		path.drawShader("shader_datas/copy_pass/copy_pass");
+		if (Config.raw.brush_3d) {
+			RenderPathPaint.commandsCursor();
+		}
+	}
+}
+
+#end

+ 1724 - 0
Sources/arm/shader/MaterialParser.hx

@@ -0,0 +1,1724 @@
+//
+// This module builds upon Cycles nodes work licensed as
+// Copyright 2011-2013 Blender Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+package arm.shader;
+
+import zui.Nodes;
+import iron.data.SceneFormat;
+import arm.shader.NodeShader;
+
+class MaterialParser {
+
+	static var con: NodeShaderContext;
+	static var vert: NodeShader;
+	static var frag: NodeShader;
+	static var curshader: NodeShader;
+	static var matcon: TMaterialContext;
+	static var parsed: Array<String>;
+	static var parents: Array<TNode>;
+
+	static var canvases: Array<TNodeCanvas>;
+	static var nodes: Array<TNode>;
+	static var links: Array<TNodeLink>;
+
+	static var cotangentFrameWritten: Bool;
+	static var tex_coord = "texCoord";
+	static inline var eps = 0.000001;
+
+	public static var customNodes = js.Syntax.code("new Map()");
+	public static var parse_surface = true;
+	public static var parse_opacity = true;
+	public static var parse_height = false;
+	public static var parse_height_as_channel = false;
+	public static var parse_emission = false;
+	public static var parse_subsurface = false;
+	public static var triplanar = false; // Sample using texCoord/1/2 & texCoordBlend
+	public static var sample_keep_aspect = false; // Adjust uvs to preserve texture aspect ratio
+	public static var sample_uv_scale = '1.0';
+
+	public static var blur_passthrough = false;
+	public static var warp_passthrough = false;
+	public static var bake_passthrough = false;
+	public static var bake_passthrough_strength = "0.0";
+	public static var bake_passthrough_radius = "0.0";
+	public static var bake_passthrough_offset = "0.0";
+	public static var start_group: TNodeCanvas = null;
+	public static var start_parents: Array<TNode> = null;
+	public static var start_node: TNode = null;
+
+	public static var arm_export_tangents = true;
+	public static var out_normaltan: String; // Raw tangent space normal parsed from normal map
+
+	public static var script_links: Map<String, String> = null;
+
+	public static function getNode(id: Int): TNode {
+		for (n in nodes) if (n.id == id) return n;
+		return null;
+	}
+
+	public static function getLink(id: Int): TNodeLink {
+		for (l in links) if (l.id == id) return l;
+		return null;
+	}
+
+	public static function getInputLink(inp: TNodeSocket): TNodeLink {
+		for (l in links) {
+			if (l.to_id == inp.node_id) {
+				var node = getNode(inp.node_id);
+				if (node.inputs.length <= l.to_socket) return null;
+				if (node.inputs[l.to_socket] == inp) return l;
+			}
+		}
+		return null;
+	}
+
+	public static function getOutputLinks(out: TNodeSocket): Array<TNodeLink> {
+		var ls: Array<TNodeLink> = null;
+		for (l in links) {
+			if (l.from_id == out.node_id) {
+				var node = getNode(out.node_id);
+				if (node.outputs.length <= l.from_socket) continue;
+				if (node.outputs[l.from_socket] == out) {
+					if (ls == null) ls = [];
+					ls.push(l);
+				}
+			}
+		}
+		return ls;
+	}
+
+	static function init() {
+		parsed = [];
+		parents = [];
+		cotangentFrameWritten = false;
+		out_normaltan = "vec3(0.5, 0.5, 1.0)";
+		script_links = null;
+	}
+
+	public static function parse(canvas: TNodeCanvas, _con: NodeShaderContext, _vert: NodeShader, _frag: NodeShader, _matcon: TMaterialContext): TShaderOut {
+		init();
+		canvases = [canvas];
+		nodes = canvas.nodes;
+		links = canvas.links;
+		con = _con;
+		vert = _vert;
+		frag = _frag;
+		curshader = frag;
+		matcon = _matcon;
+
+		if (start_group != null) {
+			push_group(start_group);
+			parents = start_parents;
+		}
+
+		if (start_node != null) {
+			var link: TNodeLink = { id: 99999, from_id: start_node.id, from_socket: 0, to_id: -1, to_socket: -1 };
+			write_result(link);
+			return {
+				out_basecol: 'vec3(0.0, 0.0, 0.0)',
+				out_roughness: '0.0',
+				out_metallic: '0.0',
+				out_occlusion: '1.0',
+				out_opacity: '1.0',
+				out_height: '0.0',
+				out_emission: '0.0',
+				out_subsurface: '0.0'
+			}
+		}
+
+		var output_node = node_by_type(nodes, "OUTPUT_MATERIAL");
+		if (output_node != null) {
+			return parse_output(output_node);
+		}
+		output_node = node_by_type(nodes, "OUTPUT_MATERIAL_PBR");
+		if (output_node != null) {
+			return parse_output_pbr(output_node);
+		}
+		return null;
+	}
+
+	public static function finalize(con: NodeShaderContext) {
+		var vert = con.vert;
+		var frag = con.frag;
+
+		if (frag.dotNV) { frag.vVec = true; frag.n = true; }
+		if (frag.vVec) vert.wposition = true;
+
+		if (frag.bposition) {
+			if (triplanar) {
+				frag.write_attrib('vec3 bposition = vec3(
+					texCoord1.x * texCoordBlend.y + texCoord2.x * texCoordBlend.z,
+					texCoord.x * texCoordBlend.x + texCoord2.y * texCoordBlend.z,
+					texCoord.y * texCoordBlend.x + texCoord1.y * texCoordBlend.y);');
+			}
+			else if (frag.ndcpos) {
+				vert.add_out("vec3 bposition");
+				vert.write('bposition = (ndc.xyz / ndc.w);');
+			}
+			else {
+				vert.add_out("vec3 bposition");
+				vert.add_uniform("vec3 dim", "_dim");
+				vert.add_uniform("vec3 hdim", "_halfDim");
+				vert.write_attrib('bposition = (pos.xyz + hdim) / dim;');
+			}
+		}
+		if (frag.wposition) {
+			vert.add_uniform("mat4 W", "_worldMatrix");
+			vert.add_out("vec3 wposition");
+			vert.write_attrib('wposition = vec4(mul(vec4(pos.xyz, 1.0), W)).xyz;');
+		}
+		else if (vert.wposition) {
+			vert.add_uniform("mat4 W", "_worldMatrix");
+			vert.write_attrib('vec3 wposition = vec4(mul(vec4(pos.xyz, 1.0), W)).xyz;');
+		}
+		if (frag.vposition) {
+			vert.add_uniform("mat4 WV", "_worldViewMatrix");
+			vert.add_out("vec3 vposition");
+			vert.write_attrib('vposition = vec4(mul(vec4(pos.xyz, 1.0), WV)).xyz;');
+		}
+		if (frag.mposition) {
+			vert.add_out("vec3 mposition");
+			if (frag.ndcpos) {
+				vert.write('mposition = (ndc.xyz / ndc.w);');
+			}
+			else {
+				vert.write_attrib('mposition = pos.xyz;');
+			}
+		}
+		if (frag.wtangent) {
+			// con.add_elem("tang", "short4norm");
+			// vert.add_uniform("mat3 N", "_normalMatrix");
+			vert.add_out("vec3 wtangent");
+			// vert.write_attrib('wtangent = normalize(mul(tang.xyz, N));');
+			vert.write_attrib('wtangent = vec3(0.0, 0.0, 0.0);');
+		}
+		if (frag.vVecCam) {
+			vert.add_uniform("mat4 WV", "_worldViewMatrix");
+			vert.add_out("vec3 eyeDirCam");
+			vert.write_attrib('eyeDirCam = vec4(mul(vec4(pos.xyz, 1.0), WV)).xyz; eyeDirCam.z *= -1.0;');
+			frag.write_attrib('vec3 vVecCam = normalize(eyeDirCam);');
+		}
+		if (frag.vVec) {
+			vert.add_uniform("vec3 eye", "_cameraPosition");
+			vert.add_out("vec3 eyeDir");
+			vert.write_attrib('eyeDir = eye - wposition;');
+			frag.write_attrib('vec3 vVec = normalize(eyeDir);');
+		}
+		if (frag.n) {
+			vert.add_uniform("mat3 N", "_normalMatrix");
+			vert.add_out("vec3 wnormal");
+			vert.write_attrib('wnormal = mul(vec3(nor.xy, pos.w), N);');
+			frag.write_attrib('vec3 n = normalize(wnormal);');
+		}
+		else if (vert.n) {
+			vert.add_uniform("mat3 N", "_normalMatrix");
+			vert.write_attrib('vec3 wnormal = normalize(mul(vec3(nor.xy, pos.w), N));');
+		}
+		if (frag.nAttr) {
+			vert.add_out("vec3 nAttr");
+			vert.write_attrib('nAttr = vec3(nor.xy, pos.w);');
+		}
+		if (frag.dotNV) {
+			frag.write_attrib('float dotNV = max(dot(n, vVec), 0.0);');
+		}
+		if (frag.wvpposition) {
+			vert.add_out("vec4 wvpposition");
+			vert.write_end('wvpposition = gl_Position;');
+		}
+		if (con.is_elem('col')) {
+			vert.add_out('vec3 vcolor');
+			vert.write_attrib('vcolor = col.rgb;');
+		}
+	}
+
+	static function parse_output(node: TNode): TShaderOut {
+		if (parse_surface || parse_opacity) {
+			return parse_shader_input(node.inputs[0]);
+		}
+		return null;
+		// Parse volume, displacement..
+	}
+
+	static function parse_output_pbr(node: TNode): TShaderOut {
+		if (parse_surface || parse_opacity) {
+			return parse_shader(node, null);
+		}
+		return null;
+		// Parse volume, displacement..
+	}
+
+	static function get_group(name: String): TNodeCanvas {
+		for (g in Project.materialGroups) if (g.canvas.name == name) return g.canvas;
+		return null;
+	}
+
+	static function push_group(g: TNodeCanvas) {
+		canvases.push(g);
+		nodes = g.nodes;
+		links = g.links;
+	}
+
+	static function pop_group() {
+		canvases.pop();
+		var g = canvases[canvases.length - 1];
+		nodes = g.nodes;
+		links = g.links;
+	}
+
+	static function parse_group(node: TNode, socket: TNodeSocket): String { // Entering group
+		parents.push(node);
+		push_group(get_group(node.name));
+		var output_node = node_by_type(nodes, 'GROUP_OUTPUT');
+		if (output_node == null) return null;
+		var index = socket_index(node, socket);
+		var inp = output_node.inputs[index];
+		var out_group = parse_input(inp);
+		parents.pop();
+		pop_group();
+		return out_group;
+	}
+
+	static function parse_group_input(node: TNode, socket: TNodeSocket): String {
+		var parent = parents.pop(); // Leaving group
+		pop_group();
+		var index = socket_index(node, socket);
+		var inp = parent.inputs[index];
+		var res = parse_input(inp);
+		parents.push(parent); // Return to group
+		push_group(get_group(parent.name));
+		return res;
+	}
+
+	static function parse_input(inp: TNodeSocket): String {
+		if (inp.type == "RGB") {
+			return parse_vector_input(inp);
+		}
+		else if (inp.type == "RGBA") {
+			return parse_vector_input(inp);
+		}
+		else if (inp.type == "VECTOR") {
+			return parse_vector_input(inp);
+		}
+		else if (inp.type == "VALUE") {
+			return parse_value_input(inp);
+		}
+		return null;
+	}
+
+	static function parse_shader_input(inp: TNodeSocket): TShaderOut {
+		var l = getInputLink(inp);
+		var from_node = l != null ? getNode(l.from_id) : null;
+		if (from_node != null) {
+			if (from_node.type == "REROUTE") {
+				return parse_shader_input(from_node.inputs[0]);
+			}
+			return parse_shader(from_node, from_node.outputs[l.from_socket]);
+		}
+		else {
+			return {
+				out_basecol: "vec3(0.8, 0.8, 0.8)",
+				out_roughness: "0.0",
+				out_metallic: "0.0",
+				out_occlusion: "1.0",
+				out_opacity: "1.0",
+				out_height: "0.0",
+				out_emission: "0.0",
+				out_subsurface: "0.0"
+			};
+		}
+	}
+
+	static function parse_shader(node: TNode, socket: TNodeSocket): TShaderOut {
+		var sout: TShaderOut = {
+			out_basecol: "vec3(0.8, 0.8, 0.8)",
+			out_roughness: "0.0",
+			out_metallic: "0.0",
+			out_occlusion: "1.0",
+			out_opacity: "1.0",
+			out_height: "0.0",
+			out_emission: "0.0",
+			out_subsurface: "0.0"
+		}
+
+		if (node.type == "OUTPUT_MATERIAL_PBR") {
+			if (parse_surface) {
+				// Normal - parsed first to retrieve uv coords
+				parse_normal_map_color_input(node.inputs[5]);
+				// Base color
+				sout.out_basecol = parse_vector_input(node.inputs[0]);
+				// Occlusion
+				sout.out_occlusion = parse_value_input(node.inputs[2]);
+				// Roughness
+				sout.out_roughness = parse_value_input(node.inputs[3]);
+				// Metallic
+				sout.out_metallic = parse_value_input(node.inputs[4]);
+				// Emission
+				if (parse_emission) {
+					sout.out_emission = parse_value_input(node.inputs[6]);
+				}
+				// Subsurface
+				if (parse_subsurface) {
+					sout.out_subsurface = parse_value_input(node.inputs[8]);
+				}
+			}
+
+			if (parse_opacity) {
+				sout.out_opacity = parse_value_input(node.inputs[1]);
+			}
+
+			// Displacement / Height
+			if (node.inputs.length > 7 && parse_height) {
+				if (!parse_height_as_channel) curshader = vert;
+				sout.out_height = parse_value_input(node.inputs[7]);
+				if (!parse_height_as_channel) curshader = frag;
+			}
+		}
+
+		return sout;
+	}
+
+	static function parse_vector_input(inp: TNodeSocket): String {
+		var l = getInputLink(inp);
+		var from_node = l != null ? getNode(l.from_id) : null;
+		if (from_node != null) {
+			if (from_node.type == "REROUTE") {
+				return parse_vector_input(from_node.inputs[0]);
+			}
+
+			var res_var = write_result(l);
+			var st = from_node.outputs[l.from_socket].type;
+			if (st == "RGB" || st == "RGBA" || st == "VECTOR") {
+				return res_var;
+			}
+			else {// VALUE
+				return to_vec3(res_var);
+			}
+		}
+		else {
+			if (inp.type == "VALUE") { //# Unlinked reroute
+				return vec3([0.0, 0.0, 0.0]);
+			}
+			else {
+				return vec3(inp.default_value);
+			}
+		}
+	}
+
+	static function parse_vector(node: TNode, socket: TNodeSocket): String {
+		if (node.type == 'GROUP') {
+			return parse_group(node, socket);
+		}
+		else if (node.type == 'GROUP_INPUT') {
+			return parse_group_input(node, socket);
+		}
+		else if (node.type == "ATTRIBUTE") {
+			if (socket == node.outputs[0]) { // Color
+				if (curshader.context.allow_vcols) {
+					curshader.context.add_elem("col", "short4norm"); // Vcols only for now
+					return "vcolor";
+				}
+				else {
+					return("vec3(0.0, 0.0, 0.0)");
+				}
+			}
+			else { // Vector
+				curshader.context.add_elem("tex", "short2norm"); // UVMaps only for now
+				return "vec3(texCoord.x, texCoord.y, 0.0)";
+			}
+		}
+		else if (node.type == "VERTEX_COLOR") {
+			if (curshader.context.allow_vcols) {
+				curshader.context.add_elem("col", "short4norm");
+				return "vcolor";
+			}
+			else {
+				return("vec3(0.0, 0.0, 0.0)");
+			}
+
+		}
+		else if (node.type == "RGB") {
+			return vec3(socket.default_value);
+		}
+		else if (node.type == "TEX_BRICK") {
+			curshader.add_function(ShaderFunctions.str_tex_brick);
+			var co = getCoord(node);
+			var col1 = parse_vector_input(node.inputs[1]);
+			var col2 = parse_vector_input(node.inputs[2]);
+			var col3 = parse_vector_input(node.inputs[3]);
+			var scale = parse_value_input(node.inputs[4]);
+			var res = 'tex_brick($co * $scale, $col1, $col2, $col3)';
+			return res;
+		}
+		else if (node.type == "TEX_CHECKER") {
+			curshader.add_function(ShaderFunctions.str_tex_checker);
+			var co = getCoord(node);
+			var col1 = parse_vector_input(node.inputs[1]);
+			var col2 = parse_vector_input(node.inputs[2]);
+			var scale = parse_value_input(node.inputs[3]);
+			var res = 'tex_checker($co, $col1, $col2, $scale)';
+			return res;
+		}
+		else if (node.type == "TEX_GRADIENT") {
+			var co = getCoord(node);
+			var but = node.buttons[0]; //gradient_type;
+			var grad: String = but.data[but.default_value].toUpperCase();
+			grad = grad.replace(" ", "_");
+			var f = getGradient(grad, co);
+			var res = to_vec3('clamp($f, 0.0, 1.0)');
+			return res;
+		}
+		else if (node.type == "TEX_IMAGE") {
+			// Already fetched
+			if (parsed.indexOf(res_var_name(node, node.outputs[1])) >= 0) { // TODO: node.outputs[0]
+				var varname = store_var_name(node);
+				return '$varname.rgb';
+			}
+			var tex_name = node_name(node);
+			var tex = make_texture(node, tex_name);
+			if (tex != null) {
+				var to_linear = node.buttons[1].default_value == 1; // srgb to linear
+				var texstore = texture_store(node, tex, tex_name, to_linear);
+				return '$texstore.rgb';
+			}
+			else {
+				var tex_store = store_var_name(node); // Pink color for missing texture
+				curshader.write('vec4 $tex_store = vec4(1.0, 0.0, 1.0, 1.0);');
+				return '$tex_store.rgb';
+			}
+		}
+		else if (node.type == "TEX_MAGIC") {
+			curshader.add_function(ShaderFunctions.str_tex_magic);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'tex_magic($co * $scale * 4.0)';
+			return res;
+		}
+		else if (node.type == "TEX_NOISE") {
+			curshader.add_function(ShaderFunctions.str_tex_noise);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'vec3(tex_noise($co * $scale), tex_noise($co * $scale + 0.33), tex_noise($co * $scale + 0.66))';
+			return res;
+		}
+		else if (node.type == "TEX_VORONOI") {
+			curshader.add_function(ShaderFunctions.str_tex_voronoi);
+			curshader.add_uniform("sampler2D snoise256", "$noise256.k");
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var but = node.buttons[0]; //coloring;
+			var coloring: String = but.data[but.default_value].toUpperCase();
+			coloring = coloring.replace(" ", "_");
+			var res = "";
+			if (coloring == "INTENSITY") {
+				res = to_vec3('tex_voronoi($co * $scale, texturePass(snoise256)).a');
+			}
+			else { // Cells
+				res = 'tex_voronoi($co * $scale, texturePass(snoise256)).rgb';
+			}
+			return res;
+		}
+		else if (node.type == "TEX_WAVE") {
+			curshader.add_function(ShaderFunctions.str_tex_wave);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = to_vec3('tex_wave_f($co * $scale)');
+			return res;
+		}
+		else if (node.type == "BRIGHTCONTRAST") {
+			var out_col = parse_vector_input(node.inputs[0]);
+			var bright = parse_value_input(node.inputs[1]);
+			var contr = parse_value_input(node.inputs[2]);
+			curshader.add_function(ShaderFunctions.str_brightcontrast);
+			return 'brightcontrast($out_col, $bright, $contr)';
+		}
+		else if (node.type == "GAMMA") {
+			var out_col = parse_vector_input(node.inputs[0]);
+			var gamma = parse_value_input(node.inputs[1]);
+			return 'pow($out_col, ' + to_vec3('$gamma') + ")";
+		}
+		else if (node.type == "DIRECT_WARP") {
+			if (warp_passthrough) return parse_vector_input(node.inputs[0]);
+			var angle = parse_value_input(node.inputs[1], true);
+			var mask = parse_value_input(node.inputs[2], true);
+			var tex_name = "texwarp_" + node_name(node);
+			curshader.add_uniform("sampler2D " + tex_name, "_" + tex_name);
+			var store = store_var_name(node);
+			curshader.write('float ${store}_rad = $angle * (${Math.PI} / 180);');
+			curshader.write('float ${store}_x = cos(${store}_rad);');
+			curshader.write('float ${store}_y = sin(${store}_rad);');
+			return 'texture($tex_name, texCoord + vec2(${store}_x, ${store}_y) * $mask).rgb;';
+		}
+		else if (node.type == "BLUR") {
+			if (blur_passthrough) return parse_vector_input(node.inputs[0]);
+			var strength = parse_value_input(node.inputs[1]);
+			if (strength == "0.0") return "vec3(0.0, 0.0, 0.0)";
+			var steps = 'int($strength * 10 + 1)';
+			var tex_name = "texblur_" + node_name(node);
+			curshader.add_uniform("sampler2D " + tex_name, "_" + tex_name);
+			var store = store_var_name(node);
+			curshader.write('vec3 ${store}_res = vec3(0.0, 0.0, 0.0);');
+			curshader.write('for (int i = -$steps; i <= $steps; ++i) {');
+			curshader.write('for (int j = -$steps; j <= $steps; ++j) {');
+			curshader.write('${store}_res += texture($tex_name, texCoord + vec2(i, j) / vec2(textureSize($tex_name, 0))).rgb;');
+			curshader.write('}');
+			curshader.write('}');
+			curshader.write('${store}_res /= ($steps * 2 + 1) * ($steps * 2 + 1);');
+			return '${store}_res';
+		}
+		else if (node.type == "HUE_SAT") {
+			curshader.add_function(ShaderFunctions.str_hue_sat);
+			var hue = parse_value_input(node.inputs[0]);
+			var sat = parse_value_input(node.inputs[1]);
+			var val = parse_value_input(node.inputs[2]);
+			var fac = parse_value_input(node.inputs[3]);
+			var col = parse_vector_input(node.inputs[4]);
+			return 'hue_sat($col, vec4($hue-0.5, $sat, $val, 1.0-$fac))';
+		}
+		else if (node.type == "INVERT") {
+			var fac = parse_value_input(node.inputs[0]);
+			var out_col = parse_vector_input(node.inputs[1]);
+			return 'mix($out_col, vec3(1.0, 1.0, 1.0) - ($out_col), $fac)';
+		}
+		else if (node.type == "MIX_RGB") {
+			var fac = parse_value_input(node.inputs[0]);
+			var fac_var = node_name(node) + "_fac";
+			curshader.write('float $fac_var = $fac;');
+			var col1 = parse_vector_input(node.inputs[1]);
+			var col2 = parse_vector_input(node.inputs[2]);
+			var but = node.buttons[0]; // blend_type
+			var blend: String = but.data[but.default_value].toUpperCase();
+			blend = blend.replace(" ", "_");
+			var use_clamp = node.buttons[1].default_value == true;
+			var out_col = "";
+			if (blend == "MIX") {
+				out_col = 'mix($col1, $col2, $fac_var)';
+			}
+			else if (blend == "DARKEN") {
+				out_col = 'min($col1, $col2 * $fac_var)';
+			}
+			else if (blend == "MULTIPLY") {
+				out_col = 'mix($col1, $col1 * $col2, $fac_var)';
+			}
+			else if (blend == "BURN") {
+				out_col = 'mix($col1, vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - $col1) / $col2, $fac_var)';
+			}
+			else if (blend == "LIGHTEN") {
+				out_col = 'max($col1, $col2 * $fac_var)';
+			}
+			else if (blend == "SCREEN") {
+				out_col = '(vec3(1.0, 1.0, 1.0) - (' + to_vec3('1.0 - $fac_var') + ' + $fac_var * (vec3(1.0, 1.0, 1.0) - $col2)) * (vec3(1.0, 1.0, 1.0) - $col1))';
+			}
+			else if (blend == "DODGE") {
+				out_col = 'mix($col1, $col1 / (vec3(1.0, 1.0, 1.0) - $col2), $fac_var)';
+			}
+			else if (blend == "ADD") {
+				out_col = 'mix($col1, $col1 + $col2, $fac_var)';
+			}
+			else if (blend == "OVERLAY") {
+				out_col = 'mix($col1, vec3(
+					$col1.r < 0.5 ? 2.0 * $col1.r * $col2.r : 1.0 - 2.0 * (1.0 - $col1.r) * (1.0 - $col2.r),
+					$col1.g < 0.5 ? 2.0 * $col1.g * $col2.g : 1.0 - 2.0 * (1.0 - $col1.g) * (1.0 - $col2.g),
+					$col1.b < 0.5 ? 2.0 * $col1.b * $col2.b : 1.0 - 2.0 * (1.0 - $col1.b) * (1.0 - $col2.b)
+				), $fac_var)';
+			}
+			else if (blend == "SOFT_LIGHT") {
+				out_col = '((1.0 - $fac_var) * $col1 + $fac_var * ((vec3(1.0, 1.0, 1.0) - $col1) * $col2 * $col1 + $col1 * (vec3(1.0, 1.0, 1.0) - (vec3(1.0, 1.0, 1.0) - $col2) * (vec3(1.0, 1.0, 1.0) - $col1))))';
+			}
+			else if (blend == "LINEAR_LIGHT") {
+				out_col = '($col1 + $fac_var * (vec3(2.0, 2.0, 2.0) * ($col2 - vec3(0.5, 0.5, 0.5))))';
+			}
+			else if (blend == "DIFFERENCE") {
+				out_col = 'mix($col1, abs($col1 - $col2), $fac_var)';
+			}
+			else if (blend == "SUBTRACT") {
+				out_col = 'mix($col1, $col1 - $col2, $fac_var)';
+			}
+			else if (blend == "DIVIDE") {
+				var eps = 0.000001;
+				col2 = 'max($col2, vec3($eps, $eps, $eps))';
+				out_col = "(" + to_vec3('(1.0 - $fac_var) * $col1 + $fac_var * $col1 / $col2') + ")";
+			}
+			else if (blend == "HUE") {
+				curshader.add_function(ShaderFunctions.str_hue_sat);
+				out_col = 'mix($col1, hsv_to_rgb(vec3(rgb_to_hsv($col2).r, rgb_to_hsv($col1).g, rgb_to_hsv($col1).b)), $fac_var)';
+			}
+			else if (blend == "SATURATION") {
+				curshader.add_function(ShaderFunctions.str_hue_sat);
+				out_col = 'mix($col1, hsv_to_rgb(vec3(rgb_to_hsv($col1).r, rgb_to_hsv($col2).g, rgb_to_hsv($col1).b)), $fac_var)';
+			}
+			else if (blend == "COLOR") {
+				curshader.add_function(ShaderFunctions.str_hue_sat);
+				out_col = 'mix($col1, hsv_to_rgb(vec3(rgb_to_hsv($col2).r, rgb_to_hsv($col2).g, rgb_to_hsv($col1).b)), $fac_var)';
+			}
+			else if (blend == "VALUE") {
+				curshader.add_function(ShaderFunctions.str_hue_sat);
+				out_col = 'mix($col1, hsv_to_rgb(vec3(rgb_to_hsv($col1).r, rgb_to_hsv($col1).g, rgb_to_hsv($col2).b)), $fac_var)';
+			}
+			if (use_clamp) return 'clamp($out_col, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0))';
+			else return out_col;
+		}
+		else if (node.type == "VALTORGB") { // ColorRamp
+			var fac = parse_value_input(node.inputs[0]);
+			var interp = node.buttons[0].data == 0 ? "LINEAR" : "CONSTANT";
+			var elems: Array<Array<Float>> = node.buttons[0].default_value;
+			if (elems.length == 1) {
+				return vec3(elems[0]);
+			}
+			// Write cols array
+			var cols_var = node_name(node) + "_cols";
+			curshader.write('vec3 $cols_var[${elems.length}];'); // TODO: Make const
+			for (i in 0...elems.length) {
+				curshader.write('$cols_var[$i] = ${vec3(elems[i])};');
+			}
+			// Get index
+			var fac_var = node_name(node) + "_fac";
+			curshader.write('float $fac_var = $fac;');
+			var index = "0";
+			for (i in 1...elems.length) {
+				index += ' + ($fac_var > ${elems[i][4]} ? 1 : 0)';
+			}
+			// Write index
+			var index_var = node_name(node) + "_i";
+			curshader.write('int $index_var = $index;');
+			if (interp == "CONSTANT") {
+				return '$cols_var[$index_var]';
+			}
+			else { // Linear
+				// Write facs array
+				var facs_var = node_name(node) + "_facs";
+				curshader.write('float $facs_var[${elems.length}];'); // TODO: Make const
+				for (i in 0...elems.length) {
+					curshader.write('$facs_var[$i] = ${elems[i][4]};');
+				}
+				// Mix color
+				// float f = (pos - start) * (1.0 / (finish - start))
+				return 'mix($cols_var[$index_var], $cols_var[$index_var + 1], ($fac_var - $facs_var[$index_var]) * (1.0 / ($facs_var[$index_var + 1] - $facs_var[$index_var]) ))';
+			}
+		}
+		else if (node.type == "CURVE_VEC") {
+			var fac = parse_value_input(node.inputs[0]);
+			var vec = parse_vector_input(node.inputs[1]);
+			var curves = node.buttons[0].default_value;
+			var name = node_name(node);
+			var vc0 = vector_curve(name + "0", vec + ".x", curves[0]);
+			var vc1 = vector_curve(name + "1", vec + ".y", curves[1]);
+			var vc2 = vector_curve(name + "2", vec + ".z", curves[2]);
+			// mapping.curves[0].points[0].handle_type # bezier curve
+			return '(vec3($vc0, $vc1, $vc2) * $fac)';
+		}
+		else if (node.type == "CURVE_RGB") { // RGB Curves
+			var fac = parse_value_input(node.inputs[0]);
+			var vec = parse_vector_input(node.inputs[1]);
+			var curves = node.buttons[0].default_value;
+			var name = node_name(node);
+			// mapping.curves[0].points[0].handle_type
+			var vc0 = vector_curve(name + "0", vec + ".x", curves[0]);
+			var vc1 = vector_curve(name + "1", vec + ".y", curves[1]);
+			var vc2 = vector_curve(name + "2", vec + ".z", curves[2]);
+			var vc3a = vector_curve(name + "3a", vec + ".x", curves[3]);
+			var vc3b = vector_curve(name + "3b", vec + ".y", curves[3]);
+			var vc3c = vector_curve(name + "3c", vec + ".z", curves[3]);
+			return '(sqrt(vec3($vc0, $vc1, $vc2) * vec3($vc3a, $vc3b, $vc3c)) * $fac)';
+		}
+		else if (node.type == "COMBHSV") {
+			curshader.add_function(ShaderFunctions.str_hue_sat);
+			var h = parse_value_input(node.inputs[0]);
+			var s = parse_value_input(node.inputs[1]);
+			var v = parse_value_input(node.inputs[2]);
+			return 'hsv_to_rgb(vec3($h, $s, $v))';
+		}
+		else if (node.type == "COMBRGB") {
+			var r = parse_value_input(node.inputs[0]);
+			var g = parse_value_input(node.inputs[1]);
+			var b = parse_value_input(node.inputs[2]);
+			return 'vec3($r, $g, $b)';
+		}
+		else if (node.type == "WAVELENGTH") {
+			curshader.add_function(ShaderFunctions.str_wavelength_to_rgb);
+			var wl = parse_value_input(node.inputs[0]);
+			curshader.add_function(ShaderFunctions.str_wavelength_to_rgb);
+			return 'wavelength_to_rgb(($wl - 450.0) / 150.0)';
+		}
+		else if (node.type == "CAMERA") {
+			curshader.vVecCam = true;
+			return "vVecCam";
+		}
+		else if (node.type == "LAYER") {
+			var l = node.buttons[0].default_value;
+			if (socket == node.outputs[0]) { // Base
+				curshader.add_uniform("sampler2D texpaint" + l, "_texpaint" + l);
+				return "texture(texpaint" + l + ", texCoord).rgb";
+			}
+			else if (socket == node.outputs[5]) { // Normal
+				curshader.add_uniform("sampler2D texpaint_nor" + l, "_texpaint_nor" + l);
+				return "texture(texpaint_nor" + l + ", texCoord).rgb";
+			}
+		}
+		else if (node.type == "MATERIAL") {
+			var result = "vec3(0.0, 0.0, 0.0)";
+			var mi = node.buttons[0].default_value;
+			if (mi >= Project.materials.length) return result;
+			var m = Project.materials[mi];
+			var _nodes = nodes;
+			var _links = links;
+			nodes = m.canvas.nodes;
+			links = m.canvas.links;
+			parents.push(node);
+			var output_node = node_by_type(nodes, "OUTPUT_MATERIAL_PBR");
+			if (socket == node.outputs[0]) { // Base
+				result = parse_vector_input(output_node.inputs[0]);
+			}
+			else if (socket == node.outputs[5]) { // Normal
+				result = parse_vector_input(output_node.inputs[5]);
+			}
+			nodes = _nodes;
+			links = _links;
+			parents.pop();
+			return result;
+		}
+		else if (node.type == "PICKER") {
+			if (socket == node.outputs[0]) { // Base
+				curshader.add_uniform("vec3 pickerBase", "_pickerBase");
+				return "pickerBase";
+			}
+			else if (socket == node.outputs[5]) { // Normal
+				curshader.add_uniform("vec3 pickerNormal", "_pickerNormal");
+				return "pickerNormal";
+			}
+		}
+		else if (node.type == "NEW_GEOMETRY") {
+			if (socket == node.outputs[0]) { // Position
+				curshader.wposition = true;
+				return "wposition";
+			}
+			else if (socket == node.outputs[1]) { // Normal
+				curshader.n = true;
+				return "n";
+			}
+			else if (socket == node.outputs[2]) { // Tangent
+				curshader.wtangent = true;
+				return "wtangent";
+			}
+			else if (socket == node.outputs[3]) { // True Normal
+				curshader.n = true;
+				return "n";
+			}
+			else if (socket == node.outputs[4]) { // Incoming
+				curshader.vVec = true;
+				return "vVec";
+			}
+			else if (socket == node.outputs[5]) { // Parametric
+				curshader.mposition = true;
+				return "mposition";
+			}
+		}
+		else if (node.type == "OBJECT_INFO") {
+			if (socket == node.outputs[0]) { // Location
+				curshader.wposition = true;
+				return "wposition";
+			}
+			else if (socket == node.outputs[1]) { // Color
+				return "vec3(0.0, 0.0, 0.0)";
+			}
+		}
+		// else if (node.type == "PARTICLE_INFO") {
+		// 	if (socket == node.outputs[3]) { // Location
+		// 		return "vec3(0.0, 0.0, 0.0)";
+		// 	}
+		// 	else if (socket == node.outputs[5]) { // Velocity
+		// 		return "vec3(0.0, 0.0, 0.0)";
+		// 	}
+		// 	else if (socket == node.outputs[6]) { // Angular Velocity
+		// 		return "vec3(0.0, 0.0, 0.0)";
+		// 	}
+		// }
+		else if (node.type == "TANGENT") {
+			curshader.wtangent = true;
+			return "wtangent";
+		}
+		else if (node.type == "TEX_COORD") {
+			if (socket == node.outputs[0]) { // Generated - bounds
+				curshader.bposition = true;
+				return "bposition";
+			}
+			else if (socket == node.outputs[1]) { // Normal
+				curshader.n = true;
+				return "n";
+			}
+			else if (socket == node.outputs[2]) {// UV
+				curshader.context.add_elem("tex", "short2norm");
+				return "vec3(texCoord.x, texCoord.y, 0.0)";
+			}
+			else if (socket == node.outputs[3]) { // Object
+				curshader.mposition = true;
+				return "mposition";
+			}
+			else if (socket == node.outputs[4]) { // Camera
+				curshader.vposition = true;
+				return "vposition";
+			}
+			else if (socket == node.outputs[5]) { // Window
+				curshader.wvpposition = true;
+				return "wvpposition.xyz";
+			}
+			else if (socket == node.outputs[6]) { // Reflection
+				return "vec3(0.0, 0.0, 0.0)";
+			}
+		}
+		else if (node.type == "UVMAP") {
+			curshader.context.add_elem("tex", "short2norm");
+			return "vec3(texCoord.x, texCoord.y, 0.0)";
+		}
+		else if (node.type == "BUMP") {
+			var strength = parse_value_input(node.inputs[0]);
+			// var distance = parse_value_input(node.inputs[1]);
+			var height = parse_value_input(node.inputs[2]);
+			var nor = parse_vector_input(node.inputs[3]);
+			var sample_bump_res = store_var_name(node) + "_bump";
+			curshader.write('float ${sample_bump_res}_x = dFdx($height) * ($strength) * 16.0;');
+			curshader.write('float ${sample_bump_res}_y = dFdy($height) * ($strength) * 16.0;');
+			return '(normalize(vec3(${sample_bump_res}_x, ${sample_bump_res}_y, 1.0) + $nor) * vec3(0.5, 0.5, 0.5) + vec3(0.5, 0.5, 0.5))';
+		}
+		else if (node.type == "MAPPING") {
+			var out = parse_vector_input(node.inputs[0]);
+			var node_translation = parse_vector_input(node.inputs[1]);
+			var node_rotation = parse_vector_input(node.inputs[2]);
+			var node_scale = parse_vector_input(node.inputs[3]);
+			if (node_scale != 'vec3(1, 1, 1)') {
+				out = '($out * $node_scale)';
+			}
+			if (node_rotation != 'vec3(0, 0, 0)') {
+				// ZYX rotation, Z axis for now..
+				var a = '${node_rotation}.z * (3.1415926535 / 180)';
+				// x * cos(theta) - y * sin(theta)
+				// x * sin(theta) + y * cos(theta)
+				out = 'vec3(${out}.x * cos($a) - ${out}.y * sin($a), ${out}.x * sin($a) + ${out}.y * cos($a), 0.0)';
+			}
+			// if node.rotation[1] != 0.0:
+			//     a = node.rotation[1]
+			//     out = 'vec3({0}.x * {1} - {0}.z * {2}, {0}.x * {2} + {0}.z * {1}, 0.0)'.format(out, math.cos(a), math.sin(a))
+			// if node.rotation[0] != 0.0:
+			//     a = node.rotation[0]
+			//     out = 'vec3({0}.y * {1} - {0}.z * {2}, {0}.y * {2} + {0}.z * {1}, 0.0)'.format(out, math.cos(a), math.sin(a))
+			if (node_translation != 'vec3(0, 0, 0)') {
+				out = '($out + $node_translation)';
+			}
+			// if node.use_min:
+				// out = 'max({0}, vec3({1}, {2}, {3}))'.format(out, node.min[0], node.min[1])
+			// if node.use_max:
+				 // out = 'min({0}, vec3({1}, {2}, {3}))'.format(out, node.max[0], node.max[1])
+			return out;
+		}
+		else if (node.type == "NORMAL") {
+			if (socket == node.outputs[0]) {
+				return vec3(node.outputs[0].default_value);
+			}
+			else if (socket == node.outputs[1]) {
+				var nor = parse_vector_input(node.inputs[0]);
+				var norout = vec3(node.outputs[0].default_value);
+				return to_vec3('dot($norout, $nor)');
+			}
+		}
+		else if (node.type == "NORMAL_MAP") {
+			var strength = parse_value_input(node.inputs[0]);
+			parse_normal_map_color_input(node.inputs[1], strength);
+			return null;
+		}
+		else if (node.type == "VECT_TRANSFORM") {
+		// 	#type = node.vector_type
+		// 	#conv_from = node.convert_from
+		// 	#conv_to = node.convert_to
+		// 	# Pass throuh
+		// 	return parse_vector_input(node.inputs[0])
+		}
+		else if (node.type == "COMBXYZ") {
+			var x = parse_value_input(node.inputs[0]);
+			var y = parse_value_input(node.inputs[1]);
+			var z = parse_value_input(node.inputs[2]);
+			return 'vec3($x, $y, $z)';
+		}
+		else if (node.type == "VECT_MATH") {
+			var vec1 = parse_vector_input(node.inputs[0]);
+			var vec2 = parse_vector_input(node.inputs[1]);
+			var but = node.buttons[0]; //operation;
+			var op: String = but.data[but.default_value].toUpperCase();
+			op = op.replace(" ", "_");
+			if (op == "ADD") {
+				return '($vec1 + $vec2)';
+			}
+			else if (op == "SUBTRACT") {
+				return '($vec1 - $vec2)';
+			}
+			else if (op == "AVERAGE") {
+				return '(($vec1 + $vec2) / 2.0)';
+			}
+			else if (op == "DOT_PRODUCT") {
+				return to_vec3('dot($vec1, $vec2)');
+			}
+			else if (op == "CROSS_PRODUCT") {
+				return 'cross($vec1, $vec2)';
+			}
+			else if (op == "NORMALIZE") {
+				return 'normalize($vec1)';
+			}
+		}
+		else if (node.type == "Displacement") {
+			var height = parse_value_input(node.inputs[0]);
+			return to_vec3('$height');
+		}
+		else if (customNodes.get(node.type) != null) {
+			return customNodes.get(node.type)(node, socket);
+		}
+		return "vec3(0.0, 0.0, 0.0)";
+	}
+
+	static function parse_normal_map_color_input(inp: TNodeSocket, strength = "1.0") {
+		frag.write_normal++;
+		out_normaltan = parse_vector_input(inp);
+		if (!arm_export_tangents) {
+			frag.write('vec3 texn = ($out_normaltan) * 2.0 - 1.0;');
+			frag.write('texn.y = -texn.y;');
+			if (!cotangentFrameWritten) {
+				cotangentFrameWritten = true;
+				frag.add_function(ShaderFunctions.str_cotangentFrame);
+			}
+			frag.n = true;
+			#if (kha_direct3d11 || kha_direct3d12 || kha_metal || kha_vulkan)
+			frag.write('mat3 TBN = cotangentFrame(n, vVec, texCoord);');
+			#else
+			frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);');
+			#end
+
+			frag.write('n = mul(normalize(texn), TBN);');
+		}
+		frag.write_normal--;
+	}
+
+	static function parse_value_input(inp: TNodeSocket, vector_as_grayscale = false) : String {
+		var l = getInputLink(inp);
+		var from_node = l != null ? getNode(l.from_id) : null;
+		if (from_node != null) {
+			if (from_node.type == "REROUTE") {
+				return parse_value_input(from_node.inputs[0]);
+			}
+
+			var res_var = write_result(l);
+			var st = from_node.outputs[l.from_socket].type;
+			if (st == "RGB" || st == "RGBA" || st == "VECTOR") {
+				if (vector_as_grayscale) {
+					return 'dot($res_var.rbg, vec3(0.299, 0.587, 0.114))';
+				}
+				else {
+					return '$res_var.x';
+				}
+			}
+			else { // VALUE
+				return res_var;
+			}
+		}
+		else {
+			return vec1(inp.default_value);
+		}
+	}
+
+	static function parse_value(node: TNode, socket: TNodeSocket): String {
+		if (node.type == 'GROUP') {
+			return parse_group(node, socket);
+		}
+		else if (node.type == 'GROUP_INPUT') {
+			return parse_group_input(node, socket);
+		}
+		else if (node.type == "ATTRIBUTE") {
+			curshader.add_uniform("float time", "_time");
+			return "time";
+		}
+		else if (node.type == "VERTEX_COLOR") {
+			return "1.0";
+		}
+		else if (node.type == "WIREFRAME") {
+			curshader.add_uniform('sampler2D texuvmap', '_texuvmap');
+			var use_pixel_size = node.buttons[0].default_value == "true";
+			var pixel_size = parse_value_input(node.inputs[0]);
+			return "textureLod(texuvmap, texCoord, 0.0).r";
+		}
+		else if (node.type == "CAMERA") {
+			if (socket == node.outputs[1]) { // View Z Depth
+				curshader.add_uniform("vec2 cameraProj", "_cameraPlaneProj");
+				#if (kha_direct3d11 || kha_direct3d12 || kha_metal || kha_vulkan)
+				curshader.wvpposition = true;
+				return "(cameraProj.y / ((wvpposition.z / wvpposition.w) - cameraProj.x))";
+				#else
+				return "(cameraProj.y / (gl_FragCoord.z - cameraProj.x))";
+				#end
+			}
+			else { // View Distance
+				curshader.add_uniform("vec3 eye", "_cameraPosition");
+				curshader.wposition = true;
+				return "distance(eye, wposition)";
+			}
+		}
+		else if (node.type == "LAYER") {
+			var l = node.buttons[0].default_value;
+			if (socket == node.outputs[1]) { // Opac
+				curshader.add_uniform("sampler2D texpaint" + l, "_texpaint" + l);
+				return "texture(texpaint" + l + ", texCoord).a";
+			}
+			else if (socket == node.outputs[2]) { // Occ
+				curshader.add_uniform("sampler2D texpaint_pack" + l, "_texpaint_pack" + l);
+				return "texture(texpaint_pack" + l + ", texCoord).r";
+			}
+			else if (socket == node.outputs[3]) { // Rough
+				curshader.add_uniform("sampler2D texpaint_pack" + l, "_texpaint_pack" + l);
+				return "texture(texpaint_pack" + l + ", texCoord).g";
+			}
+			else if (socket == node.outputs[4]) { // Metal
+				curshader.add_uniform("sampler2D texpaint_pack" + l, "_texpaint_pack" + l);
+				return "texture(texpaint_pack" + l + ", texCoord).b";
+			}
+			else if (socket == node.outputs[7]) { // Height
+				curshader.add_uniform("sampler2D texpaint_pack" + l, "_texpaint_pack" + l);
+				return "texture(texpaint_pack" + l + ", texCoord).a";
+			}
+		}
+		else if (node.type == "LAYER_MASK") {
+			if (socket == node.outputs[0]) {
+				var l = node.buttons[0].default_value;
+				curshader.add_uniform("sampler2D texpaint_mask" + l, "_texpaint_mask" + l);
+				return "texture(texpaint_mask" + l + ", texCoord).r";
+			}
+		}
+		else if (node.type == "MATERIAL") {
+			var result = "0.0";
+			var mi = node.buttons[0].default_value;
+			if (mi >= Project.materials.length) return result;
+			var m = Project.materials[mi];
+			var _nodes = nodes;
+			var _links = links;
+			nodes = m.canvas.nodes;
+			links = m.canvas.links;
+			parents.push(node);
+			var output_node = node_by_type(nodes, "OUTPUT_MATERIAL_PBR");
+			if (socket == node.outputs[1]) { // Opac
+				result = parse_value_input(output_node.inputs[1]);
+			}
+			else if (socket == node.outputs[2]) { // Occ
+				result = parse_value_input(output_node.inputs[2]);
+			}
+			else if (socket == node.outputs[3]) { // Rough
+				result = parse_value_input(output_node.inputs[3]);
+			}
+			else if (socket == node.outputs[4]) { // Metal
+				result = parse_value_input(output_node.inputs[4]);
+			}
+			else if (socket == node.outputs[7]) { // Height
+				result = parse_value_input(output_node.inputs[7]);
+			}
+			nodes = _nodes;
+			links = _links;
+			parents.pop();
+			return result;
+		}
+		else if (node.type == "PICKER") {
+			if (socket == node.outputs[1]) {
+				curshader.add_uniform("float pickerOpacity", "_pickerOpacity");
+				return "pickerOpacity";
+			}
+			else if (socket == node.outputs[2]) {
+				curshader.add_uniform("float pickerOcclusion", "_pickerOcclusion");
+				return "pickerOcclusion";
+			}
+			else if (socket == node.outputs[3]) {
+				curshader.add_uniform("float pickerRoughness", "_pickerRoughness");
+				return "pickerRoughness";
+			}
+			else if (socket == node.outputs[4]) {
+				curshader.add_uniform("float pickerMetallic", "_pickerMetallic");
+				return "pickerMetallic";
+			}
+			else if (socket == node.outputs[7]) {
+				curshader.add_uniform("float pickerHeight", "_pickerHeight");
+				return "pickerHeight";
+			}
+		}
+		else if (node.type == "FRESNEL") {
+			var ior = parse_value_input(node.inputs[0]);
+			curshader.dotNV = true;
+			return 'pow(1.0 - dotNV, 7.25 / $ior)';
+		}
+		else if (node.type == "NEW_GEOMETRY") {
+			if (socket == node.outputs[6]) { // Backfacing
+				#if (kha_direct3d11 || kha_direct3d12 || kha_metal || kha_vulkan)
+				return "0.0"; // SV_IsFrontFace
+				#else
+				return "(1.0 - float(gl_FrontFacing))";
+				#end
+			}
+			else if (socket == node.outputs[7]) { // Pointiness
+				var strength = 1.0;
+				var radius = 1.0;
+				var offset = 0.0;
+				var store = store_var_name(node);
+				curshader.n = true;
+				curshader.write('vec3 ${store}_dx = dFdx(n);');
+				curshader.write('vec3 ${store}_dy = dFdy(n);');
+				curshader.write('float ${store}_curvature = max(dot(${store}_dx, ${store}_dx), dot(${store}_dy, ${store}_dy));');
+				curshader.write('${store}_curvature = clamp(pow(${store}_curvature, (1.0 / ' + radius + ') * 0.25) * ' + strength + ' * 2.0 + ' + offset + ' / 10.0, 0.0, 1.0);');
+				return '${store}_curvature';
+			}
+			else if (socket == node.outputs[8]) { // Random Per Island
+				return "0.0";
+			}
+		}
+		else if (node.type == "HAIR_INFO") {
+			return "0.5";
+		}
+		else if (node.type == "LAYER_WEIGHT") {
+			var blend = parse_value_input(node.inputs[0]);
+			if (socket == node.outputs[0]) { // Fresnel
+				curshader.dotNV = true;
+				return 'clamp(pow(1.0 - dotNV, (1.0 - $blend) * 10.0), 0.0, 1.0)';
+			}
+			else if (socket == node.outputs[1]) { // Facing
+				curshader.dotNV = true;
+				return '((1.0 - dotNV) * $blend)';
+			}
+		}
+		else if (node.type == "OBJECT_INFO") {
+			if (socket == node.outputs[1]) { // Object Index
+				curshader.add_uniform("float objectInfoIndex", "_objectInfoIndex");
+				return "objectInfoIndex";
+			}
+			else if (socket == node.outputs[2]) { // Material Index
+				curshader.add_uniform("float objectInfoMaterialIndex", "_objectInfoMaterialIndex");
+				return "objectInfoMaterialIndex";
+			}
+			else if (socket == node.outputs[3]) { // Random
+				curshader.add_uniform("float objectInfoRandom", "_objectInfoRandom");
+				return "objectInfoRandom";
+			}
+		}
+		else if (node.type == "VALUE") {
+			return vec1(node.outputs[0].default_value);
+		}
+		else if (node.type == "TEX_BRICK") {
+			curshader.add_function(ShaderFunctions.str_tex_brick);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[4]);
+			var res = 'tex_brick_f($co * $scale)';
+			return res;
+		}
+		else if (node.type == "TEX_CHECKER") {
+			curshader.add_function(ShaderFunctions.str_tex_checker);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[3]);
+			var res = 'tex_checker_f($co, $scale)';
+			return res;
+		}
+		else if (node.type == "TEX_GRADIENT") {
+			var co = getCoord(node);
+			var but = node.buttons[0]; //gradient_type;
+			var grad: String = but.data[but.default_value].toUpperCase();
+			grad = grad.replace(" ", "_");
+			var f = getGradient(grad, co);
+			var res = '(clamp($f, 0.0, 1.0))';
+			return res;
+		}
+		else if (node.type == "TEX_IMAGE") {
+			// Already fetched
+			if (parsed.indexOf(res_var_name(node, node.outputs[0])) >= 0) { // TODO: node.outputs[1]
+				var varname = store_var_name(node);
+				return '$varname.a';
+			}
+			var tex_name = node_name(node);
+			var tex = make_texture(node, tex_name);
+			if (tex != null) {
+				var to_linear = node.buttons[1].default_value == 1; // srgb to linear
+				var texstore = texture_store(node, tex, tex_name, to_linear);
+				return '$texstore.a';
+			}
+		}
+		else if (node.type == "TEX_MAGIC") {
+			curshader.add_function(ShaderFunctions.str_tex_magic);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'tex_magic_f($co * $scale * 4.0)';
+			return res;
+		}
+		else if (node.type == "TEX_MUSGRAVE") {
+			curshader.add_function(ShaderFunctions.str_tex_musgrave);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'tex_musgrave_f($co * $scale * 0.5)';
+			return res;
+		}
+		else if (node.type == "TEX_NOISE") {
+			curshader.add_function(ShaderFunctions.str_tex_noise);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'tex_noise($co * $scale)';
+			return res;
+		}
+		else if (node.type == "TEX_VORONOI") {
+			curshader.add_function(ShaderFunctions.str_tex_voronoi);
+			curshader.add_uniform("sampler2D snoise256", "$noise256.k");
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var but = node.buttons[0]; // coloring
+			var coloring: String = but.data[but.default_value].toUpperCase();
+			coloring = coloring.replace(" ", "_");
+			var res = "";
+			if (coloring == "INTENSITY") {
+				res = 'tex_voronoi($co * $scale, texturePass(snoise256)).a';
+			}
+			else { // Cells
+				res = 'tex_voronoi($co * $scale, texturePass(snoise256)).r';
+			}
+			return res;
+		}
+		else if (node.type == "TEX_WAVE") {
+			curshader.add_function(ShaderFunctions.str_tex_wave);
+			var co = getCoord(node);
+			var scale = parse_value_input(node.inputs[1]);
+			var res = 'tex_wave_f($co * $scale)';
+			return res;
+		}
+		else if (node.type == "BAKE_CURVATURE") {
+			if (bake_passthrough) {
+				bake_passthrough_strength = parse_value_input(node.inputs[0]);
+				bake_passthrough_radius = parse_value_input(node.inputs[1]);
+				bake_passthrough_offset = parse_value_input(node.inputs[2]);
+				return "0.0";
+			}
+			var tex_name = "texbake_" + node_name(node);
+			curshader.add_uniform("sampler2D " + tex_name, "_" + tex_name);
+			var store = store_var_name(node);
+			curshader.write('float ${store}_res = texture($tex_name, texCoord).r;');
+			return '${store}_res';
+		}
+		else if (node.type == "NORMAL") {
+			var nor = parse_vector_input(node.inputs[0]);
+			var norout = vec3(node.outputs[0].default_value);
+			return 'dot($norout, $nor)';
+		}
+		else if (node.type == "MATH") {
+			var val1 = parse_value_input(node.inputs[0]);
+			var val2 = parse_value_input(node.inputs[1]);
+			var but = node.buttons[0]; // operation
+			var op: String = but.data[but.default_value].toUpperCase();
+			op = op.replace(" ", "_");
+			var use_clamp = node.buttons[1].default_value == true;
+			var out_val = "";
+			if (op == "ADD") {
+				out_val = '($val1 + $val2)';
+			}
+			else if (op == "SUBTRACT") {
+				out_val = '($val1 - $val2)';
+			}
+			else if (op == "MULTIPLY") {
+				out_val = '($val1 * $val2)';
+			}
+			else if (op == "DIVIDE") {
+				val2 = '($val2 == 0.0 ? $eps : $val2)';
+				out_val = '($val1 / $val2)';
+			}
+			else if (op == "POWER") {
+				out_val = 'pow($val1, $val2)';
+			}
+			else if (op == "LOGARITHM") {
+				out_val = 'log($val1)';
+			}
+			else if (op == "SQUARE_ROOT") {
+				out_val = 'sqrt($val1)';
+			}
+			else if (op == "ABSOLUTE") {
+				out_val = 'abs($val1)';
+			}
+			else if (op == "MINIMUM") {
+				out_val = 'min($val1, $val2)';
+			}
+			else if (op == "MAXIMUM") {
+				out_val = 'max($val1, $val2)';
+			}
+			else if (op == "LESS_THAN") {
+				out_val = 'float($val1 < $val2)';
+			}
+			else if (op == "GREATER_THAN") {
+				out_val = 'float($val1 > $val2)';
+			}
+			else if (op == "ROUND") {
+				out_val = 'floor($val1 + 0.5)';
+			}
+			else if (op == "FLOOR") {
+				out_val = 'floor($val1)';
+			}
+			else if (op == "CEIL") {
+				out_val = 'ceil($val1)';
+			}
+			else if (op == "FRACT") {
+				out_val = 'fract($val1)';
+			}
+			else if (op == "MODULO") {
+				out_val = 'mod($val1, $val2)';
+			}
+			else if (op == "SINE") {
+				out_val = 'sin($val1)';
+			}
+			else if (op == "COSINE") {
+				out_val = 'cos($val1)';
+			}
+			else if (op == "TANGENT") {
+				out_val = 'tan($val1)';
+			}
+			else if (op == "ARCSINE") {
+				out_val = 'asin($val1)';
+			}
+			else if (op == "ARCCOSINE") {
+				out_val = 'acos($val1)';
+			}
+			else if (op == "ARCTANGENT") {
+				out_val = 'atan($val1)';
+			}
+			else if (op == "ARCTAN2") {
+				out_val = 'atan($val2, $val1)';
+			}
+			if (use_clamp) {
+				return 'clamp($out_val, 0.0, 1.0)';
+			}
+			else {
+				return out_val;
+			}
+		}
+		else if (node.type == "SCRIPT_CPU") {
+			if (script_links == null) script_links = [];
+			var script = node.buttons[0].default_value;
+			var link = node_name(node);
+			script_links.set(link, script);
+			curshader.add_uniform("float " + link, "_" + link);
+			return link;
+		}
+		else if (node.type == "SHADER_GPU") {
+			var shader = node.buttons[0].default_value;
+			return shader == "" ? "0.0" : shader;
+		}
+		else if (node.type == "RGBTOBW") {
+			var col = parse_vector_input(node.inputs[0]);
+			return '((($col.r * 0.3 + $col.g * 0.59 + $col.b * 0.11) / 3.0) * 2.5)';
+		}
+		else if (node.type == "SEPRGB") {
+			var col = parse_vector_input(node.inputs[0]);
+			if (socket == node.outputs[0]) {
+				return '$col.r';
+			}
+			else if (socket == node.outputs[1]) {
+				return '$col.g';
+			}
+			else if (socket == node.outputs[2]) {
+				return '$col.b';
+			}
+		}
+		else if (node.type == "SEPXYZ") {
+			var vec = parse_vector_input(node.inputs[0]);
+			if (socket == node.outputs[0]) {
+				return '$vec.x';
+			}
+			else if (socket == node.outputs[1]) {
+				return '$vec.y';
+			}
+			else if (socket == node.outputs[2]) {
+				return '$vec.z';
+			}
+		}
+		else if (node.type == "VECT_MATH") {
+			var vec1 = parse_vector_input(node.inputs[0]);
+			var vec2 = parse_vector_input(node.inputs[1]);
+			var but = node.buttons[0]; //operation;
+			var op: String = but.data[but.default_value].toUpperCase();
+			op = op.replace(" ", "_");
+			if (op == "DOT_PRODUCT") {
+				return 'dot($vec1, $vec2)';
+			}
+			else {
+				return "0.0";
+			}
+		}
+		else if (customNodes.get(node.type) != null) {
+			return customNodes.get(node.type)(node, socket);
+		}
+		return "0.0";
+	}
+
+	static function getCoord(node: TNode): String {
+		if (getInputLink(node.inputs[0]) != null) {
+			return parse_vector_input(node.inputs[0]);
+		}
+		else {
+			curshader.bposition = true;
+			return "bposition";
+		}
+	}
+
+	static function getGradient(grad: String, co: String): String {
+		if (grad == "LINEAR") {
+			return '$co.x';
+		}
+		else if (grad == "QUADRATIC") {
+			return "0.0";
+		}
+		else if (grad == "EASING") {
+			return "0.0";
+		}
+		else if (grad == "DIAGONAL") {
+			return '($co.x + $co.y) * 0.5';
+		}
+		else if (grad == "RADIAL") {
+			return 'atan($co.y, $co.x) / (3.141592 * 2.0) + 0.5';
+		}
+		else if (grad == "QUADRATIC_SPHERE") {
+			return "0.0";
+		}
+		else { // "SPHERICAL"
+			return 'max(1.0 - sqrt($co.x * $co.x + $co.y * $co.y + $co.z * $co.z), 0.0)';
+		}
+	}
+
+	static function vector_curve(name: String, fac: String, points: Array<kha.arrays.Float32Array>): String {
+		// Write Ys array
+		var ys_var = name + "_ys";
+		var num = points.length;
+		curshader.write('float $ys_var[$num];'); // TODO: Make const
+		for (i in 0...num) {
+			curshader.write('$ys_var[$i] = ${points[i][1]};');
+		}
+		// Get index
+		var fac_var = name + "_fac";
+		curshader.write('float $fac_var = $fac;');
+		var index = "0";
+		for (i in 1...num) {
+			index += ' + ($fac_var > ${points[i][0]} ? 1 : 0)';
+		}
+		// Write index
+		var index_var = name + "_i";
+		curshader.write('int $index_var = $index;');
+		// Linear
+		// Write Xs array
+		var facs_var = name + "_xs";
+		curshader.write('float $facs_var[$num];'); // TODO: Make const
+		for (i in 0...num) {
+			curshader.write('$facs_var[$i] = ${points[i][0]};');
+		}
+		// Map vector
+		return 'mix($ys_var[$index_var], $ys_var[$index_var + 1], ($fac_var - $facs_var[$index_var]) * (1.0 / ($facs_var[$index_var + 1] - $facs_var[$index_var])))';
+	}
+
+	static function res_var_name(node: TNode, socket: TNodeSocket): String {
+		return node_name(node) + "_" + safesrc(socket.name) + "_res";
+	}
+
+	static var parsedMap = new Map<String, String>();
+	static var textureMap = new Map<String, String>();
+
+	static function write_result(l: TNodeLink): String {
+		var from_node = getNode(l.from_id);
+		var from_socket = from_node.outputs[l.from_socket];
+		var res_var = res_var_name(from_node, from_socket);
+		var st = from_socket.type;
+		if (parsed.indexOf(res_var) < 0) {
+			parsed.push(res_var);
+			if (st == "RGB" || st == "RGBA" || st == "VECTOR") {
+				var res = parse_vector(from_node, from_socket);
+				if (res == null) {
+					return null;
+				}
+				parsedMap.set(res_var, res);
+				curshader.write('vec3 $res_var = $res;');
+			}
+			else if (st == "VALUE") {
+				var res = parse_value(from_node, from_socket);
+				if (res == null) {
+					return null;
+				}
+				parsedMap.set(res_var, res);
+				curshader.write('float $res_var = $res;');
+			}
+		}
+		return res_var;
+	}
+
+	static function store_var_name(node: TNode): String {
+		return node_name(node) + "_store";
+	}
+
+	static function texture_store(node: TNode, tex: TBindTexture, tex_name: String, to_linear = false): String {
+		matcon.bind_textures.push(tex);
+		curshader.context.add_elem("tex", "short2norm");
+		curshader.add_uniform("sampler2D " + tex_name);
+		var uv_name = "";
+		if (getInputLink(node.inputs[0]) != null) {
+			uv_name = parse_vector_input(node.inputs[0]);
+		}
+		else {
+			uv_name = tex_coord;
+		}
+		var tex_store = store_var_name(node);
+
+		if (sample_keep_aspect) {
+			curshader.write('vec2 ${tex_store}_size = textureSize($tex_name, 0);');
+			curshader.write('float ${tex_store}_ax = ${tex_store}_size.x / ${tex_store}_size.y;');
+			curshader.write('float ${tex_store}_ay = ${tex_store}_size.y / ${tex_store}_size.x;');
+			curshader.write('vec2 ${tex_store}_uv = ((${uv_name}.xy / ${sample_uv_scale} - vec2(0.5, 0.5)) * vec2(max(${tex_store}_ay, 1.0), max(${tex_store}_ax, 1.0))) + vec2(0.5, 0.5);');
+			curshader.write('if (${tex_store}_uv.x < 0.0 || ${tex_store}_uv.y < 0.0 || ${tex_store}_uv.x > 1.0 || ${tex_store}_uv.y > 1.0) discard;');
+			curshader.write('${tex_store}_uv *= ${sample_uv_scale};');
+			uv_name = '${tex_store}_uv';
+		}
+
+		if (triplanar) {
+			curshader.write('vec4 $tex_store = vec4(0.0, 0.0, 0.0, 0.0);');
+			curshader.write('if (texCoordBlend.x > 0) $tex_store += texture($tex_name, ${uv_name}.xy) * texCoordBlend.x;');
+			curshader.write('if (texCoordBlend.y > 0) $tex_store += texture($tex_name, ${uv_name}1.xy) * texCoordBlend.y;');
+			curshader.write('if (texCoordBlend.z > 0) $tex_store += texture($tex_name, ${uv_name}2.xy) * texCoordBlend.z;');
+		}
+		else {
+			textureMap.set(tex_store, 'texture($tex_name, $uv_name.xy)');
+			curshader.write('vec4 $tex_store = texture($tex_name, $uv_name.xy);');
+			if (!tex.file.endsWith(".jpg")) { // Pre-mult alpha
+				curshader.write('$tex_store.rgb *= $tex_store.a;');
+			}
+		}
+
+		if (to_linear) {
+			curshader.write('$tex_store.rgb = pow($tex_store.rgb, vec3(2.2, 2.2, 2.2));');
+		}
+		return tex_store;
+	}
+
+	public static inline function vec1(v: Float): String {
+		#if krom_android
+		return 'float($v)';
+		#else
+		return '$v';
+		#end
+	}
+
+	public static inline function vec3(v: Array<Float>): String {
+		#if krom_android
+		return 'vec3(float(${v[0]}), float(${v[1]}), float(${v[2]}))';
+		#else
+		return 'vec3(${v[0]}, ${v[1]}, ${v[2]})';
+		#end
+	}
+
+	public static inline function to_vec3(s: String): String {
+		#if (kha_direct3d11 || kha_direct3d12)
+		return '($s).xxx';
+		#else
+		return 'vec3($s)';
+		#end
+	}
+
+	public static function node_by_type(nodes: Array<TNode>, ntype: String): TNode {
+		for (n in nodes) if (n.type == ntype) return n;
+		return null;
+	}
+
+	static function socket_index(node: TNode, socket: TNodeSocket): Int {
+		for (i in 0...node.outputs.length) if (node.outputs[i] == socket) return i;
+		return -1;
+	}
+
+	public static function node_name(node: TNode, _parents: Array<TNode> = null): String {
+		if (_parents == null) _parents = parents;
+		var s = node.name;
+		for (p in _parents) s = p.name + p.id + '_' + s;
+		s = safesrc(s) + node.id;
+		return s;
+	}
+
+	static function safesrc(s: String): String {
+		for (i in 0...s.length) {
+			var code = s.charCodeAt(i);
+			var letter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
+			var digit = code >= 48 && code <= 57;
+			if (!letter && !digit) s = s.replace(s.charAt(i), "_");
+			if (i == 0 && digit) s = "_" + s;
+		}
+		#if kha_opengl
+		while (s.indexOf("__") >= 0) s = s.replace("__", "_");
+		#end
+		return s;
+	}
+
+	public static function enumData(s: String): String {
+		for (a in Project.assets) if (a.name == s) return a.file;
+		return "";
+	}
+
+	static function make_texture(image_node: TNode, tex_name: String, matname: String = null): TBindTexture {
+
+		var filepath = enumData(App.enumTexts(image_node.type)[image_node.buttons[0].default_value]);
+		if (filepath == "" || filepath.indexOf(".") == -1) {
+			return null;
+		}
+
+		var tex: TBindTexture = {
+			name: tex_name,
+			file: filepath
+		};
+
+		if (Context.textureFilter) {
+			tex.min_filter = "anisotropic";
+			tex.mag_filter = "linear";
+			tex.mipmap_filter = "linear";
+			tex.generate_mipmaps = true;
+		}
+		else {
+			tex.min_filter = "point";
+			tex.mag_filter = "point";
+			tex.mipmap_filter = "no";
+		}
+
+		tex.u_addressing = "repeat";
+		tex.v_addressing = "repeat";
+		return tex;
+	}
+
+	static function is_pow(num: Int): Bool {
+		return ((num & (num - 1)) == 0) && num != 0;
+	}
+
+	static function asset_path(s: String): String {
+		return s;
+	}
+
+	static function extract_filename(s: String): String {
+		var ar = s.split(".");
+		return ar[ar.length - 2] + "." + ar[ar.length - 1];
+	}
+
+	static function safestr(s: String): String {
+		return s;
+	}
+}
+
+typedef TShaderOut = {
+	var out_basecol: String;
+	var out_roughness: String;
+	var out_metallic: String;
+	var out_occlusion: String;
+	var out_opacity: String;
+	var out_height: String;
+	var out_emission: String;
+	var out_subsurface: String;
+}

+ 630 - 0
Sources/arm/shader/NodeShader.hx

@@ -0,0 +1,630 @@
+package arm.shader;
+
+import zui.Nodes;
+import iron.data.SceneFormat;
+
+class NodeShader {
+
+	public var context: NodeShaderContext;
+	var shader_type = '';
+	var includes: Array<String> = [];
+	public var ins: Array<String> = [];
+	public var outs: Array<String> = [];
+	public var sharedSamplers: Array<String> = [];
+	var uniforms: Array<String> = [];
+	var functions = new Map<String, String>();
+	public var main = '';
+	public var main_init = '';
+	public var main_end = '';
+	public var main_normal = '';
+	public var main_textures = '';
+	public var main_attribs = '';
+	var header = '';
+	public var write_pre = false;
+	public var write_normal = 0;
+	public var write_textures = 0;
+	var vstruct_as_vsin = true;
+	var lock = false;
+
+	// References
+	public var bposition = false;
+	public var wposition = false;
+	public var mposition = false;
+	public var vposition = false;
+	public var wvpposition = false;
+	public var ndcpos = false;
+	public var wtangent = false;
+	public var vVec = false;
+	public var vVecCam = false;
+	public var n = false;
+	public var nAttr = false;
+	public var dotNV = false;
+	public var invTBN = false;
+
+	public function new(context: NodeShaderContext, shader_type: String) {
+		this.context = context;
+		this.shader_type = shader_type;
+	}
+
+	public function add_include(s: String) {
+		includes.push(s);
+	}
+
+	public function add_in(s: String) {
+		ins.push(s);
+	}
+
+	public function add_out(s: String) {
+		outs.push(s);
+	}
+
+	public function add_uniform(s: String, link: String = null, included = false) {
+		var ar = s.split(' ');
+		// layout(RGBA8) image3D voxels
+		var utype = ar[ar.length - 2];
+		var uname = ar[ar.length - 1];
+		if (StringTools.startsWith(utype, 'sampler') || StringTools.startsWith(utype, 'image') || StringTools.startsWith(utype, 'uimage')) {
+			var is_image = (StringTools.startsWith(utype, 'image') || StringTools.startsWith(utype, 'uimage')) ? true : false;
+			context.add_texture_unit(utype, uname, link, is_image);
+		}
+		else {
+			// Prefer vec4[] for d3d to avoid padding
+			if (ar[0] == 'float' && ar[1].indexOf('[') >= 0) {
+				ar[0] = 'floats';
+				ar[1] = ar[1].split('[')[0];
+			}
+			else if (ar[0] == 'vec4' && ar[1].indexOf('[') >= 0) {
+				ar[0] = 'floats';
+				ar[1] = ar[1].split('[')[0];
+			}
+			context.add_constant(ar[0], ar[1], link);
+		}
+		if (included == false && uniforms.indexOf(s) == -1) {
+			uniforms.push(s);
+		}
+	}
+
+	public function add_shared_sampler(s: String) {
+		if (sharedSamplers.indexOf(s) == -1) {
+			sharedSamplers.push(s);
+			var ar = s.split(' ');
+			// layout(RGBA8) sampler2D tex
+			var utype = ar[ar.length - 2];
+			var uname = ar[ar.length - 1];
+			context.add_texture_unit(utype, uname, null, false);
+		}
+	}
+
+	public function add_function(s: String) {
+		var fname = s.split('(')[0];
+		if (functions.exists(fname)) return;
+		functions.set(fname, s);
+	}
+
+	public function contains(s: String): Bool {
+		return main.indexOf(s) >= 0 ||
+			   main_init.indexOf(s) >= 0 ||
+			   main_normal.indexOf(s) >= 0 ||
+			   ins.indexOf(s) >= 0 ||
+			   main_textures.indexOf(s) >= 0 ||
+			   main_attribs.indexOf(s) >= 0;
+	}
+
+	public function write_init(s: String) {
+		main_init = s + '\n' + main_init;
+	}
+
+	public function write(s: String) {
+		if (lock) return;
+		if (write_textures > 0) {
+			main_textures += s + '\n';
+		}
+		else if (write_normal > 0) {
+			main_normal += s + '\n';
+		}
+		else if (write_pre) {
+			main_init += s + '\n';
+		}
+		else {
+			main += s + '\n';
+		}
+	}
+
+	public function write_header(s: String) {
+		header += s + '\n';
+	}
+
+	public function write_end(s: String) {
+		main_end += s + '\n';
+	}
+
+	public function write_attrib(s: String) {
+		main_attribs += s + '\n';
+	}
+
+	function dataSize(data: String): String {
+		if (data == 'float1') return '1';
+		else if (data == 'float2') return '2';
+		else if (data == 'float3') return '3';
+		else if (data == 'float4') return '4';
+		else if (data == 'short2norm') return '2';
+		else if (data == 'short4norm') return '4';
+		else return '1';
+	}
+
+	function vstruct_to_vsin() {
+		// if self.shader_type != 'vert' or self.ins != [] or not self.vstruct_as_vsin: # Vertex structure as vertex shader input
+			// return
+		var vs = context.data.vertex_elements;
+		for (e in vs) {
+			add_in('vec' + dataSize(e.data) + ' ' + e.name);
+		}
+	}
+
+	public function get(): String {
+
+		if (shader_type == 'vert' && vstruct_as_vsin) {
+			vstruct_to_vsin();
+		}
+
+		var sharedSampler = 'shared_sampler';
+		if (sharedSamplers.length > 0) {
+			sharedSampler = sharedSamplers[0].split(' ')[1] + '_sampler';
+		}
+
+		#if (kha_direct3d11 || kha_direct3d12)
+		var s = '#define HLSL\n';
+		s += '#define textureArg(tex) Texture2D tex,SamplerState tex ## _sampler\n';
+		s += '#define texturePass(tex) tex,tex ## _sampler\n';
+		s += '#define sampler2D Texture2D\n';
+		s += '#define sampler3D Texture3D\n';
+		s += '#define texture(tex, coord) tex.Sample(tex ## _sampler, coord)\n';
+		s += '#define textureShared(tex, coord) tex.Sample($sharedSampler, coord)\n';
+		s += '#define textureLod(tex, coord, lod) tex.SampleLevel(tex ## _sampler, coord, lod)\n';
+		s += '#define textureLodShared(tex, coord, lod) tex.SampleLevel($sharedSampler, coord, lod)\n';
+		s += '#define texelFetch(tex, coord, lod) tex.Load(float3(coord.xy, lod))\n';
+		s += 'uint2 _GetDimensions(Texture2D tex, uint lod) { uint x, y; tex.GetDimensions(x, y); return uint2(x, y); }\n';
+		s += '#define textureSize _GetDimensions\n';
+		s += '#define mod(a, b) (a % b)\n';
+		s += '#define vec2 float2\n';
+		s += '#define vec3 float3\n';
+		s += '#define vec4 float4\n';
+		s += '#define ivec2 int2\n';
+		s += '#define ivec3 int3\n';
+		s += '#define ivec4 int4\n';
+		s += '#define mat2 float2x2\n';
+		s += '#define mat3 float3x3\n';
+		s += '#define mat4 float4x4\n';
+		s += '#define dFdx ddx\n';
+		s += '#define dFdy ddy\n';
+		s += '#define inversesqrt rsqrt\n';
+		s += '#define fract frac\n';
+		s += '#define mix lerp\n';
+		// s += '#define fma mad\n';
+		s += '#define atan(x, y) atan2(y, x)\n';
+		// s += '#define clamp(x, 0.0, 1.0) saturate(x)\n';
+
+		s += header;
+
+		var in_ext = '';
+		var out_ext = '';
+
+		for (a in includes)
+			s += '#include "' + a + '"\n';
+
+		// Input structure
+		var index = 0;
+		if (ins.length > 0) {
+			s += 'struct SPIRV_Cross_Input {\n';
+			index = 0;
+			ins.sort(function(a, b): Int {
+				// Sort inputs by name
+				return a.substring(4) >= b.substring(4) ? 1 : -1;
+			});
+			for (a in ins) {
+				s += '$a$in_ext : TEXCOORD$index;\n';
+				index++;
+			}
+			// Built-ins
+			if (shader_type == 'vert' && main.indexOf("gl_VertexID") >= 0) {
+				s += 'uint gl_VertexID : SV_VertexID;\n';
+				ins.push('uint gl_VertexID');
+			}
+			if (shader_type == 'vert' && main.indexOf("gl_InstanceID") >= 0) {
+				s += 'uint gl_InstanceID : SV_InstanceID;\n';
+				ins.push('uint gl_InstanceID');
+			}
+			s += '};\n';
+		}
+
+		// Output structure
+		var num = 0;
+		if (outs.length > 0 || shader_type == 'vert') {
+			s += 'struct SPIRV_Cross_Output {\n';
+			outs.sort(function(a, b): Int {
+				// Sort outputs by name
+				return a.substring(4) >= b.substring(4) ? 1 : -1;
+			});
+			index = 0;
+			if (shader_type == 'vert') {
+				for (a in outs) {
+					s += '$a$out_ext : TEXCOORD$index;\n';
+					index++;
+				}
+				s += 'float4 svpos : SV_POSITION;\n';
+			}
+			else {
+				var out = outs[0];
+				// Multiple render targets
+				if (out.charAt(out.length - 1) == ']') {
+					num = Std.parseInt(out.charAt(out.length - 2));
+					s += 'vec4 fragColor[$num] : SV_TARGET0;\n';
+				}
+				else {
+					s += 'vec4 fragColor : SV_TARGET0;\n';
+				}
+			}
+			s += '};\n';
+		}
+
+		for (a in uniforms) {
+			s += 'uniform ' + a + ';\n';
+			if (StringTools.startsWith(a, 'sampler')) {
+				s += 'SamplerState ' + a.split(' ')[1] + '_sampler;\n';
+			}
+		}
+
+		if (sharedSamplers.length > 0) {
+			for (a in sharedSamplers) {
+				s += 'uniform ' + a + ';\n';
+			}
+			s += 'SamplerState $sharedSampler;\n';
+		}
+
+		for (f in functions) {
+			s += f + '\n';
+		}
+
+		// Begin main
+		if (outs.length > 0 || shader_type == 'vert') {
+			if (ins.length > 0) {
+				s += 'SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) {\n';
+			}
+			else {
+				s += 'SPIRV_Cross_Output main() {\n';
+			}
+		}
+		else {
+			if (ins.length > 0) {
+				s += 'void main(SPIRV_Cross_Input stage_input) {\n';
+			}
+			else {
+				s += 'void main() {\n';
+			}
+		}
+
+		// Declare inputs
+		for (a in ins) {
+			var b = a.substring(5); // Remove type 'vec4 '
+			s += '$a = stage_input.$b;\n';
+		}
+
+		if (shader_type == 'vert') {
+			s += 'vec4 gl_Position;\n';
+			for (a in outs) {
+				s += '$a;\n';
+			}
+		}
+		else {
+			if (outs.length > 0) {
+				if (num > 0) s += 'vec4 fragColor[$num];\n';
+				else s += 'vec4 fragColor;\n';
+			}
+		}
+
+		s += main_attribs;
+		s += main_textures;
+		s += main_normal;
+		s += main_init;
+		s += main;
+		s += main_end;
+
+		// Write output structure
+		if (outs.length > 0 || shader_type == 'vert') {
+			s += 'SPIRV_Cross_Output stage_output;\n';
+			if (shader_type == 'vert') {
+				s += 'gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n';
+				s += 'stage_output.svpos = gl_Position;\n';
+				for (a in outs) {
+					var b = a.substring(5); // Remove type 'vec4 '
+					s += 'stage_output.$b = $b;\n';
+				}
+			}
+			else {
+				if (num > 0) {
+					for (i in 0...num) {
+						s += 'stage_output.fragColor[$i] = fragColor[$i];\n';
+					}
+				}
+				else {
+					s += 'stage_output.fragColor = fragColor;\n';
+				}
+			}
+			s += 'return stage_output;\n';
+		}
+		s += '}\n';
+
+		#elseif kha_metal
+
+		var s = '#define METAL\n';
+		s += '#include <metal_stdlib>\n';
+		s += '#include <simd/simd.h>\n';
+		s += 'using namespace metal;\n';
+
+		s += '#define textureArg(tex) texture2d<float> tex,sampler tex ## _sampler\n';
+		s += '#define texturePass(tex) tex,tex ## _sampler\n';
+		s += '#define sampler2D texture2d<float>\n';
+		s += '#define sampler3D texture3d<float>\n';
+		s += '#define texture(tex, coord) tex.sample(tex ## _sampler, coord)\n';
+		s += '#define textureShared(tex, coord) tex.sample($sharedSampler, coord)\n';
+		s += '#define textureLod(tex, coord, lod) tex.sample(tex ## _sampler, coord, level(lod))\n';
+		s += '#define textureLodShared(tex, coord, lod) tex.sample($sharedSampler, coord, level(lod))\n';
+		s += '#define texelFetch(tex, coord, lod) tex.read(uint2(coord), uint(lod))\n';
+		s += 'float2 _getDimensions(texture2d<float> tex, uint lod) { return float2(tex.get_width(lod), tex.get_height(lod)); }\n';
+		s += '#define textureSize _getDimensions\n';
+		s += '#define mod(a, b) fmod(a, b)\n';
+		s += '#define vec2 float2\n';
+		s += '#define vec3 float3\n';
+		s += '#define vec4 float4\n';
+		s += '#define ivec2 int2\n';
+		s += '#define ivec3 int3\n';
+		s += '#define ivec4 int4\n';
+		s += '#define mat2 float2x2\n';
+		s += '#define mat3 float3x3\n';
+		s += '#define mat4 float4x4\n';
+		s += '#define dFdx dfdx\n';
+		s += '#define dFdy dfdy\n';
+		s += '#define inversesqrt rsqrt\n';
+		s += '#define atan(x, y) atan2(y, x)\n';
+		s += '#define mul(a, b) b * a\n';
+		s += '#define discard discard_fragment()\n';
+
+		for (a in includes) {
+			s += '#include "' + a + '"\n';
+		}
+
+		s += header;
+
+		// Input structure
+		var index = 0;
+		//if (ins.length > 0) {
+			s += 'struct main_in {\n';
+			index = 0;
+			ins.sort(function(a, b): Int {
+				// Sort inputs by name
+				return a.substring(4) >= b.substring(4) ? 1 : -1;
+			});
+			if (shader_type == 'vert') {
+				for (a in ins) {
+					s += '$a [[attribute($index)]];\n';
+					index++;
+				}
+			}
+			else {
+				for (a in ins) {
+					s += '$a [[user(locn$index)]];\n';
+					index++;
+				}
+			}
+			s += '};\n';
+		//}
+
+		// Output structure
+		var num = 0;
+		if (outs.length > 0 || shader_type == 'vert') {
+			s += 'struct main_out {\n';
+			outs.sort(function(a, b): Int {
+				// Sort outputs by name
+				return a.substring(4) >= b.substring(4) ? 1 : -1;
+			});
+			index = 0;
+			if (shader_type == 'vert') {
+				for (a in outs) {
+					s += '$a [[user(locn$index)]];\n';
+					index++;
+				}
+				s += 'float4 svpos [[position]];\n';
+			}
+			else {
+				var out = outs[0];
+				// Multiple render targets
+				if (out.charAt(out.length - 1) == ']') {
+					num = Std.parseInt(out.charAt(out.length - 2));
+					for (i in 0...num) {
+						s += 'float4 fragColor_$i [[color($i)]];\n';
+					}
+				}
+				else {
+					s += 'float4 fragColor [[color(0)]];\n';
+				}
+			}
+			s += '};\n';
+		}
+
+		var samplers: Array<String> = [];
+
+		if (uniforms.length > 0) {
+			s += 'struct main_uniforms {\n';
+
+			for (a in uniforms) {
+				if (StringTools.startsWith(a, 'sampler')) {
+					samplers.push(a);
+				}
+				else {
+					s += a + ';\n';
+				}
+			}
+
+			s += '};\n';
+		}
+
+		for (f in functions) {
+			s += f + '\n';
+		}
+
+		// Begin main declaration
+		s += '#undef texture\n';
+
+		s += shader_type == 'vert' ? 'vertex ' : 'fragment ';
+		s += (outs.length > 0 || shader_type == 'vert') ? 'main_out ' : 'void ';
+		s += 'my_main(';
+		//if (ins.length > 0) {
+			s += 'main_in in [[stage_in]]';
+		//}
+		if (uniforms.length > 0) {
+			var bufi = shader_type == 'vert' ? 1 : 0;
+			s += ', constant main_uniforms& uniforms [[buffer($bufi)]]';
+		}
+
+		if (samplers.length > 0) {
+			for (i in 0...samplers.length) {
+				s += ', ${samplers[i]} [[texture($i)]]';
+				s += ', sampler ' + samplers[i].split(' ')[1] + '_sampler [[sampler($i)]]';
+			}
+		}
+
+		if (sharedSamplers.length > 0) {
+			for (i in 0...sharedSamplers.length) {
+				var index = samplers.length + i;
+				s += ', ${sharedSamplers[i]} [[texture($index)]]';
+			}
+			s += ', sampler $sharedSampler [[sampler(${samplers.length})]]';
+		}
+
+		// Built-ins
+		if (shader_type == 'vert' && main.indexOf("gl_VertexID") >= 0) {
+			s += ', uint gl_VertexID [[vertex_id]]';
+		}
+		if (shader_type == 'vert' && main.indexOf("gl_InstanceID") >= 0) {
+			s += ', uint gl_InstanceID [[instance_id]]';
+		}
+
+		// End main declaration
+		s += ') {\n';
+		s += '#define texture(tex, coord) tex.sample(tex ## _sampler, coord)\n';
+
+		// Declare inputs
+		for (a in ins) {
+			var b = a.substring(5); // Remove type 'vec4 '
+			s += '$a = in.$b;\n';
+		}
+
+		for (a in uniforms) {
+			if (!StringTools.startsWith(a, 'sampler')) {
+				var b = a.split(" ")[1]; // Remove type 'vec4 '
+				if (b.indexOf("[") >= 0) {
+					b = b.substring(0, b.indexOf("["));
+					var type = a.split(" ")[0];
+					s += 'constant $type *$b = uniforms.$b;\n';
+				}
+				else {
+					s += '$a = uniforms.$b;\n';
+				}
+			}
+		}
+
+		if (shader_type == 'vert') {
+			s += 'vec4 gl_Position;\n';
+			for (a in outs) {
+				s += '$a;\n';
+			}
+		}
+		else {
+			if (outs.length > 0) {
+				if (num > 0) s += 'vec4 fragColor[$num];\n';
+				else s += 'vec4 fragColor;\n';
+			}
+		}
+
+		s += main_attribs;
+		s += main_textures;
+		s += main_normal;
+		s += main_init;
+		s += main;
+		s += main_end;
+
+		// Write output structure
+		if (outs.length > 0 || shader_type == 'vert') {
+			s += 'main_out out = {};\n';
+			if (shader_type == 'vert') {
+				s += 'gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n';
+				s += 'out.svpos = gl_Position;\n';
+				for (a in outs) {
+					var b = a.split(" ")[1]; // Remove type 'vec4 '
+					s += 'out.$b = $b;\n';
+				}
+			}
+			else {
+				if (num > 0) {
+					for (i in 0...num) {
+						s += 'out.fragColor_$i = fragColor[$i];\n';
+					}
+				}
+				else {
+					s += 'out.fragColor = fragColor;\n';
+				}
+			}
+			s += 'return out;\n';
+		}
+		s += '}\n';
+
+		#else // kha_opengl
+
+		#if kha_vulkan
+		var s = '#version 450\n';
+		#elseif krom_android
+		var s = '#version 300 es\n';
+		if (shader_type == 'frag') {
+			s += 'precision highp float;\n';
+			s += 'precision mediump int;\n';
+		}
+		#else
+		var s = '#version 330\n';
+		#end
+
+		s += '#define textureArg(tex) sampler2D tex\n';
+		s += '#define texturePass(tex) tex\n';
+		s += '#define mul(a, b) b * a\n';
+		s += '#define textureShared texture\n';
+		s += '#define textureLodShared textureLod\n';
+		s += header;
+
+		var in_ext = '';
+		var out_ext = '';
+
+		for (a in includes)
+			s += '#include "' + a + '"\n';
+		for (a in ins)
+			s += 'in $a$in_ext;\n';
+		for (a in outs)
+			s += 'out $a$out_ext;\n';
+		for (a in uniforms)
+			s += 'uniform ' + a + ';\n';
+		for (a in sharedSamplers)
+			s += 'uniform ' + a + ';\n';
+		for (f in functions)
+			s += f + '\n';
+		s += 'void main() {\n';
+		s += main_attribs;
+		s += main_textures;
+		s += main_normal;
+		s += main_init;
+		s += main;
+		s += main_end;
+		s += '}\n';
+
+		#end
+
+		return s;
+	}
+}

+ 119 - 0
Sources/arm/shader/NodeShaderContext.hx

@@ -0,0 +1,119 @@
+package arm.shader;
+
+import zui.Nodes;
+import iron.data.SceneFormat;
+import arm.shader.NodeShaderData;
+
+class NodeShaderContext {
+	public var vert: NodeShader;
+	public var frag: NodeShader;
+	public var geom: NodeShader;
+	public var tesc: NodeShader;
+	public var tese: NodeShader;
+	public var data: TShaderContext;
+	public var allow_vcols = false;
+	var material: TMaterial;
+	var constants: Array<TShaderConstant>;
+	var tunits: Array<TTextureUnit>;
+
+	public function new(material: TMaterial, props: Dynamic) {
+		this.material = material;
+		data = {
+			name: props.name,
+			depth_write: props.depth_write,
+			compare_mode: props.compare_mode,
+			cull_mode: props.cull_mode,
+			blend_source: props.blend_source,
+			blend_destination: props.blend_destination,
+			blend_operation: props.blend_operation,
+			alpha_blend_source: props.alpha_blend_source,
+			alpha_blend_destination: props.alpha_blend_destination,
+			alpha_blend_operation: props.alpha_blend_operation,
+			fragment_shader: '',
+			vertex_shader: '',
+			vertex_elements: Reflect.hasField(props, 'vertex_elements') ? props.vertex_elements : [ {name: "pos", data: 'short4norm'}, {name: "nor", data: 'short2norm'}],
+			color_attachments: props.color_attachments,
+			depth_attachment: props.depth_attachment
+		};
+
+		if (props.color_writes_red != null)
+			data.color_writes_red = props.color_writes_red;
+		if (props.color_writes_green != null)
+			data.color_writes_green = props.color_writes_green;
+		if (props.color_writes_blue != null)
+			data.color_writes_blue = props.color_writes_blue;
+		if (props.color_writes_alpha != null)
+			data.color_writes_alpha = props.color_writes_alpha;
+
+		tunits = data.texture_units = [];
+		constants = data.constants = [];
+	}
+
+	public function add_elem(name: String, data_type: String) {
+		for (e in data.vertex_elements) {
+			if (e.name == name) return;
+		}
+		var elem: TVertexElement = { name: name, data: data_type };
+		data.vertex_elements.push(elem);
+	}
+
+	public function is_elem(name: String): Bool {
+		for (elem in data.vertex_elements)
+			if (elem.name == name)
+				return true;
+		return false;
+	}
+
+	public function get_elem(name: String): TVertexElement {
+		for (elem in data.vertex_elements) {
+			#if cpp
+			if (Reflect.field(elem, "name") == name)
+			#else
+			if (elem.name == name)
+			#end {
+				return elem;
+			}
+		}
+		return null;
+	}
+
+	public function add_constant(ctype: String, name: String, link: String = null) {
+		for (c in constants)
+			if (c.name == name)
+				return;
+
+		var c:TShaderConstant = { name: name, type: ctype };
+		if (link != null)
+			c.link = link;
+		constants.push(c);
+	}
+
+	public function add_texture_unit(ctype: String, name: String, link: String = null, is_image = false) {
+		for (c in tunits) {
+			if (c.name == name) {
+				return;
+			}
+		}
+
+		var c: TTextureUnit = { name: name };
+		if (link != null) {
+			c.link = link;
+		}
+		if (is_image) {
+			c.is_image = is_image;
+		}
+		tunits.push(c);
+	}
+
+	public function make_vert(): NodeShader {
+		data.vertex_shader = material.name + '_' + data.name + '.vert';
+		vert = new NodeShader(this, 'vert');
+		return vert;
+	}
+
+	public function make_frag(): NodeShader {
+		data.fragment_shader = material.name + '_' + data.name + '.frag';
+		frag = new NodeShader(this, 'frag');
+		return frag;
+	}
+}

+ 21 - 0
Sources/arm/shader/NodeShaderData.hx

@@ -0,0 +1,21 @@
+package arm.shader;
+
+import zui.Nodes;
+import iron.data.SceneFormat;
+
+class NodeShaderData {
+	var material: TMaterial;
+
+	public function new(material: TMaterial) {
+		this.material = material;
+	}
+
+	public function add_context(props: Dynamic): NodeShaderContext {
+		return new NodeShaderContext(material, props);
+	}
+}
+
+typedef TMaterial = {
+	var name: String;
+	var canvas: TNodeCanvas;
+}

+ 2738 - 0
Sources/arm/shader/NodesMaterial.hx

@@ -0,0 +1,2738 @@
+package arm.shader;
+
+import haxe.Json;
+import zui.Zui;
+import zui.Id;
+import zui.Nodes;
+import arm.Project;
+
+class NodesMaterial {
+
+	// Mark strings as localizable
+	public static inline function _tr(s: String) { return s; }
+
+	public static var categories = [_tr("Input"), _tr("Texture"), _tr("Color"), _tr("Vector"), _tr("Converter"), _tr("Group")];
+
+	public static var list: Array<Array<TNode>> = [
+		[ // Input
+			{
+				id: 0,
+				name: _tr("Attribute"),
+				type: "ATTRIBUTE",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Name"),
+						type: "STRING"
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Camera Data"),
+				type: "CAMERA",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("View Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("View Z Depth"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("View Distance"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Fresnel"),
+				type: "FRESNEL",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("IOR"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.45,
+						min: 0,
+						max: 3
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Geometry"),
+				type: "NEW_GEOMETRY",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Position"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Tangent"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("True Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Incoming"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Parametric"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Backfacing"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Pointiness"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Random Per Island"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Layer"),
+				type: "LAYER", // extension
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Base Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Opacity"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Occlusion"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Roughness"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Metallic"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal Map"),
+						type: "VECTOR",
+						color: -10238109,
+						default_value: f32([0.5, 0.5, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Emission"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Height"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Subsurface"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Layer"),
+						type: "ENUM",
+						default_value: 0,
+						data: ""
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Layer Mask"),
+				type: "LAYER_MASK", // extension
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Layer"),
+						type: "ENUM",
+						default_value: 0,
+						data: ""
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Layer Weight"),
+				type: "LAYER_WEIGHT",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Blend"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fresnel"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Facing"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Material"),
+				type: "MATERIAL", // extension
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Base Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Opacity"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Occlusion"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Roughness"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Metallic"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal Map"),
+						type: "VECTOR",
+						color: -10238109,
+						default_value: f32([0.5, 0.5, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Emission"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Height"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Subsurface"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Material"),
+						type: "ENUM",
+						default_value: 0,
+						data: ""
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Object Info"),
+				type: "OBJECT_INFO",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Location"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Object Index"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Material Index"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Random"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Picker"),
+				type: "PICKER", // extension
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Base Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Opacity"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Occlusion"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Roughness"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Metallic"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal Map"),
+						type: "VECTOR",
+						color: -10238109,
+						default_value: f32([0.5, 0.5, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Emission"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Height"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Subsurface"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("RGB"),
+				type: "RGB",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.5, 0.5, 0.5, 1.0])
+					}
+				],
+				buttons: [
+					{
+						name: _tr("default_value"),
+						type: "RGBA",
+						output: 0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Script"),
+				type: "SCRIPT_CPU", // extension
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				buttons: [
+					{
+						name: " ",
+						type: "STRING",
+						default_value: ""
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Shader"),
+				type: "SHADER_GPU", // extension
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				buttons: [
+					{
+						name: " ",
+						type: "STRING",
+						default_value: ""
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Tangent"),
+				type: "TANGENT",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Tangent"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Texture Coordinate"),
+				type: "TEX_COORD",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Generated"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("UV"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Object"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Camera"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Window"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Reflection"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("UV Map"),
+				type: "UVMAP",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("UV"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Value"),
+				type: "VALUE",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				buttons: [
+					{
+						name: _tr("default_value"),
+						type: "VALUE",
+						output: 0,
+						min: 0.0,
+						max: 10.0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Vertex Color"),
+				type: "VERTEX_COLOR",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Alpha"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Wireframe"),
+				type: "WIREFRAME",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Size"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.01,
+						max: 0.1
+					},
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Pixel Size"),
+						type: "BOOL",
+						default_value: false,
+						output: 0
+					}
+				]
+			},
+		],
+		// [ // Output
+		// 	{
+		// 		id: 0,
+		// 		name: _tr("Material Output"),
+		// 		type: "OUTPUT_MATERIAL_PBR",
+		// 		x: 0,
+		// 		y: 0,
+		// 		color: 0xffb34f5a,
+		// 		inputs: [
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Base Color"),
+		// 				type: "RGBA",
+		// 				color: 0xffc7c729,
+		// 				default_value: f32([0.8, 0.8, 0.8, 1.0])
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Opacity"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 1.0
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Occlusion"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 1.0
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Roughness"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 0.1
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Metallic"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 0.0
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Normal Map"),
+		// 				type: "VECTOR",
+		// 				color: -10238109,
+		// 				default_value: f32([0.5, 0.5, 1.0])
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Emission"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 0.0
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Height"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 0.0
+		// 			},
+		// 			{
+		// 				id: 0,
+		// 				node_id: 0,
+		// 				name: _tr("Subsurface"),
+		// 				type: "VALUE",
+		// 				color: 0xffa1a1a1,
+		// 				default_value: 0.0
+		// 			}
+		// 		],
+		// 		outputs: [],
+		// 		buttons: []
+		// 	}
+		// ],
+		[ // Texture
+			{
+				id: 0,
+				name: _tr("Brick Texture"),
+				type: "TEX_BRICK",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color 1"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color 2"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.2, 0.2, 0.2])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Mortar"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Checker Texture"),
+				type: "TEX_CHECKER",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color 1"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color 2"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.2, 0.2, 0.2])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Curvature Bake"),
+				type: "BAKE_CURVATURE",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Strength"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0,
+						min: 0.0,
+						max: 2.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Radius"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0,
+						min: 0.0,
+						max: 2.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Offset"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0,
+						min: -2.0,
+						max: 2.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Gradient Texture"),
+				type: "TEX_GRADIENT",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("gradient_type"),
+						type: "ENUM",
+						// data: ["Linear", "Quadratic", "Easing", "Diagonal", "Radial", "Quadratic Sphere", "Spherical"],
+						data: [_tr("Linear"), _tr("Diagonal"), _tr("Radial"), _tr("Spherical")],
+						default_value: 0,
+						output: 0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Image Texture"),
+				type: "TEX_IMAGE",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Alpha"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("File"),
+						type: "ENUM",
+						default_value: 0,
+						data: ""
+					},
+					{
+						name: _tr("Color Space"),
+						type: "ENUM",
+						default_value: 0,
+						data: [_tr("linear"), _tr("srgb")]
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Magic Texture"),
+				type: "TEX_MAGIC",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Musgrave Texture"),
+				type: "TEX_MUSGRAVE",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Height"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Noise Texture"),
+				type: "TEX_NOISE",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Voronoi Texture"),
+				type: "TEX_VORONOI",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("coloring"),
+						type: "ENUM",
+						data: [_tr("Intensity"), _tr("Cells")],
+						default_value: 0,
+						output: 0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Wave Texture"),
+				type: "TEX_WAVE",
+				x: 0,
+				y: 0,
+				color: 0xff4982a0,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 5.0,
+						min: 0.0,
+						max: 10.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: []
+			}
+		],
+		[ // Color
+			{
+				id: 0,
+				name: _tr("Warp"),
+				type: "DIRECT_WARP", // extension
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Angle"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0,
+						min: 0.0,
+						max: 360.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Mask"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5,
+						min: 0.0,
+						max: 1.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Blur"),
+				type: "BLUR", // extension
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Strength"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Bright/Contrast"),
+				type: "BRIGHTCONTRAST",
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Bright"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Contrast"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Gamma"),
+				type: "GAMMA",
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Gamma"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Hue/Saturation"),
+				type: "HUE_SAT",
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Hue"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Saturation"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Invert"),
+				type: "INVERT",
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("MixRGB"),
+				type: "MIX_RGB",
+				x: 0,
+				y: 0,
+				color: 0xff448c6d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color1"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.5, 0.5, 0.5, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color2"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.5, 0.5, 0.5, 1.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: [
+					{
+						name: _tr("blend_type"),
+						type: "ENUM",
+						data: [_tr("Mix"), _tr("Darken"), _tr("Multiply"), _tr("Burn"), _tr("Lighten"), _tr("Screen"), _tr("Dodge"), _tr("Add"), _tr("Overlay"), _tr("Soft Light"), _tr("Linear Light"), _tr("Difference"), _tr("Subtract"), _tr("Divide"), _tr("Hue"), _tr("Saturation"), _tr("Color"), _tr("Value")],
+						default_value: 0,
+						output: 0
+					},
+					{
+						name: _tr("use_clamp"),
+						type: "BOOL",
+						default_value: false,
+						output: 0
+					}
+				]
+			}
+		],
+		[ // Vector
+			{
+				id: 0,
+				name: _tr("Bump"),
+				type: "BUMP",
+				x: 0,
+				y: 0,
+				color: 0xff522c99,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Strength"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Distance"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Height"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						// name: _tr("Normal"),
+						name: _tr("Normal Map"),
+						type: "VECTOR",
+						color: -10238109,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Mapping"),
+				type: "MAPPING",
+				x: 0,
+				y: 0,
+				color: 0xff522c99,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Location"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0]),
+						display: 1
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Rotation"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0]),
+						max: 360.0,
+						display: 1
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Scale"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([1.0, 1.0, 1.0]),
+						display: 1
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Normal"),
+				type: "NORMAL",
+				x: 0,
+				y: 0,
+				color: 0xff522c99,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Normal"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Dot"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("Vector"),
+						type: "VECTOR",
+						default_value: f32([0.0, 0.0, 0.0]),
+						output: 0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Vector Curves"),
+				type: "CURVE_VEC",
+				x: 0,
+				y: 0,
+				color: 0xff522c99,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 1.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: [
+					{
+						name: "arm.shader.NodesMaterial.vectorCurvesButton",
+						type: "CUSTOM",
+						default_value: [[f32([0.0, 0.0]), f32([0.0, 0.0])], [f32([0.0, 0.0]), f32([0.0, 0.0])], [f32([0.0, 0.0]), f32([0.0, 0.0])]],
+						output: 0,
+						height: 8.5
+					}
+				]
+			}
+		],
+		[ // Converter
+			{
+				id: 0,
+				name: _tr("Color Ramp"),
+				type: "VALTORGB",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Fac"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 1.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Alpha"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: "arm.shader.NodesMaterial.colorRampButton",
+						type: "CUSTOM",
+						default_value: [f32([1.0, 1.0, 1.0, 1.0, 0.0])],
+						data: 0,
+						output: 0,
+						height: 4.5
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("Combine HSV"),
+				type: "COMBHSV",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("H"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("S"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("V"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Combine RGB"),
+				type: "COMBRGB",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("R"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("G"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("B"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Combine XYZ"),
+				type: "COMBXYZ",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("X"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Y"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Z"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Math"),
+				type: "MATH",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.5
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("operation"),
+						type: "ENUM",
+						data: [_tr("Add"), _tr("Subtract"), _tr("Multiply"), _tr("Divide"), _tr("Power"), _tr("Logarithm"), _tr("Square Root"), _tr("Absolute"), _tr("Minimum"), _tr("Maximum"), _tr("Less Than"), _tr("Greater Than"), _tr("Round"), _tr("Floor"), _tr("Ceil"), _tr("Fract"), _tr("Modulo"), _tr("Sine"), _tr("Cosine"), _tr("Tangent"), _tr("Arcsine"), _tr("Arccosine"), _tr("Arctangent"), _tr("Arctan2")],
+						default_value: 0,
+						output: 0
+					},
+					{
+						name: _tr("use_clamp"),
+						type: "BOOL",
+						default_value: false,
+						output: 0
+					}
+				]
+			},
+			{
+				id: 0,
+				name: _tr("RGB to BW"),
+				type: "RGBTOBW",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.0, 0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Val"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Separate HSV"),
+				type: "SEPHSV",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.5, 0.5, 0.5, 1.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("H"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("S"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("V"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Separate RGB"),
+				type: "SEPRGB",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Color"),
+						type: "RGBA",
+						color: 0xffc7c729,
+						default_value: f32([0.8, 0.8, 0.8, 1.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("R"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("G"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("B"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Separate XYZ"),
+				type: "SEPXYZ",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("X"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Y"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Z"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: []
+			},
+			{
+				id: 0,
+				name: _tr("Vector Math"),
+				type: "VECT_MATH",
+				x: 0,
+				y: 0,
+				color: 0xff62676d,
+				inputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					}
+				],
+				outputs: [
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Vector"),
+						type: "VECTOR",
+						color: 0xff6363c7,
+						default_value: f32([0.0, 0.0, 0.0])
+					},
+					{
+						id: 0,
+						node_id: 0,
+						name: _tr("Value"),
+						type: "VALUE",
+						color: 0xffa1a1a1,
+						default_value: 0.0
+					}
+				],
+				buttons: [
+					{
+						name: _tr("operation"),
+						type: "ENUM",
+						data: [_tr("Add"), _tr("Subtract"), _tr("Average"), _tr("Dot Product"), _tr("Cross Product"), _tr("Normalize")],
+						default_value: 0,
+						output: 0
+					}
+				]
+			}
+		],
+		[ // Input
+			{
+				id: 0,
+				name: _tr("New Group"),
+				type: "GROUP",
+				x: 0,
+				y: 0,
+				color: 0xffb34f5a,
+				inputs: [],
+				outputs: [],
+				buttons: [
+					{
+						name: "arm.shader.NodesMaterial.newGroupButton",
+						type: "CUSTOM",
+						height: 1
+					}
+				]
+			}
+		]
+	];
+
+	@:keep
+	@:access(zui.Zui)
+	public static function vectorCurvesButton(ui: Zui, nodes: Nodes, node: TNode) {
+		var but = node.buttons[0];
+		var nhandle = Id.handle().nest(node.id);
+		ui.row([1 / 3, 1 / 3, 1 / 3]);
+		ui.radio(nhandle.nest(0).nest(1), 0, "X");
+		ui.radio(nhandle.nest(0).nest(1), 1, "Y");
+		ui.radio(nhandle.nest(0).nest(1), 2, "Z");
+		// Preview
+		var axis = nhandle.nest(0).nest(1).position;
+		var val: Array<kha.arrays.Float32Array> = but.default_value[axis]; // [ [[x, y], [x, y], ..], [[x, y]], ..]
+		var num = val.length;
+		// for (i in 0...num) { ui.line(); }
+		ui._y += nodes.LINE_H() * 5;
+		// Edit
+		ui.row([1 / 5, 1 / 5, 3 / 5]);
+		if (ui.button("+")) {
+			var f32 = new kha.arrays.Float32Array(2);
+			f32[0] = 0; f32[1] = 0;
+			val.push(f32);
+		}
+		if (ui.button("-")) {
+			if (val.length > 2) { val.pop(); }
+		}
+		var i = Std.int(ui.slider(nhandle.nest(0).nest(2).nest(axis, {position: 0}), "Index", 0, num - 1, false, 1, true, Left));
+		ui.row([1 / 2, 1 / 2]);
+		nhandle.nest(0).nest(3).value = val[i][0];
+		nhandle.nest(0).nest(4).value = val[i][1];
+		val[i][0] = ui.slider(nhandle.nest(0).nest(3, {value: 0}), "X", -1, 1, true, 100, true, Left);
+		val[i][1] = ui.slider(nhandle.nest(0).nest(4, {value: 0}), "Y", -1, 1, true, 100, true, Left);
+	}
+
+	@:keep
+	@:access(zui.Zui)
+	public static function colorRampButton(ui: Zui, nodes: Nodes, node: TNode) {
+		var but = node.buttons[0];
+		var nhandle = Id.handle().nest(node.id);
+		var nx = ui._x;
+		var ny = ui._y;
+
+		// Preview
+		var vals: Array<kha.arrays.Float32Array> = but.default_value; // [[r, g, b, a, pos], ..]
+		var sw = ui._w / nodes.SCALE();
+		for (val in vals) {
+			var pos = val[4];
+			var col = kha.Color.fromFloats(val[0], val[1], val[2]);
+			ui.fill(pos * sw, 0, (1.0 - pos) * sw, nodes.LINE_H() - 2 * nodes.SCALE(), col);
+		}
+		ui._y += nodes.LINE_H();
+		// Edit
+		var ihandle = nhandle.nest(0).nest(2);
+		ui.row([1 / 4, 1 / 4, 2 / 4]);
+		if (ui.button("+")) {
+			var last = vals[vals.length - 1];
+			var f32 = new kha.arrays.Float32Array(5);
+			f32[0] = last[0]; f32[1] = last[1]; f32[2] = last[2]; f32[3] = last[3]; f32[4] = 1.0;
+			vals.push(f32);
+			ihandle.value += 1;
+		}
+		if (ui.button("-") && vals.length > 1) {
+			vals.pop();
+			ihandle.value -= 1;
+		}
+		but.data = ui.combo(nhandle.nest(0).nest(1, {position: but.data}), [tr("Linear"), tr("Constant")], tr("Interpolate"));
+		ui.row([1 / 2, 1 / 2]);
+		var i = Std.int(ui.slider(ihandle, "Index", 0, vals.length - 1, false, 1, true, Left));
+		var val = vals[i];
+		nhandle.nest(0).nest(3).value = val[4];
+		val[4] = ui.slider(nhandle.nest(0).nest(3), "Pos", 0, 1, true, 100, true, Left);
+		var chandle = nhandle.nest(0).nest(4);
+		chandle.color = kha.Color.fromFloats(val[0], val[1], val[2]);
+		if (ui.text("", Right, chandle.color) == Started) {
+			var rx = nx + ui._w - nodes.p(37);
+			var ry = ny - nodes.p(5);
+			nodes._inputStarted = ui.inputStarted = false;
+			nodes.rgbaPopup(ui, chandle, val, Std.int(rx), Std.int(ry + ui.ELEMENT_H()));
+		}
+		val[0] = chandle.color.R;
+		val[1] = chandle.color.G;
+		val[2] = chandle.color.B;
+	}
+
+	@:keep
+	public static function newGroupButton(ui: Zui, nodes: Nodes, node: TNode) {
+		if (node.name == "New Group") {
+			for (i in 1...999) {
+				node.name = tr("Group") + " " + i;
+				var found = false;
+				for (g in Project.materialGroups) if (g.canvas.name == node.name) { found = true; break; }
+				if (!found) break;
+			}
+			var canvas: TNodeCanvas = {
+				name: node.name,
+				nodes: [
+					{
+						id: 0,
+						x: 50,
+						y: 200,
+						name: _tr("Group Input"),
+						type: "GROUP_INPUT",
+						inputs: [],
+						outputs: [],
+						buttons: [
+							{
+								name: "arm.shader.NodesMaterial.groupInputButton",
+								type: "CUSTOM",
+								height: 1
+							}
+						],
+						color: 0xff448c6d
+					},
+					{
+						id: 1,
+						x: 450,
+						y: 200,
+						name: _tr("Group Output"),
+						type: "GROUP_OUTPUT",
+						inputs: [],
+						outputs: [],
+						buttons: [
+							{
+								name: "arm.shader.NodesMaterial.groupOutputButton",
+								type: "CUSTOM",
+								height: 1
+							}
+						],
+						color: 0xff448c6d
+					}
+				],
+				links: []
+			};
+			Project.materialGroups.push({ canvas: canvas, nodes: new Nodes() });
+		}
+
+		var group: TNodeGroup = null;
+		for (g in Project.materialGroups) if (g.canvas.name == node.name) { group = g; break; }
+
+		if (ui.button(tr("Nodes"))) {
+			arm.ui.UINodes.inst.groupStack.push(group);
+		}
+	}
+
+	@:keep
+	public static function groupInputButton(ui: Zui, nodes: Nodes, node: TNode) {
+		addSocketButton(ui, nodes, node, node.outputs);
+	}
+
+	@:keep
+	public static function groupOutputButton(ui: Zui, nodes: Nodes, node: TNode) {
+		addSocketButton(ui, nodes, node, node.inputs);
+	}
+
+	static function addSocketButton(ui: Zui, nodes: Nodes, node: TNode, sockets: Array<TNodeSocket>) {
+		if (ui.button(tr("Add"))) {
+			arm.ui.UIMenu.draw(function(ui: Zui) {
+				ui.text(tr("Socket"), Right, ui.t.HIGHLIGHT_COL);
+				var groupStack = arm.ui.UINodes.inst.groupStack;
+				var c = groupStack[groupStack.length - 1].canvas;
+				if (ui.button(tr("RGBA"), Left)) {
+					sockets.push(createSocket(nodes, node, null, "RGBA", c));
+					syncSockets(node);
+				}
+				if (ui.button(tr("Vector"), Left)) {
+					sockets.push(createSocket(nodes, node, null, "VECTOR", c));
+					syncSockets(node);
+				}
+				if (ui.button(tr("Value"), Left)) {
+					sockets.push(createSocket(nodes, node, null, "VALUE", c));
+					syncSockets(node);
+				}
+			}, 4);
+		}
+	}
+
+	public static function syncSockets(node: TNode) {
+		var groupStack = arm.ui.UINodes.inst.groupStack;
+		var c = groupStack[groupStack.length - 1].canvas;
+		for (m in Project.materials) syncGroupSockets(m.canvas, c.name, node);
+		for (g in Project.materialGroups) syncGroupSockets(g.canvas, c.name, node);
+	}
+
+	static function syncGroupSockets(canvas: TNodeCanvas, groupName: String, node: TNode) {
+		for (n in canvas.nodes) {
+			if (n.type == "GROUP" && n.name == groupName) {
+				var isInputs = node.name == "Group Input";
+				var oldSockets = isInputs ? n.inputs : n.outputs;
+				var sockets = Json.parse(Json.stringify(isInputs ? node.outputs : node.inputs));
+				isInputs ? n.inputs = sockets : n.outputs = sockets;
+				for (s in sockets) s.node_id = n.id;
+				var numSockets = sockets.length < oldSockets.length ? sockets.length : oldSockets.length;
+				for (i in 0...numSockets) {
+					if (sockets[i].type == oldSockets[i].type) {
+						sockets[i].default_value = oldSockets[i].default_value;
+					}
+				}
+			}
+		}
+	}
+
+	public static inline function get_socket_color(type: String): Int {
+		return type == "RGBA" ? 0xffc7c729 : type == "VECTOR" ? 0xff6363c7 : 0xffa1a1a1;
+	}
+
+	public static inline function get_socket_default_value(type: String): Dynamic {
+		return type == "RGBA" ? f32([0.8, 0.8, 0.8, 1.0]) : type == "VECTOR" ? f32([0.0, 0.0, 0.0]) : 0.0;
+	}
+
+	public static inline function get_socket_name(type: String): String {
+		return type == "RGBA" ? _tr("Color") : type == "VECTOR" ? _tr("Vector") : _tr("Value");
+	}
+
+	public static function createSocket(nodes: Nodes, node: TNode, name: String, type: String, canvas: TNodeCanvas, min = 0.0, max = 1.0, default_value: Dynamic = null): TNodeSocket {
+		return {
+			id: nodes.getSocketId(canvas.nodes),
+			node_id: node.id,
+			name: name == null ? get_socket_name(type) : name,
+			type: type,
+			color: get_socket_color(type),
+			default_value: default_value == null ? get_socket_default_value(type) : default_value,
+			min: min,
+			max: max
+		}
+	}
+
+	public static function getTNode(nodeType: String): TNode {
+		for (c in list) for (n in c) if (n.type == nodeType) return n;
+		return null;
+	}
+
+	public static function createNode(nodeType: String, group: TNodeGroup = null): TNode {
+		var n = getTNode(nodeType);
+		if (n == null) return null;
+		var canvas = group != null ? group.canvas : Context.material.canvas;
+		var nodes = group != null ? group.nodes : Context.material.nodes;
+		var node = arm.ui.UINodes.makeNode(n, nodes, canvas);
+		canvas.nodes.push(node);
+		return node;
+	}
+
+	static function f32(ar: Array<kha.FastFloat>): kha.arrays.Float32Array {
+		var res = new kha.arrays.Float32Array(ar.length);
+		for (i in 0...ar.length) res[i] = ar[i];
+		return res;
+	}
+}

+ 524 - 0
Sources/arm/shader/ShaderFunctions.hx

@@ -0,0 +1,524 @@
+package arm.shader;
+
+class ShaderFunctions {
+
+	public static var str_tex_checker = "
+vec3 tex_checker(const vec3 co, const vec3 col1, const vec3 col2, const float scale) {
+	// Prevent precision issues on unit coordinates
+	vec3 p = (co + 0.000001 * 0.999999) * scale;
+	float xi = abs(floor(p.x));
+	float yi = abs(floor(p.y));
+	float zi = abs(floor(p.z));
+	bool check = ((mod(xi, 2.0) == mod(yi, 2.0)) == bool(mod(zi, 2.0)));
+	return check ? col1 : col2;
+}
+float tex_checker_f(const vec3 co, const float scale) {
+	vec3 p = (co + 0.000001 * 0.999999) * scale;
+	float xi = abs(floor(p.x));
+	float yi = abs(floor(p.y));
+	float zi = abs(floor(p.z));
+	return float((mod(xi, 2.0) == mod(yi, 2.0)) == bool(mod(zi, 2.0)));
+}
+";
+
+	// Created by inigo quilez - iq/2013
+	// License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
+	public static var str_tex_voronoi = "
+vec4 tex_voronoi(const vec3 x, textureArg(snoise256)) {
+	vec3 p = floor(x);
+	vec3 f = fract(x);
+	float id = 0.0;
+	float res = 100.0;
+	for (int k = -1; k <= 1; k++)
+	for (int j = -1; j <= 1; j++)
+	for (int i = -1; i <= 1; i++) {
+		vec3 b = vec3(float(i), float(j), float(k));
+		vec3 pb = p + b;
+		vec3 r = vec3(b) - f + texture(snoise256, (pb.xy + vec2(3.0, 1.0) * pb.z + 0.5) / 256.0).xyz;
+		float d = dot(r, r);
+		if (d < res) {
+			id = dot(p + b, vec3(1.0, 57.0, 113.0));
+			res = d;
+		}
+	}
+	vec3 col = 0.5 + 0.5 * cos(id * 0.35 + vec3(0.0, 1.0, 2.0));
+	return vec4(col, sqrt(res));
+}
+";
+
+	// By Morgan McGuire @morgan3d, http://graphicscodex.com Reuse permitted under the BSD license.
+	// https://www.shadertoy.com/view/4dS3Wd
+	public static var str_tex_noise = "
+float hash(float n) { return fract(sin(n) * 1e4); }
+float tex_noise_f(vec3 x) {
+    const vec3 step = vec3(110, 241, 171);
+    vec3 i = floor(x);
+    vec3 f = fract(x);
+    float n = dot(i, step);
+    vec3 u = f * f * (3.0 - 2.0 * f);
+    return mix(mix(mix(hash(n + dot(step, vec3(0, 0, 0))), hash(n + dot(step, vec3(1, 0, 0))), u.x),
+                   mix(hash(n + dot(step, vec3(0, 1, 0))), hash(n + dot(step, vec3(1, 1, 0))), u.x), u.y),
+               mix(mix(hash(n + dot(step, vec3(0, 0, 1))), hash(n + dot(step, vec3(1, 0, 1))), u.x),
+                   mix(hash(n + dot(step, vec3(0, 1, 1))), hash(n + dot(step, vec3(1, 1, 1))), u.x), u.y), u.z);
+}
+float tex_noise(vec3 p) {
+	p *= 1.25;
+	float f = 0.5 * tex_noise_f(p); p *= 2.01;
+	f += 0.25 * tex_noise_f(p); p *= 2.02;
+	f += 0.125 * tex_noise_f(p); p *= 2.03;
+	f += 0.0625 * tex_noise_f(p);
+	return 1.0 - f;
+}
+";
+
+	// Based on noise created by Nikita Miropolskiy, nikat/2013
+	// Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
+	public static var str_tex_musgrave = "
+vec3 random3(const vec3 c) {
+	float j = 4096.0 * sin(dot(c, vec3(17.0, 59.4, 15.0)));
+	vec3 r;
+	r.z = fract(512.0 * j);
+	j *= 0.125;
+	r.x = fract(512.0 * j);
+	j *= 0.125;
+	r.y = fract(512.0 * j);
+	return r - 0.5;
+}
+float tex_musgrave_f(const vec3 p) {
+	const float F3 = 0.3333333;
+	const float G3 = 0.1666667;
+	vec3 s = floor(p + dot(p, vec3(F3, F3, F3)));
+	vec3 x = p - s + dot(s, vec3(G3, G3, G3));
+	vec3 e = step(vec3(0.0, 0.0, 0.0), x - x.yzx);
+	vec3 i1 = e*(1.0 - e.zxy);
+	vec3 i2 = 1.0 - e.zxy*(1.0 - e);
+	vec3 x1 = x - i1 + G3;
+	vec3 x2 = x - i2 + 2.0*G3;
+	vec3 x3 = x - 1.0 + 3.0*G3;
+	vec4 w, d;
+	w.x = dot(x, x);
+	w.y = dot(x1, x1);
+	w.z = dot(x2, x2);
+	w.w = dot(x3, x3);
+	w = max(0.6 - w, 0.0);
+	d.x = dot(random3(s), x);
+	d.y = dot(random3(s + i1), x1);
+	d.z = dot(random3(s + i2), x2);
+	d.w = dot(random3(s + 1.0), x3);
+	w *= w;
+	w *= w;
+	d *= w;
+	return clamp(dot(d, vec4(52.0, 52.0, 52.0, 52.0)), 0.0, 1.0);
+}
+";
+
+	public static var str_hue_sat = "
+vec3 hsv_to_rgb(const vec3 c) {
+	const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+vec3 rgb_to_hsv(const vec3 c) {
+	const vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+	vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+	vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+	float d = q.x - min(q.w, q.y);
+	float e = 1.0e-10;
+	return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+}
+vec3 hue_sat(const vec3 col, const vec4 shift) {
+	vec3 hsv = rgb_to_hsv(col);
+	hsv.x += shift.x;
+	hsv.y *= shift.y;
+	hsv.z *= shift.z;
+	return mix(hsv_to_rgb(hsv), col, shift.w);
+}
+";
+
+	// https://twitter.com/Donzanoid/status/903424376707657730
+	public static var str_wavelength_to_rgb = "
+vec3 wavelength_to_rgb(const float t) {
+	vec3 r = t * 2.1 - vec3(1.8, 1.14, 0.3);
+	return 1.0 - r * r;
+}
+";
+
+	public static var str_tex_magic = "
+vec3 tex_magic(const vec3 p) {
+	float a = 1.0 - (sin(p.x) + sin(p.y));
+	float b = 1.0 - sin(p.x - p.y);
+	float c = 1.0 - sin(p.x + p.y);
+	return vec3(a, b, c);
+}
+float tex_magic_f(const vec3 p) {
+	vec3 c = tex_magic(p);
+	return (c.x + c.y + c.z) / 3.0;
+}
+";
+
+	public static var str_tex_brick = "
+float tex_brick_noise(int n) { /* fast integer noise */
+	int nn;
+	n = (n >> 13) ^ n;
+	nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff;
+	return 0.5f * float(nn) / 1073741824.0;
+}
+vec3 tex_brick(vec3 p, const vec3 c1, const vec3 c2, const vec3 c3) {
+	vec3 brickSize = vec3(0.9, 0.49, 0.49);
+	vec3 mortarSize = vec3(0.05, 0.1, 0.1);
+	p /= brickSize / 2;
+	if (fract(p.y * 0.5) > 0.5) p.x += 0.5;
+	float col = floor(p.x / (brickSize.x + (mortarSize.x * 2.0)));
+	float row = p.y;
+	p = fract(p);
+	vec3 b = step(p, 1.0 - mortarSize);
+	float tint = min(max(tex_brick_noise((int(col) << 16) + (int(row) & 0xFFFF)), 0.0), 1.0);
+	return mix(c3, mix(c1, c2, tint), b.x * b.y * b.z);
+}
+float tex_brick_f(vec3 p) {
+	p /= vec3(0.9, 0.49, 0.49) / 2;
+	if (fract(p.y * 0.5) > 0.5) p.x += 0.5;
+	p = fract(p);
+	vec3 b = step(p, vec3(0.95, 0.9, 0.9));
+	return mix(1.0, 0.0, b.x * b.y * b.z);
+}
+";
+
+	public static var str_tex_wave = "
+float tex_wave_f(const vec3 p) {
+	return 1.0 - sin((p.x + p.y) * 10.0);
+}
+";
+
+	public static var str_brightcontrast = "
+vec3 brightcontrast(const vec3 col, const float bright, const float contr) {
+	float a = 1.0 + contr;
+	float b = bright - contr * 0.5;
+	return max(a * col + b, 0.0);
+}
+";
+
+//
+
+	#if rp_voxelao
+	public static var str_traceAO = '
+float traceConeAO(sampler3D voxels, const vec3 origin, vec3 dir, const float aperture, const float maxDist, const float offset) {
+	const ivec3 voxelgiResolution = ivec3(256, 256, 256);
+	const float voxelgiStep = 1.0;
+	const float VOXEL_SIZE = (2.0 / voxelgiResolution.x) * voxelgiStep;
+	dir = normalize(dir);
+	float sampleCol = 0.0;
+	float dist = offset;
+	float diam = dist * aperture;
+	vec3 samplePos;
+	while (sampleCol < 1.0 && dist < maxDist) {
+		samplePos = dir * dist + origin;
+		float mip = max(log2(diam * voxelgiResolution.x), 0);
+		float mipSample = textureLod(voxels, samplePos * 0.5 + vec3(0.5, 0.5, 0.5), mip).r;
+		sampleCol += (1 - sampleCol) * mipSample;
+		dist += max(diam / 2, VOXEL_SIZE);
+		diam = dist * aperture;
+	}
+	return sampleCol;
+}
+vec3 tangent(const vec3 n) {
+	vec3 t1 = cross(n, vec3(0, 0, 1));
+	vec3 t2 = cross(n, vec3(0, 1, 0));
+	if (length(t1) > length(t2)) return normalize(t1);
+	else return normalize(t2);
+}
+float traceAO(const vec3 origin, const vec3 normal, const float vrange, const float voffset) {
+	const float angleMix = 0.5f;
+	const float aperture = 0.55785173935;
+	vec3 o1 = normalize(tangent(normal));
+	vec3 o2 = normalize(cross(o1, normal));
+	vec3 c1 = 0.5f * (o1 + o2);
+	vec3 c2 = 0.5f * (o1 - o2);
+	float MAX_DISTANCE = 1.73205080757 * 2.0 * vrange;
+	const ivec3 voxelgiResolution = ivec3(256, 256, 256);
+	const float voxelgiStep = 1.0;
+	const float VOXEL_SIZE = (2.0 / voxelgiResolution.x) * voxelgiStep;
+	float offset = 1.5 * VOXEL_SIZE * 2.5 * voffset;
+	float col = traceConeAO(voxels, origin, normal, aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, o1, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, o2, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, -c1, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, -c2, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, -o1, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, -o2, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, c1, angleMix), aperture, MAX_DISTANCE, offset);
+	col += traceConeAO(voxels, origin, mix(normal, c2, angleMix), aperture, MAX_DISTANCE, offset);
+	return col / 9.0;
+}
+';
+	#end
+
+	public static var str_cotangentFrame = "
+mat3 cotangentFrame(const vec3 n, const vec3 p, const vec2 duv1, const vec2 duv2) {
+	vec3 dp1 = dFdx(p);
+	vec3 dp2 = dFdy(p);
+	vec3 dp2perp = cross(dp2, n);
+	vec3 dp1perp = cross(n, dp1);
+	vec3 t = dp2perp * duv1.x + dp1perp * duv2.x;
+	vec3 b = dp2perp * duv1.y + dp1perp * duv2.y;
+	float invmax = inversesqrt(max(dot(t, t), dot(b, b)));
+	return mat3(t * invmax, b * invmax, n);
+}
+mat3 cotangentFrame(const vec3 n, const vec3 p, const vec2 texCoord) {
+	return cotangentFrame(n, p, dFdx(texCoord), dFdy(texCoord));
+}
+";
+
+	public static var str_octahedronWrap = "
+vec2 octahedronWrap(const vec2 v) {
+	return (1.0 - abs(v.yx)) * (vec2(v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0));
+}
+";
+
+	public static var str_packFloatInt16 = "
+float packFloatInt16(const float f, const uint i) {
+	const float prec = float(1 << 16);
+	const float maxi = float(1 << 4);
+	const float precMinusOne = prec - 1.0;
+	const float t1 = ((prec / maxi) - 1.0) / precMinusOne;
+	const float t2 = (prec / maxi) / precMinusOne;
+	return t1 * f + t2 * float(i);
+}
+";
+
+	#if arm_skin
+	public static var str_getSkinningDualQuat = "
+void getSkinningDualQuat(const ivec4 bone, vec4 weight, out vec4 A, inout vec4 B) {
+	ivec4 bonei = bone * 2;
+	mat4 matA = mat4(
+		skinBones[bonei.x],
+		skinBones[bonei.y],
+		skinBones[bonei.z],
+		skinBones[bonei.w]);
+	mat4 matB = mat4(
+		skinBones[bonei.x + 1],
+		skinBones[bonei.y + 1],
+		skinBones[bonei.z + 1],
+		skinBones[bonei.w + 1]);
+	weight.xyz *= sign(mul(matA, matA[3])).xyz;
+	A = mul(weight, matA);
+	B = mul(weight, matB);
+	float invNormA = 1.0 / length(A);
+	A *= invNormA;
+	B *= invNormA;
+}
+";
+	#end
+
+	public static var str_createBasis = "
+void createBasis(vec3 normal, out vec3 tangent, out vec3 binormal) {
+	tangent = normalize(cameraRight - normal * dot(cameraRight, normal));
+	binormal = cross(tangent, normal);
+}
+";
+
+	public static var str_shIrradiance =
+#if kha_metal
+"vec3 shIrradiance(const vec3 nor, constant vec4 shirr[7]) {
+	const float c1 = 0.429043;
+	const float c2 = 0.511664;
+	const float c3 = 0.743125;
+	const float c4 = 0.886227;
+	const float c5 = 0.247708;
+	vec3 cl00 = vec3(shirr[0].x, shirr[0].y, shirr[0].z);
+	vec3 cl1m1 = vec3(shirr[0].w, shirr[1].x, shirr[1].y);
+	vec3 cl10 = vec3(shirr[1].z, shirr[1].w, shirr[2].x);
+	vec3 cl11 = vec3(shirr[2].y, shirr[2].z, shirr[2].w);
+	vec3 cl2m2 = vec3(shirr[3].x, shirr[3].y, shirr[3].z);
+	vec3 cl2m1 = vec3(shirr[3].w, shirr[4].x, shirr[4].y);
+	vec3 cl20 = vec3(shirr[4].z, shirr[4].w, shirr[5].x);
+	vec3 cl21 = vec3(shirr[5].y, shirr[5].z, shirr[5].w);
+	vec3 cl22 = vec3(shirr[6].x, shirr[6].y, shirr[6].z);
+	return (
+		c1 * cl22 * (nor.y * nor.y - (-nor.z) * (-nor.z)) +
+		c3 * cl20 * nor.x * nor.x +
+		c4 * cl00 -
+		c5 * cl20 +
+		2.0 * c1 * cl2m2 * nor.y * (-nor.z) +
+		2.0 * c1 * cl21  * nor.y * nor.x +
+		2.0 * c1 * cl2m1 * (-nor.z) * nor.x +
+		2.0 * c2 * cl11  * nor.y +
+		2.0 * c2 * cl1m1 * (-nor.z) +
+		2.0 * c2 * cl10  * nor.x
+	);
+}
+";
+#else
+"vec3 shIrradiance(const vec3 nor, const vec4 shirr[7]) {
+	const float c1 = 0.429043;
+	const float c2 = 0.511664;
+	const float c3 = 0.743125;
+	const float c4 = 0.886227;
+	const float c5 = 0.247708;
+	vec3 cl00 = vec3(shirr[0].x, shirr[0].y, shirr[0].z);
+	vec3 cl1m1 = vec3(shirr[0].w, shirr[1].x, shirr[1].y);
+	vec3 cl10 = vec3(shirr[1].z, shirr[1].w, shirr[2].x);
+	vec3 cl11 = vec3(shirr[2].y, shirr[2].z, shirr[2].w);
+	vec3 cl2m2 = vec3(shirr[3].x, shirr[3].y, shirr[3].z);
+	vec3 cl2m1 = vec3(shirr[3].w, shirr[4].x, shirr[4].y);
+	vec3 cl20 = vec3(shirr[4].z, shirr[4].w, shirr[5].x);
+	vec3 cl21 = vec3(shirr[5].y, shirr[5].z, shirr[5].w);
+	vec3 cl22 = vec3(shirr[6].x, shirr[6].y, shirr[6].z);
+	return (
+		c1 * cl22 * (nor.y * nor.y - (-nor.z) * (-nor.z)) +
+		c3 * cl20 * nor.x * nor.x +
+		c4 * cl00 -
+		c5 * cl20 +
+		2.0 * c1 * cl2m2 * nor.y * (-nor.z) +
+		2.0 * c1 * cl21  * nor.y * nor.x +
+		2.0 * c1 * cl2m1 * (-nor.z) * nor.x +
+		2.0 * c2 * cl11  * nor.y +
+		2.0 * c2 * cl1m1 * (-nor.z) +
+		2.0 * c2 * cl10  * nor.x
+	);
+}
+";
+#end
+
+	public static var str_envMapEquirect = "
+vec2 envMapEquirect(const vec3 normal, const float angle) {
+	const float PI = 3.1415926535;
+	const float PI2 = PI * 2.0;
+	float phi = acos(normal.z);
+	float theta = atan(-normal.y, normal.x) + PI + angle;
+	return vec2(theta / PI2, phi / PI);
+}
+";
+
+	// Linearly Transformed Cosines
+	// https://eheitzresearch.wordpress.com/415-2/
+	public static var str_ltcEvaluate = "
+float integrateEdge(vec3 v1, vec3 v2) {
+	float cosTheta = dot(v1, v2);
+	float theta = acos(cosTheta);
+	float res = cross(v1, v2).z * ((theta > 0.001) ? theta / sin(theta) : 1.0);
+	return res;
+}
+float ltcEvaluate(vec3 N, vec3 V, float dotNV, vec3 P, mat3 Minv, vec3 points0, vec3 points1, vec3 points2, vec3 points3) {
+	vec3 T1 = normalize(V - N * dotNV);
+	vec3 T2 = cross(N, T1);
+	Minv = mul(transpose(mat3(T1, T2, N)), Minv);
+	vec3 L0 = mul((points0 - P), Minv);
+	vec3 L1 = mul((points1 - P), Minv);
+	vec3 L2 = mul((points2 - P), Minv);
+	vec3 L3 = mul((points3 - P), Minv);
+	vec3 L4 = vec3(0.0, 0.0, 0.0);
+	int n = 0;
+	int config = 0;
+	if (L0.z > 0.0) config += 1;
+	if (L1.z > 0.0) config += 2;
+	if (L2.z > 0.0) config += 4;
+	if (L3.z > 0.0) config += 8;
+	if (config == 0) {}
+	else if (config == 1) {
+		n = 3;
+		L1 = -L1.z * L0 + L0.z * L1;
+		L2 = -L3.z * L0 + L0.z * L3;
+	}
+	else if (config == 2) {
+		n = 3;
+		L0 = -L0.z * L1 + L1.z * L0;
+		L2 = -L2.z * L1 + L1.z * L2;
+	}
+	else if (config == 3) {
+		n = 4;
+		L2 = -L2.z * L1 + L1.z * L2;
+		L3 = -L3.z * L0 + L0.z * L3;
+	}
+	else if (config == 4) {
+		n = 3;
+		L0 = -L3.z * L2 + L2.z * L3;
+		L1 = -L1.z * L2 + L2.z * L1;
+	}
+	else if (config == 5) { n = 0; }
+	else if (config == 6) {
+		n = 4;
+		L0 = -L0.z * L1 + L1.z * L0;
+		L3 = -L3.z * L2 + L2.z * L3;
+	}
+	else if (config == 7) {
+		n = 5;
+		L4 = -L3.z * L0 + L0.z * L3;
+		L3 = -L3.z * L2 + L2.z * L3;
+	}
+	else if (config == 8) {
+		n = 3;
+		L0 = -L0.z * L3 + L3.z * L0;
+		L1 = -L2.z * L3 + L3.z * L2;
+		L2 =  L3;
+	}
+	else if (config == 9) {
+		n = 4;
+		L1 = -L1.z * L0 + L0.z * L1;
+		L2 = -L2.z * L3 + L3.z * L2;
+	}
+	else if (config == 10) { n = 0; }
+	else if (config == 11) {
+		n = 5;
+		L4 = L3;
+		L3 = -L2.z * L3 + L3.z * L2;
+		L2 = -L2.z * L1 + L1.z * L2;
+	}
+	else if (config == 12) {
+		n = 4;
+		L1 = -L1.z * L2 + L2.z * L1;
+		L0 = -L0.z * L3 + L3.z * L0;
+	}
+	else if (config == 13) {
+		n = 5;
+		L4 = L3;
+		L3 = L2;
+		L2 = -L1.z * L2 + L2.z * L1;
+		L1 = -L1.z * L0 + L0.z * L1;
+	}
+	else if (config == 14) {
+		n = 5;
+		L4 = -L0.z * L3 + L3.z * L0;
+		L0 = -L0.z * L1 + L1.z * L0;
+	}
+	else if (config == 15) { n = 4; }
+	if (n == 0) return 0.0;
+	if (n == 3) L3 = L0;
+	if (n == 4) L4 = L0;
+	L0 = normalize(L0);
+	L1 = normalize(L1);
+	L2 = normalize(L2);
+	L3 = normalize(L3);
+	L4 = normalize(L4);
+	float sum = 0.0;
+	sum += integrateEdge(L0, L1);
+	sum += integrateEdge(L1, L2);
+	sum += integrateEdge(L2, L3);
+	if (n >= 4) sum += integrateEdge(L3, L4);
+	if (n == 5) sum += integrateEdge(L4, L0);
+	return max(0.0, -sum);
+}
+";
+
+	public static var str_get_pos_from_depth = "
+vec3 get_pos_from_depth(vec2 uv, mat4 invVP, textureArg(gbufferD)) {
+	#if defined(HLSL) || defined(METAL) || defined(SPIRV)
+	float depth = textureLod(gbufferD, vec2(uv.x, 1.0 - uv.y), 0.0).r;
+	#else
+	float depth = textureLod(gbufferD, uv, 0.0).r;
+	#endif
+	vec4 wpos = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
+	wpos = mul(wpos, invVP);
+	return wpos.xyz / wpos.w;
+}
+";
+
+	public static var str_get_nor_from_depth = "
+vec3 get_nor_from_depth(vec3 p0, vec2 uv, mat4 invVP, vec2 texStep, textureArg(gbufferD)) {
+	vec3 p1 = get_pos_from_depth(uv + vec2(texStep.x * 4.0, 0.0), invVP, texturePass(gbufferD));
+	vec3 p2 = get_pos_from_depth(uv + vec2(0.0, texStep.y * 4.0), invVP, texturePass(gbufferD));
+	return normalize(cross(p2 - p0, p1 - p0));
+}
+";
+
+}

+ 12 - 0
Sources/arm/sys/BuildMacros.hx

@@ -0,0 +1,12 @@
+package arm.sys;
+
+import haxe.macro.Context;
+
+class BuildMacros {
+
+	#if krom_android
+	macro public static function readDirectory(path: String): ExprOf<Array<String>> {
+		return Context.makeExpr(sys.FileSystem.readDirectory(path), Context.currentPos());
+	}
+	#end
+}

+ 177 - 0
Sources/arm/sys/File.hx

@@ -0,0 +1,177 @@
+package arm.sys;
+
+import haxe.io.Bytes;
+
+class File {
+
+	#if krom_windows
+	static inline var cmd_dir = "dir /b";
+	static inline var cmd_dir_nofile = "dir /b /ad";
+	static inline var cmd_copy = "copy";
+	static inline var cmd_del = "del /f";
+	#else
+	static inline var cmd_dir = "ls";
+	static inline var cmd_dir_nofile = "ls";
+	static inline var cmd_copy = "cp";
+	static inline var cmd_del = "rm";
+	#end
+
+	static var cloud: Map<String, Array<String>> = null;
+	static var cloudSizes: Map<String, Int> = null;
+
+	#if krom_android
+	static var internal: Map<String, Array<String>> = null; // .apk contents
+	#end
+
+	public static function readDirectory(path: String, foldersOnly = false): Array<String> {
+		if (path.startsWith("cloud")) {
+			if (cloud == null) initCloud();
+			var files = cloud.get(path.replace("\\", "/"));
+			return files != null ? files : [];
+		}
+		#if krom_android
+		path = path.replace("//", "/");
+		if (internal == null) {
+			internal = [];
+			internal.set("/data/plugins", BuildMacros.readDirectory("krom/data/plugins"));
+			internal.set("/data/export_presets", BuildMacros.readDirectory("krom/data/export_presets"));
+			internal.set("/data/keymap_presets", BuildMacros.readDirectory("krom/data/keymap_presets"));
+			internal.set("/data/locale", BuildMacros.readDirectory("krom/data/locale"));
+			internal.set("/data/meshes", BuildMacros.readDirectory("krom/data/meshes"));
+			internal.set("/data/themes", BuildMacros.readDirectory("krom/data/themes"));
+		}
+		if (internal.exists(path)) return internal.get(path);
+		#end
+		return Krom.readDirectory(path, foldersOnly).split("\n");
+	}
+
+	public static function createDirectory(path: String) {
+		#if krom_windows
+		Krom.sysCommand("mkdir " + path); // -p by default
+		#else
+		Krom.sysCommand("mkdir -p " + path);
+		#end
+	}
+
+	public static function copy(srcPath: String, dstPath: String) {
+		Krom.sysCommand(cmd_copy + ' "' + srcPath + '" "' + dstPath + '"');
+	}
+
+	public static function start(path: String) {
+		#if krom_windows
+		Krom.sysCommand('start "" "' + path + '"');
+		#elseif krom_linux
+		Krom.sysCommand('xdg-open "' + path + '"');
+		#else
+		Krom.sysCommand('open "' + path + '"');
+		#end
+	}
+
+	public static function explorer(url: String) {
+		#if krom_windows
+		Krom.sysCommand('explorer "' + url + '"');
+		#elseif krom_linux
+		Krom.sysCommand('xdg-open "' + url + '"');
+		#elseif (krom_android || krom_ios)
+		Krom.loadUrl(url);
+		#else
+		Krom.sysCommand('open "' + url + '"');
+		#end
+	}
+
+	public static function delete(path: String) {
+		Krom.sysCommand(cmd_del + ' "' + path + '"');
+	}
+
+	public static function exists(path: String): Bool {
+		return Krom.fileExists(path);
+	}
+
+	public static function download(url: String, dstPath: String, size: Null<Int> = null) {
+		#if krom_windows
+		if (size == null) {
+			Krom.sysCommand('powershell -c "Invoke-WebRequest -Uri ' + url + " -OutFile '" + dstPath + "'");
+		}
+		else {
+			Krom.httpRequest(url, size, function(ab: js.lib.ArrayBuffer) {
+				if (ab != null) Krom.fileSaveBytes(dstPath, ab);
+			});
+		}
+		#else
+		Krom.sysCommand("curl -L " + url + " -o " + dstPath);
+		#end
+	}
+
+	public static function downloadBytes(url: String): Bytes {
+		var save = Path.data() + Path.sep + "download.bin";
+		download(url, save);
+		try {
+			return Bytes.ofData(Krom.loadBlob(save));
+		}
+		catch (e: Dynamic) {
+			return null;
+		}
+	}
+
+	public static function cacheCloud(path: String): String {
+		var dest = Krom.getFilesLocation() + Path.sep + path;
+		if (!File.exists(dest)) {
+			var fileDir = dest.substr(0, dest.lastIndexOf(Path.sep));
+			File.createDirectory(fileDir);
+			#if krom_windows
+			path = path.replace("\\", "/");
+			#end
+			var url = Config.raw.server + "/" + path;
+			File.download(url, dest, cloudSizes.get(path));
+			if (!File.exists(dest)) {
+				Log.error(Strings.error5());
+				return null;
+			}
+		}
+		#if krom_darwin
+		return dest;
+		#else
+		return Path.workingDir() + Path.sep + path;
+		#end
+	}
+
+	static function initCloud() {
+		cloud = [];
+		cloudSizes = [];
+		var files: Array<String> = [];
+		var sizes: Array<Int> = [];
+		var bytes = File.downloadBytes(Config.raw.server);
+		var dataPath = Path.data().startsWith(Path.workingDir()) ? Path.data() : Path.workingDir() + Path.sep + Path.data();
+		if (!File.exists(dataPath + Path.sep + "download.bin")) {
+			cloud.set("cloud", []);
+			Log.error(Strings.error5());
+			return;
+		}
+		for (e in Xml.parse(bytes.toString()).firstElement().elementsNamed("Contents")) {
+			for (k in e.elementsNamed("Key")) {
+				files.push(k.firstChild().nodeValue);
+			}
+			for (k in e.elementsNamed("Size")) {
+				sizes.push(Std.parseInt(k.firstChild().nodeValue));
+			}
+		}
+		for (file in files) {
+			if (Path.isFolder(file)) {
+				cloud.set(file.substr(0, file.length - 1), []);
+			}
+		}
+		for (i in 0...files.length) {
+			var file = files[i];
+			var nested = file.indexOf("/") != file.lastIndexOf("/");
+			if (nested) {
+				var delim = Path.isFolder(file) ? file.substr(0, file.length - 1).lastIndexOf("/") : file.lastIndexOf("/");
+				var parent = file.substr(0, delim);
+				var child = Path.isFolder(file) ? file.substring(delim + 1, file.length - 1)  : file.substr(delim + 1);
+				cloud.get(parent).push(child);
+				if (!Path.isFolder(file)) {
+					cloudSizes.set(file, sizes[i]);
+				}
+			}
+		}
+	}
+}

+ 132 - 0
Sources/arm/sys/Path.hx

@@ -0,0 +1,132 @@
+package arm.sys;
+
+import iron.data.Data;
+
+class Path {
+
+	#if krom_windows // no inline for plugin access
+	public static var sep = "\\";
+	#else
+	public static var sep = "/";
+	#end
+
+	public static var meshFormats = ["obj", "fbx", "blend"];
+	public static var textureFormats = ["jpg", "jpeg", "png", "tga", "bmp", "psd", "gif", "hdr"];
+
+	public static var meshImporters = new Map<String, String->(Dynamic->Void)->Void>();
+	public static var textureImporters = new Map<String, String->(kha.Image->Void)->Void>();
+
+	public static var baseColorExt = ["albedo", "alb", "basecol", "basecolor", "diffuse", "diff", "base", "bc", "d", "color", "col"];
+	public static var opacityExt = ["opac", "opacity", "alpha"];
+	public static var normalMapExt = ["normal", "nor", "n", "nrm"];
+	public static var occlusionExt = ["ao", "occlusion", "ambientOcclusion", "o", "occ"];
+	public static var roughnessExt = ["roughness", "rough", "r", "rgh"];
+	public static var metallicExt = ["metallic", "metal", "metalness", "m", "met"];
+	public static var displacementExt = ["displacement", "height", "h", "disp"];
+
+	public static function data(): String {
+		return Krom.getFilesLocation() + Path.sep + Data.dataPath;
+	}
+
+	public static function toRelative(from: String, to: String): String {
+		var a = from.split(Path.sep);
+		var b = to.split(Path.sep);
+		while (a[0] == b[0]) {
+			a.shift();
+			b.shift();
+			if (a.length == 0 || b.length == 0) break;
+		}
+		var base = "";
+		for (i in 0...a.length - 1) base += ".." + Path.sep;
+		base += b.join(Path.sep);
+		return base;
+	}
+
+	public static function baseDir(path: String): String {
+		return path.substr(0, path.lastIndexOf(Path.sep) + 1);
+	}
+
+	public static function workingDir(): String {
+		#if krom_windows
+		var cmd = "cd";
+		#else
+		var cmd = "echo $PWD";
+		#end
+		var save = data() + sep + "tmp.txt";
+		Krom.sysCommand(cmd + ' > "' + save + '"');
+		return haxe.io.Bytes.ofData(Krom.loadBlob(save)).toString().rtrim();
+	}
+
+	public static function isMesh(path: String): Bool {
+		var p = path.toLowerCase();
+		for (s in meshFormats) if (p.endsWith("." + s)) return true;
+		return false;
+	}
+
+	public static function isTexture(path: String): Bool {
+		var p = path.toLowerCase();
+		for (s in textureFormats) if (p.endsWith("." + s)) return true;
+		return false;
+	}
+
+	public static function isFont(path: String): Bool {
+		var p = path.toLowerCase();
+		return p.endsWith(".ttf") ||
+				p.endsWith(".ttc") ||
+				p.endsWith(".otf");
+	}
+
+	public static function isProject(path: String): Bool {
+		var p = path.toLowerCase();
+		return p.endsWith(".arm");
+	}
+
+	public static function isPlugin(path: String): Bool {
+		var p = path.toLowerCase();
+		return p.endsWith(".js");
+			   // p.endsWith(".wasm") ||
+			   // p.endsWith(".zip");
+	}
+
+	public static function isJson(path: String): Bool {
+		var p = path.toLowerCase();
+		return p.endsWith(".json");
+	}
+
+	public static function isKnown(path: String): Bool {
+		return isMesh(path) || isTexture(path) || isFont(path) || isProject(path) || isPlugin(path);
+	}
+
+	static function checkExt(p: String, exts: Array<String>): Bool {
+		p = p.replace("-", "_");
+		for (ext in exts) {
+			if (p.endsWith("_" + ext) ||
+				(p.indexOf("_" + ext + "_") >= 0 && !p.endsWith("_preview") && !p.endsWith("_icon"))) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public static inline function isBaseColorTex(p: String): Bool { return checkExt(p, baseColorExt); }
+	public static inline function isOpacityTex(p: String): Bool { return checkExt(p, opacityExt); }
+	public static inline function isNormalMapTex(p: String): Bool { return checkExt(p, normalMapExt); }
+	public static inline function isOcclusionTex(p: String): Bool { return checkExt(p, occlusionExt); }
+	public static inline function isRoughnessTex(p: String): Bool { return checkExt(p, roughnessExt); }
+	public static inline function isMetallicTex(p: String): Bool { return checkExt(p, metallicExt); }
+	public static inline function isDisplacementTex(p: String): Bool { return checkExt(p, displacementExt); }
+
+	public static function isFolder(p: String): Bool {
+		return p.indexOf(".") == -1;
+	}
+
+	public static function isProtected(): Bool {
+		#if krom_windows
+		return Krom.getFilesLocation().indexOf("Program Files") >= 0;
+		#elseif krom_android
+		return true;
+		#else
+		return false;
+		#end
+	}
+}

+ 8 - 0
Sources/import.hx

@@ -0,0 +1,8 @@
+// Global imports
+
+#if (!macro)
+
+import arm.Translator.tr;
+using StringTools;
+
+#end